generated from boboko/starter
Compare commits
9
Commits
2d1624bcb2
...
elv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9abe45c5f7 | ||
|
|
e57bfe3d42 | ||
|
|
72b9abf92f | ||
|
|
1f0612861b | ||
|
|
b2207a622c | ||
|
|
b87e22381f | ||
|
|
53f30ff51a | ||
|
|
210ed3b094 | ||
|
|
5e2ec7a60a |
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Catalog;
|
||||
|
||||
/**
|
||||
* Presentation shaping — how a storefront product listing/grid card is built:
|
||||
* name, price, image, href. Deliberately not in boboko-core: `href` depends on
|
||||
* this storefront's own routes, and another app on the same core package could
|
||||
* want an entirely different card shape. One place, so HomeController /
|
||||
* CategoryController / ProductController / SearchController don't each
|
||||
* hand-write the same name/price/image/href mapping.
|
||||
*
|
||||
* One source: a localized index array — ProductService::list()/getById()/
|
||||
* random(), and now ProductSearchService::search() too, all return the exact
|
||||
* same document shape (see ProductListingResult), so this is the only mapping
|
||||
* every storefront listing page needs.
|
||||
*/
|
||||
final class ProductCard
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $product one item from ProductService's localized array shape
|
||||
* @return array{name: ?string, price: ?string, image: ?string, href: string}
|
||||
*/
|
||||
public static function fromIndexed(array $product): array
|
||||
{
|
||||
return [
|
||||
'name' => $product['name'],
|
||||
'price' => $product['price'],
|
||||
'image' => $product['media'][0]['url'] ?? null,
|
||||
'href' => route('product.show', ['id' => $product['id']]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,21 @@
|
||||
use Modules\Core\Catalog\Enums\ProductSort;
|
||||
|
||||
/**
|
||||
* The parsed state of a category listing request. The query string is the single
|
||||
* source of truth for sort / filters / page — build one of these from the
|
||||
* request, read the applied values off it, and use query() to build links (sort
|
||||
* options, pagination, "clear filter") that carry the rest of the state along.
|
||||
* The parsed filter/sort/page state of a product-listing request — used by the
|
||||
* category page (scoped to a collection) and the all-products page. The query
|
||||
* string is the single source of truth; build one of these from the request,
|
||||
* read the applied values off it, and use query() to build links (sort options,
|
||||
* pagination, "clear filter") that carry the rest of the state along.
|
||||
*
|
||||
* A param is only ever emitted when it differs from its default, so a pristine
|
||||
* listing is just `/category/{id}` with no query string.
|
||||
* listing has no query string at all.
|
||||
*/
|
||||
final class CategoryListing
|
||||
final class ProductListing
|
||||
{
|
||||
private function __construct(
|
||||
public readonly ?ProductSort $sort,
|
||||
public readonly ?int $minPrice,
|
||||
public readonly ?int $maxPrice,
|
||||
public readonly ?float $minPrice,
|
||||
public readonly ?float $maxPrice,
|
||||
public readonly bool $inStockOnly,
|
||||
public readonly int $page,
|
||||
) {}
|
||||
@@ -29,14 +30,17 @@ public static function fromRequest(Request $request): self
|
||||
{
|
||||
return new self(
|
||||
sort: ProductSort::tryFrom((string) $request->query('sort')),
|
||||
minPrice: self::intOrNull($request->query('price_min')),
|
||||
maxPrice: self::intOrNull($request->query('price_max')),
|
||||
minPrice: self::floatOrNull($request->query('price_min')),
|
||||
maxPrice: self::floatOrNull($request->query('price_max')),
|
||||
inStockOnly: $request->boolean('in_stock'),
|
||||
page: max(1, (int) $request->query('page', 1)),
|
||||
);
|
||||
}
|
||||
|
||||
public function filters(int $collectionId): ProductFilters
|
||||
/**
|
||||
* @param ?int $collectionId scope to a collection (category page); null = every product
|
||||
*/
|
||||
public function filters(?int $collectionId = null): ProductFilters
|
||||
{
|
||||
return new ProductFilters(
|
||||
collectionId: $collectionId,
|
||||
@@ -48,8 +52,7 @@ public function filters(int $collectionId): ProductFilters
|
||||
|
||||
/**
|
||||
* The applied params as a clean array (defaults omitted), with `$overrides`
|
||||
* merged on top — pass `['key' => null]` to drop one. Feeds straight into
|
||||
* route('category.show', ['id' => $id] + $listing->query([...])).
|
||||
* merged on top — pass `['key' => null]` to drop one.
|
||||
*
|
||||
* @param array<string, string|int|null> $overrides
|
||||
* @return array<string, string|int>
|
||||
@@ -68,7 +71,7 @@ public function query(array $overrides = []): array
|
||||
|
||||
/**
|
||||
* Whether the listing is reordered/narrowed enough that it shouldn't be
|
||||
* indexed as its own page (the canonical still points at the bare category
|
||||
* indexed as its own page (the canonical still points at the bare listing
|
||||
* URL either way). A plain in-stock toggle is left indexable.
|
||||
*/
|
||||
public function isRefined(): bool
|
||||
@@ -79,8 +82,8 @@ public function isRefined(): bool
|
||||
|| $this->page > 1;
|
||||
}
|
||||
|
||||
private static function intOrNull(mixed $value): ?int
|
||||
private static function floatOrNull(mixed $value): ?float
|
||||
{
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
return is_numeric($value) ? (float) $value : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Catalog;
|
||||
|
||||
use Closure;
|
||||
use Modules\Core\Catalog\Enums\ProductSort;
|
||||
use Modules\Core\Catalog\Services\ProductSearchService;
|
||||
use Modules\Core\Catalog\Services\ProductService;
|
||||
|
||||
/**
|
||||
* Assembles the data the shared shop listing body (shop/partials/listing.blade.php)
|
||||
* needs — the product page, price-slider bounds, sort links and the "clear price"
|
||||
* link. Used by the category page (scoped to a collection), the all-products
|
||||
* page, and the search page (scoped to a query — see $query below); the `$url`
|
||||
* closure turns a query-param array into a URL for whichever page is calling,
|
||||
* so this class never has to know the route.
|
||||
*/
|
||||
final class ProductListingPage
|
||||
{
|
||||
private const PER_PAGE = 12;
|
||||
|
||||
public function __construct(
|
||||
private readonly ProductService $products,
|
||||
private readonly ProductSearchService $search,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Closure(array<string, string|int>): string $url
|
||||
* @param ?int $collectionId scope to a collection, or null for every product
|
||||
* @param ?string $query scope to a text search — when given, calls
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function build(ProductListing $listing, Closure $url, ?int $collectionId = null, ?string $query = null): array
|
||||
{
|
||||
$filters = $listing->filters($collectionId);
|
||||
|
||||
// Listing reads from the Meilisearch index via ProductService/
|
||||
// ProductSearchService, not Eloquent. Both return a
|
||||
// ProductListingResult — the product page plus the price-slider
|
||||
// bounds (and available tags) from one call; the controller no
|
||||
// longer stitches list()/search() + priceRange() together itself.
|
||||
// Sort/filter/page all come from $listing (the query string).
|
||||
$result = $query !== null
|
||||
? $this->search->search(
|
||||
query: $query,
|
||||
filters: $filters,
|
||||
sort: $listing->sort,
|
||||
perPage: self::PER_PAGE,
|
||||
page: $listing->page,
|
||||
)
|
||||
: $this->products->list(
|
||||
filters: $filters,
|
||||
perPage: self::PER_PAGE,
|
||||
page: $listing->page,
|
||||
sort: $listing->sort,
|
||||
);
|
||||
|
||||
$products = $result->products
|
||||
->through(ProductCard::fromIndexed(...))
|
||||
->appends($listing->query(['page' => null]));
|
||||
|
||||
// Slider bounds — the price span of everything matching the *other*
|
||||
// filters, rounded to whole euros, plus whether the current price params
|
||||
// actually narrow that span. All computed in core
|
||||
$priceBounds = $result->priceBounds;
|
||||
|
||||
return [
|
||||
'listing' => $listing,
|
||||
'products' => $products,
|
||||
'listingAction' => $url([]),
|
||||
'priceFloor' => $priceBounds->floor,
|
||||
'priceCeil' => $priceBounds->ceil,
|
||||
'clearPriceUrl' => $priceBounds->filtered
|
||||
? $url($listing->query(['price_min' => null, 'price_max' => null, 'page' => null]))
|
||||
: null,
|
||||
'sortOptions' => ProductSortOptions::build(
|
||||
$listing->sort,
|
||||
fn (?ProductSort $sort) => $url($listing->query(['sort' => $sort?->value, 'page' => null])),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Catalog;
|
||||
|
||||
use Closure;
|
||||
use Modules\Core\Catalog\Enums\ProductSort;
|
||||
|
||||
/**
|
||||
* Builds the option list for the shared <x-shop.sort> dropdown, so the label
|
||||
* map and "the default sort has no URL param" rule live in one place. Each page
|
||||
* supplies a `$url` closure that turns a sort (or null = default/relevance)
|
||||
* into the right href for that page — category vs. search build their URLs
|
||||
* differently.
|
||||
*/
|
||||
final class ProductSortOptions
|
||||
{
|
||||
/**
|
||||
* @param Closure(?ProductSort): string $url
|
||||
* @return array<int, array{label: string, href: string, current: bool}>
|
||||
*/
|
||||
public static function build(?ProductSort $current, Closure $url): array
|
||||
{
|
||||
$sorts = [
|
||||
null => 'storefront.shop.sort_popularity',
|
||||
ProductSort::PriceAsc->value => 'storefront.shop.sort_price_asc',
|
||||
ProductSort::PriceDesc->value => 'storefront.shop.sort_price_desc',
|
||||
ProductSort::Newest->value => 'storefront.shop.sort_newest',
|
||||
];
|
||||
|
||||
return array_map(function (string $key, string $label) use ($current, $url) {
|
||||
$sort = $key === '' ? null : ProductSort::from($key);
|
||||
|
||||
return [
|
||||
'label' => __($label),
|
||||
'href' => $url($sort),
|
||||
'current' => $sort === $current,
|
||||
];
|
||||
}, array_keys($sorts), array_values($sorts));
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Catalog\CategoryListing;
|
||||
use App\Catalog\ProductListing;
|
||||
use App\Catalog\ProductListingPage;
|
||||
use Illuminate\Http\Response;
|
||||
use Modules\Core\Catalog\Services\CollectionService;
|
||||
use Modules\Core\Catalog\Services\ProductService;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductService $products,
|
||||
private readonly ProductListingPage $listingPage,
|
||||
private readonly CollectionService $collections,
|
||||
) {}
|
||||
|
||||
@@ -19,46 +19,14 @@ public function show(string $locale, int $collection)
|
||||
$collectionData = $this->collections->getById($collection);
|
||||
abort_if($collectionData === null, Response::HTTP_NOT_FOUND);
|
||||
|
||||
$listing = CategoryListing::fromRequest(request());
|
||||
$filters = $listing->filters($collectionData['id']);
|
||||
$perPage = 12;
|
||||
$listing = ProductListing::fromRequest(request());
|
||||
|
||||
// Listing/filtering reads from the Meilisearch index via ProductService,
|
||||
// not Eloquent — see Modules\Core\Catalog\Services\ProductService. list() returns a
|
||||
// real LengthAwarePaginator of plain arrays (already localized/flattened),
|
||||
// not Product models. Sort/filter/page all come from the query string via
|
||||
// CategoryListing, which is the single source of truth for that state.
|
||||
$products = $this->products->list(
|
||||
filters: $filters,
|
||||
perPage: $perPage,
|
||||
page: $listing->page,
|
||||
sort: $listing->sort,
|
||||
)->through(fn (array $product) => [
|
||||
'name' => $product['name'],
|
||||
'price' => $product['price'],
|
||||
'image' => $product['media'][0]['url'] ?? null,
|
||||
'href' => route('product.show', ['id' => $product['id']]),
|
||||
])->appends($listing->query(['page' => null]));
|
||||
$data = $this->listingPage->build(
|
||||
$listing,
|
||||
fn (array $query) => route('category.show', ['id' => $collectionData['id']] + $query),
|
||||
$collectionData['id'],
|
||||
);
|
||||
|
||||
// Slider bounds — the price span of everything matching the *other*
|
||||
// filters (priceRange() drops the price filter itself, so the handles
|
||||
// don't collapse to whatever's already selected). Whole euros.
|
||||
$priceRange = $this->products->priceRange($filters);
|
||||
$priceFloor = $priceRange['min'] !== null ? (int) floor($priceRange['min']) : null;
|
||||
$priceCeil = $priceRange['max'] !== null ? (int) ceil($priceRange['max']) : null;
|
||||
|
||||
// A price param is only a real filter if it's tighter than the bounds —
|
||||
// drives whether the "clear" link shows.
|
||||
$priceFiltered = ($listing->minPrice !== null && $listing->minPrice > ($priceFloor ?? PHP_INT_MIN))
|
||||
|| ($listing->maxPrice !== null && $listing->maxPrice < ($priceCeil ?? PHP_INT_MAX));
|
||||
|
||||
return view('category.show', [
|
||||
'collection' => $collectionData,
|
||||
'products' => $products,
|
||||
'listing' => $listing,
|
||||
'priceFloor' => $priceFloor,
|
||||
'priceCeil' => $priceCeil,
|
||||
'priceFiltered' => $priceFiltered,
|
||||
]);
|
||||
return view('category.show', [...$data, 'collection' => $collectionData]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Catalog\ProductCard;
|
||||
use App\Models\StoicPage;
|
||||
use Lunar\Models\Product;
|
||||
use Modules\Core\Catalog\Services\ProductService;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ProductService $products) {}
|
||||
|
||||
public function index(string $locale)
|
||||
{
|
||||
$page = StoicPage::firstWhere('slug', 'home');
|
||||
@@ -15,16 +18,8 @@ public function index(string $locale)
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$products = Product::with(['variants.prices.currency', 'media'])
|
||||
->inRandomOrder()
|
||||
->limit(13)
|
||||
->get()
|
||||
->map(fn (Product $product) => [
|
||||
'name' => $product->translateAttribute('name'),
|
||||
'price' => $product->variants->first()?->prices->first()?->price->decimal,
|
||||
'image' => $product->media->first()?->getUrl(),
|
||||
'href' => route('product.show', ['id' => $product->id]),
|
||||
]);
|
||||
$products = collect($this->products->random(13))
|
||||
->map(fn (array $product) => ProductCard::fromIndexed($product));
|
||||
|
||||
return view('home', [
|
||||
'page' => $page,
|
||||
|
||||
@@ -2,12 +2,33 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Catalog\ProductListing;
|
||||
use App\Catalog\ProductListingPage;
|
||||
use Illuminate\Http\Response;
|
||||
use Modules\Core\Catalog\Services\ProductService;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ProductService $products) {}
|
||||
public function __construct(
|
||||
private readonly ProductService $products,
|
||||
private readonly ProductListingPage $listingPage,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* All products — the category page without a collection scope. Filters,
|
||||
* sort, pagination and the shared listing body all work identically.
|
||||
*/
|
||||
public function index(string $locale)
|
||||
{
|
||||
$listing = ProductListing::fromRequest(request());
|
||||
|
||||
$data = $this->listingPage->build(
|
||||
$listing,
|
||||
fn (array $query) => route('products', $query),
|
||||
);
|
||||
|
||||
return view('products.index', $data);
|
||||
}
|
||||
|
||||
public function show(string $locale, int $id)
|
||||
{
|
||||
@@ -16,14 +37,7 @@ public function show(string $locale, int $id)
|
||||
|
||||
$collection = $product['collections'][0] ?? null;
|
||||
|
||||
$variantsData = collect($product['variants'])
|
||||
->map(fn (array $variant) => [
|
||||
'id' => $variant['id'],
|
||||
'price' => $variant['prices'][0]['price'] ?? null,
|
||||
'image' => $variant['media'][0]['url'] ?? null,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
$variantsData = $this->products->variantSummaries($product);
|
||||
|
||||
$firstVariant = $product['variants'][0] ?? null;
|
||||
$option = $firstVariant['options'][0]['option'] ?? null;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Catalog\ProductListing;
|
||||
use App\Catalog\ProductListingPage;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ProductListingPage $listingPage) {}
|
||||
|
||||
public function show(string $locale)
|
||||
{
|
||||
$query = trim((string) request()->query('q', ''));
|
||||
|
||||
if ($query === '') {
|
||||
return redirect()->route('products');
|
||||
}
|
||||
|
||||
$listing = ProductListing::fromRequest(request());
|
||||
|
||||
$data = $this->listingPage->build(
|
||||
$listing,
|
||||
fn (array $overrides) => route('search', ['q' => $query] + $overrides),
|
||||
collectionId: null,
|
||||
query: $query,
|
||||
);
|
||||
|
||||
return view('search.index', [...$data, 'query' => $query]);
|
||||
}
|
||||
}
|
||||
Generated
+103
-102
@@ -515,11 +515,11 @@
|
||||
},
|
||||
{
|
||||
"name": "boboko/core",
|
||||
"version": "0.10.0",
|
||||
"version": "0.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://code.radical-elements.com/boboko/core.git",
|
||||
"reference": "d680200ab1270f861e8d4e58804c215bb2dcd6ef"
|
||||
"reference": "4ff9bdacc3394bb02537998a2d43adbc2799768a"
|
||||
},
|
||||
"require": {
|
||||
"laravel/framework": "^12.0",
|
||||
@@ -556,7 +556,8 @@
|
||||
"Modules\\Core\\Providers\\CatalogServiceProvider",
|
||||
"Modules\\Core\\Providers\\CartServiceProvider",
|
||||
"Modules\\Core\\Providers\\ReviewServiceProvider",
|
||||
"Modules\\Core\\Providers\\ShippingServiceProvider"
|
||||
"Modules\\Core\\Providers\\ShippingServiceProvider",
|
||||
"Modules\\Core\\Providers\\OrderServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -566,7 +567,7 @@
|
||||
}
|
||||
},
|
||||
"description": "Core module — authentication and shared panel behaviour",
|
||||
"time": "2026-08-31T11:18:51+00:00"
|
||||
"time": "2026-09-04T10:06:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1685,16 +1686,16 @@
|
||||
},
|
||||
{
|
||||
"name": "filament/actions",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/actions.git",
|
||||
"reference": "537593a66af7fbc1a6f4d1bd8e58767a4b6847e9"
|
||||
"reference": "7bf5935b85e33b7627496e49a36bd196bf5dc228"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/actions/zipball/537593a66af7fbc1a6f4d1bd8e58767a4b6847e9",
|
||||
"reference": "537593a66af7fbc1a6f4d1bd8e58767a4b6847e9",
|
||||
"url": "https://api.github.com/repos/filamentphp/actions/zipball/7bf5935b85e33b7627496e49a36bd196bf5dc228",
|
||||
"reference": "7bf5935b85e33b7627496e49a36bd196bf5dc228",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1730,20 +1731,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:40:59+00:00"
|
||||
"time": "2026-08-31T17:01:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/filament",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/panels.git",
|
||||
"reference": "affce81542de43cf87573eda0667697c3bd79f0e"
|
||||
"reference": "5a51dfc51c31ef6e3bd063ba63923254356fb7f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/panels/zipball/affce81542de43cf87573eda0667697c3bd79f0e",
|
||||
"reference": "affce81542de43cf87573eda0667697c3bd79f0e",
|
||||
"url": "https://api.github.com/repos/filamentphp/panels/zipball/5a51dfc51c31ef6e3bd063ba63923254356fb7f6",
|
||||
"reference": "5a51dfc51c31ef6e3bd063ba63923254356fb7f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1787,20 +1788,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:40:40+00:00"
|
||||
"time": "2026-08-31T17:01:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/forms",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/forms.git",
|
||||
"reference": "29b4fce3ca9faab1af9e1d65d1a0b8feafc6ae75"
|
||||
"reference": "282eba77756ed7f28251981a8f27d3018b15006d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/forms/zipball/29b4fce3ca9faab1af9e1d65d1a0b8feafc6ae75",
|
||||
"reference": "29b4fce3ca9faab1af9e1d65d1a0b8feafc6ae75",
|
||||
"url": "https://api.github.com/repos/filamentphp/forms/zipball/282eba77756ed7f28251981a8f27d3018b15006d",
|
||||
"reference": "282eba77756ed7f28251981a8f27d3018b15006d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1837,20 +1838,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:41:13+00:00"
|
||||
"time": "2026-08-31T17:01:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/infolists",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/infolists.git",
|
||||
"reference": "87a964ec71f40a195142ed28f013b241ef88ae8c"
|
||||
"reference": "097ef96993e0ef5bb82f7660813e699c89b6cec8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/87a964ec71f40a195142ed28f013b241ef88ae8c",
|
||||
"reference": "87a964ec71f40a195142ed28f013b241ef88ae8c",
|
||||
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/097ef96993e0ef5bb82f7660813e699c89b6cec8",
|
||||
"reference": "097ef96993e0ef5bb82f7660813e699c89b6cec8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1882,20 +1883,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:40:46+00:00"
|
||||
"time": "2026-08-31T17:01:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/notifications",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/notifications.git",
|
||||
"reference": "ad33db1c66df7d88f388f3558bf216288c04f7fe"
|
||||
"reference": "dd2e23e6a3e5a68854e424812adc5c75fbfa17f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/ad33db1c66df7d88f388f3558bf216288c04f7fe",
|
||||
"reference": "ad33db1c66df7d88f388f3558bf216288c04f7fe",
|
||||
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/dd2e23e6a3e5a68854e424812adc5c75fbfa17f7",
|
||||
"reference": "dd2e23e6a3e5a68854e424812adc5c75fbfa17f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1929,20 +1930,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:40:49+00:00"
|
||||
"time": "2026-08-31T17:01:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/query-builder",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/query-builder.git",
|
||||
"reference": "024843db4765b51c0edcfae1a6ca97ad80f72ee8"
|
||||
"reference": "899a11d90a95cb2b2671f204e234e246a347ad72"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/query-builder/zipball/024843db4765b51c0edcfae1a6ca97ad80f72ee8",
|
||||
"reference": "024843db4765b51c0edcfae1a6ca97ad80f72ee8",
|
||||
"url": "https://api.github.com/repos/filamentphp/query-builder/zipball/899a11d90a95cb2b2671f204e234e246a347ad72",
|
||||
"reference": "899a11d90a95cb2b2671f204e234e246a347ad72",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1975,20 +1976,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:42:37+00:00"
|
||||
"time": "2026-08-31T17:01:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/schemas",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/schemas.git",
|
||||
"reference": "40cee8d1d5aa7be2c32597254013587214611d7d"
|
||||
"reference": "890cb1dac78fce8c63825195a8d9ca6c6bcc8029"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/schemas/zipball/40cee8d1d5aa7be2c32597254013587214611d7d",
|
||||
"reference": "40cee8d1d5aa7be2c32597254013587214611d7d",
|
||||
"url": "https://api.github.com/repos/filamentphp/schemas/zipball/890cb1dac78fce8c63825195a8d9ca6c6bcc8029",
|
||||
"reference": "890cb1dac78fce8c63825195a8d9ca6c6bcc8029",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2020,11 +2021,11 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:40:36+00:00"
|
||||
"time": "2026-08-31T17:01:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/spatie-laravel-media-library-plugin",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git",
|
||||
@@ -2061,16 +2062,16 @@
|
||||
},
|
||||
{
|
||||
"name": "filament/support",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/support.git",
|
||||
"reference": "9fc269265f97b3a8c9529e6e32c6f2fed0b3e62e"
|
||||
"reference": "37f6b22412f098c8770db1d0edeeb36cf6e47433"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/support/zipball/9fc269265f97b3a8c9529e6e32c6f2fed0b3e62e",
|
||||
"reference": "9fc269265f97b3a8c9529e6e32c6f2fed0b3e62e",
|
||||
"url": "https://api.github.com/repos/filamentphp/support/zipball/37f6b22412f098c8770db1d0edeeb36cf6e47433",
|
||||
"reference": "37f6b22412f098c8770db1d0edeeb36cf6e47433",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2115,20 +2116,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:40:43+00:00"
|
||||
"time": "2026-09-01T08:55:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/tables",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/tables.git",
|
||||
"reference": "78c78deed4005b05f80b5d6b623ff9830704644f"
|
||||
"reference": "812745d91c3836cbb8046d9a619386ab7ac07ce7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/tables/zipball/78c78deed4005b05f80b5d6b623ff9830704644f",
|
||||
"reference": "78c78deed4005b05f80b5d6b623ff9830704644f",
|
||||
"url": "https://api.github.com/repos/filamentphp/tables/zipball/812745d91c3836cbb8046d9a619386ab7ac07ce7",
|
||||
"reference": "812745d91c3836cbb8046d9a619386ab7ac07ce7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2161,20 +2162,20 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:41:42+00:00"
|
||||
"time": "2026-08-31T17:01:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "filament/widgets",
|
||||
"version": "v4.12.6",
|
||||
"version": "v4.12.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filamentphp/widgets.git",
|
||||
"reference": "70b472cee64d5eebadfd51bb81b72ee00a89fbfd"
|
||||
"reference": "9f3b5ad3aea0f52908ee88775a890f0a50e8d903"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filamentphp/widgets/zipball/70b472cee64d5eebadfd51bb81b72ee00a89fbfd",
|
||||
"reference": "70b472cee64d5eebadfd51bb81b72ee00a89fbfd",
|
||||
"url": "https://api.github.com/repos/filamentphp/widgets/zipball/9f3b5ad3aea0f52908ee88775a890f0a50e8d903",
|
||||
"reference": "9f3b5ad3aea0f52908ee88775a890f0a50e8d903",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2205,7 +2206,7 @@
|
||||
"issues": "https://github.com/filamentphp/filament/issues",
|
||||
"source": "https://github.com/filamentphp/filament"
|
||||
},
|
||||
"time": "2026-08-05T20:41:58+00:00"
|
||||
"time": "2026-08-31T17:01:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
@@ -2992,16 +2993,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v12.68.0",
|
||||
"version": "v12.69.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "1343c22b92edd48e29ff273d8ce7c15cf75d974c"
|
||||
"reference": "0c07b0b1f88af44d8558ffadf66900a860f93c23"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/1343c22b92edd48e29ff273d8ce7c15cf75d974c",
|
||||
"reference": "1343c22b92edd48e29ff273d8ce7c15cf75d974c",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/0c07b0b1f88af44d8558ffadf66900a860f93c23",
|
||||
"reference": "0c07b0b1f88af44d8558ffadf66900a860f93c23",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3210,7 +3211,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2026-08-25T14:18:36+00:00"
|
||||
"time": "2026-09-01T21:34:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
@@ -3763,16 +3764,16 @@
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "3.35.3",
|
||||
"version": "3.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "5fc8404762179ae514678487b23494fd69b2309c"
|
||||
"reference": "f7fb152932f30072d573510cbd4dd657d6475b25"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5fc8404762179ae514678487b23494fd69b2309c",
|
||||
"reference": "5fc8404762179ae514678487b23494fd69b2309c",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25",
|
||||
"reference": "f7fb152932f30072d573510cbd4dd657d6475b25",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3840,9 +3841,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/flysystem/issues",
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/3.35.3"
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/3.36.0"
|
||||
},
|
||||
"time": "2026-08-22T12:55:54+00:00"
|
||||
"time": "2026-09-02T08:00:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-local",
|
||||
@@ -4293,16 +4294,16 @@
|
||||
},
|
||||
{
|
||||
"name": "livewire/livewire",
|
||||
"version": "v3.8.6",
|
||||
"version": "v3.8.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/livewire.git",
|
||||
"reference": "be5729e9accfa9255a28d82f7759a3cc4be0f079"
|
||||
"reference": "ff019f8f6f48b7a2315922e45a70ad8fd75d1934"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/be5729e9accfa9255a28d82f7759a3cc4be0f079",
|
||||
"reference": "be5729e9accfa9255a28d82f7759a3cc4be0f079",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/ff019f8f6f48b7a2315922e45a70ad8fd75d1934",
|
||||
"reference": "ff019f8f6f48b7a2315922e45a70ad8fd75d1934",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4357,7 +4358,7 @@
|
||||
"description": "A front-end framework for Laravel.",
|
||||
"support": {
|
||||
"issues": "https://github.com/livewire/livewire/issues",
|
||||
"source": "https://github.com/livewire/livewire/tree/v3.8.6"
|
||||
"source": "https://github.com/livewire/livewire/tree/v3.8.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -4365,7 +4366,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-24T15:02:02+00:00"
|
||||
"time": "2026-08-31T15:40:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lukascivil/treewalker",
|
||||
@@ -5099,16 +5100,16 @@
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.10.0",
|
||||
"version": "3.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Seldaek/monolog.git",
|
||||
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
|
||||
"reference": "147f303310f06334f03f409e49d7ad1e275ff05a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
||||
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/147f303310f06334f03f409e49d7ad1e275ff05a",
|
||||
"reference": "147f303310f06334f03f409e49d7ad1e275ff05a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5186,7 +5187,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Seldaek/monolog/issues",
|
||||
"source": "https://github.com/Seldaek/monolog/tree/3.10.0"
|
||||
"source": "https://github.com/Seldaek/monolog/tree/3.11.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5198,7 +5199,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-02T08:56:05+00:00"
|
||||
"time": "2026-09-02T12:39:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
@@ -7105,16 +7106,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.4",
|
||||
"version": "2.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "98dbc9412932af5825e6d5aa5d6bc4de7d82538a"
|
||||
"reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/98dbc9412932af5825e6d5aa5d6bc4de7d82538a",
|
||||
"reference": "98dbc9412932af5825e6d5aa5d6bc4de7d82538a",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9",
|
||||
"reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7146,9 +7147,9 @@
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.4"
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5"
|
||||
},
|
||||
"time": "2026-08-30T16:25:38+00:00"
|
||||
"time": "2026-08-31T16:05:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pragmarx/google2fa",
|
||||
@@ -8791,16 +8792,16 @@
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-medialibrary",
|
||||
"version": "11.23.5",
|
||||
"version": "11.23.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-medialibrary.git",
|
||||
"reference": "8ca16954d607de1853c9609e88eb91eab43d67b9"
|
||||
"reference": "94b11766ea5e10b1e2a88159b24937eae89a5d5f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/8ca16954d607de1853c9609e88eb91eab43d67b9",
|
||||
"reference": "8ca16954d607de1853c9609e88eb91eab43d67b9",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/94b11766ea5e10b1e2a88159b24937eae89a5d5f",
|
||||
"reference": "94b11766ea5e10b1e2a88159b24937eae89a5d5f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8885,7 +8886,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-medialibrary/issues",
|
||||
"source": "https://github.com/spatie/laravel-medialibrary/tree/11.23.5"
|
||||
"source": "https://github.com/spatie/laravel-medialibrary/tree/11.23.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8897,7 +8898,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-10T08:15:27+00:00"
|
||||
"time": "2026-09-03T13:52:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-package-tools",
|
||||
@@ -12654,16 +12655,16 @@
|
||||
},
|
||||
{
|
||||
"name": "technikermathe/blade-lucide-icons",
|
||||
"version": "v3.171.0",
|
||||
"version": "v3.174.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PascaleBeier/blade-lucide-icons.git",
|
||||
"reference": "6419d6426865c9e69195405d52bdd8a859356353"
|
||||
"reference": "e22d65587b2a6f21405161f399bda8c871135724"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/6419d6426865c9e69195405d52bdd8a859356353",
|
||||
"reference": "6419d6426865c9e69195405d52bdd8a859356353",
|
||||
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/e22d65587b2a6f21405161f399bda8c871135724",
|
||||
"reference": "e22d65587b2a6f21405161f399bda8c871135724",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12713,9 +12714,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PascaleBeier/blade-lucide-icons/issues",
|
||||
"source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.171.0"
|
||||
"source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.174.0"
|
||||
},
|
||||
"time": "2026-08-30T02:22:57+00:00"
|
||||
"time": "2026-09-04T01:58:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "thecodingmachine/safe",
|
||||
@@ -12986,16 +12987,16 @@
|
||||
},
|
||||
{
|
||||
"name": "ueberdosis/tiptap-php",
|
||||
"version": "2.1.2",
|
||||
"version": "2.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap-php.git",
|
||||
"reference": "0561e2146edbcdc622b5d4008d0ee02582f8642b"
|
||||
"reference": "5a2e8155c5b09c9ad4efd480550270a6924865b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ueberdosis/tiptap-php/zipball/0561e2146edbcdc622b5d4008d0ee02582f8642b",
|
||||
"reference": "0561e2146edbcdc622b5d4008d0ee02582f8642b",
|
||||
"url": "https://api.github.com/repos/ueberdosis/tiptap-php/zipball/5a2e8155c5b09c9ad4efd480550270a6924865b9",
|
||||
"reference": "5a2e8155c5b09c9ad4efd480550270a6924865b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -13035,7 +13036,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ueberdosis/tiptap-php/issues",
|
||||
"source": "https://github.com/ueberdosis/tiptap-php/tree/2.1.2"
|
||||
"source": "https://github.com/ueberdosis/tiptap-php/tree/2.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -13051,7 +13052,7 @@
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-11T08:47:45+00:00"
|
||||
"time": "2026-08-31T07:00:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
|
||||
@@ -114,7 +114,7 @@ services:
|
||||
- "${VALKEY_PORT:-6339}:6379"
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.10
|
||||
image: getmeili/meilisearch:v1.12
|
||||
ports:
|
||||
- "${MEILISEARCH_PORT:-7700}:7700"
|
||||
environment:
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ services:
|
||||
- valkeydata:/data
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.10
|
||||
image: getmeili/meilisearch:v1.12
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MEILI_MASTER_KEY: ${MEILISEARCH_KEY:?MEILISEARCH_KEY is required}
|
||||
|
||||
@@ -47,6 +47,12 @@ if [ "$APP_ENV" != "production" ]; then
|
||||
php artisan vendor:publish --tag=core-assets --force --ansi --quiet
|
||||
php artisan vendor:publish --tag=public --force --ansi --quiet
|
||||
php artisan filament:assets --ansi --quiet
|
||||
|
||||
# No --force: core-views publishes editable Blade templates (order
|
||||
# notification emails), meant to be hand-customized per app — unlike
|
||||
# core-assets above, republishing must not silently wipe local edits.
|
||||
# Only fills in the vendor/core views directory if it doesn't exist yet.
|
||||
php artisan vendor:publish --tag=core-views --ansi --quiet
|
||||
fi
|
||||
|
||||
# Everything below is universal, in both dev and production: application STATE
|
||||
@@ -80,6 +86,7 @@ php artisan lunar:install --quiet || true
|
||||
# just shipped, same reasoning as optimize:clear above.
|
||||
echo "[entrypoint] Syncing search indexes..."
|
||||
php artisan lunar:meilisearch:setup
|
||||
php artisan lunar:meilisearch:tune-product-search --quiet || true
|
||||
php artisan lunar:search:index --quiet || true
|
||||
|
||||
if [ "$APP_ENV" = "production" ]; then
|
||||
|
||||
@@ -213,6 +213,28 @@ @layer components {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Search overlay (popover) — opacity fade over the header ──── */
|
||||
#search-overlay {
|
||||
/* Fallback height until the `nav-search` controller measures the real
|
||||
header on open; the header has no fixed height below `lg`. */
|
||||
--search-overlay-h: 6.5rem;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
display 0.2s allow-discrete,
|
||||
overlay 0.2s allow-discrete;
|
||||
}
|
||||
|
||||
#search-overlay:popover-open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
#search-overlay:popover-open {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Shared underline-slide animation ────────────────────────── */
|
||||
.underline-slide {
|
||||
background-image: linear-gradient(currentColor, currentColor);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
// Turbo doesn't scroll for <turbo-frame> navigations, so after the frame swaps
|
||||
// its contents (a sort, filter, or pagination link) this brings the top of the
|
||||
// frame back into view — otherwise clicking pagination at the bottom of the
|
||||
// list leaves you stranded down there. The frame's own scroll-margin-top keeps
|
||||
// it clear of the sticky header.
|
||||
//
|
||||
// `turbo:frame-render` fires only on a content swap, not on the initial page
|
||||
// render, and the frame element itself persists across swaps — so the listener
|
||||
// is bound once in connect().
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.addEventListener('turbo:frame-render', this.#toTop)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.element.removeEventListener('turbo:frame-render', this.#toTop)
|
||||
}
|
||||
|
||||
#toTop = () => {
|
||||
this.element.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import AutoSubmitController from './auto-submit-controller'
|
||||
import BackToTopController from './back-to-top-controller'
|
||||
import CarouselController from './carousel-controller'
|
||||
import DropdownController from './dropdown-controller'
|
||||
import FrameScrollController from './frame-scroll-controller'
|
||||
import NavSearchController from './nav-search-controller'
|
||||
import ProductFormController from './product-form-controller'
|
||||
import ProductGalleryController from './product-gallery-controller'
|
||||
import QuantityController from './quantity-controller'
|
||||
@@ -21,6 +23,8 @@ export function registerControllers(application) {
|
||||
application.register('back-to-top', BackToTopController)
|
||||
application.register('carousel', CarouselController)
|
||||
application.register('dropdown', DropdownController)
|
||||
application.register('frame-scroll', FrameScrollController)
|
||||
application.register('nav-search', NavSearchController)
|
||||
application.register('product-form', ProductFormController)
|
||||
application.register('product-gallery', ProductGalleryController)
|
||||
application.register('quantity', QuantityController)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
// The search overlay is a [popover] that must sit exactly over the header. The
|
||||
// header has no fixed height below `lg`, so on open we copy its current height
|
||||
// onto --search-overlay-h and move focus into the field. Escape / click-away
|
||||
// close come from the Popover API; the fade is CSS (#search-overlay).
|
||||
export default class extends Controller {
|
||||
static targets = ['input']
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener('toggle', this.#onToggle)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.element.removeEventListener('toggle', this.#onToggle)
|
||||
}
|
||||
|
||||
#onToggle = (event) => {
|
||||
if (event.newState !== 'open') return
|
||||
|
||||
const header = this.element.closest('header')
|
||||
if (header) {
|
||||
this.element.style.setProperty('--search-overlay-h', `${header.offsetHeight}px`)
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => this.inputTarget.focus())
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
{{--
|
||||
The reloadable body of the category listing — count + sort, product grid +
|
||||
pagination, and the filter sidebar. Rendered both on full page load and on
|
||||
every <turbo-frame id="category-listing"> navigation, always straight from
|
||||
the query string ($listing). Anything that must reflect the applied
|
||||
sort/filter/page state belongs in here.
|
||||
|
||||
Vars: $collection, $products (LengthAwarePaginator), $listing (App\Catalog\CategoryListing)
|
||||
--}}
|
||||
|
||||
@php
|
||||
// Keys are the `sort` URL param values — the ProductSort enum cases, plus
|
||||
// "popularity" for the default (no param). App\Catalog\CategoryListing is
|
||||
// the single place that validates them back into the enum.
|
||||
$sortLabels = [
|
||||
'popularity' => __('storefront.shop.sort_popularity'),
|
||||
'price_asc' => __('storefront.shop.sort_price_asc'),
|
||||
'price_desc' => __('storefront.shop.sort_price_desc'),
|
||||
'newest' => __('storefront.shop.sort_newest'),
|
||||
];
|
||||
$currentSort = $listing->sort?->value ?? 'popularity';
|
||||
@endphp
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
|
||||
|
||||
<div class="flex items-center justify-between gap-6 flex-wrap mb-7">
|
||||
<p>
|
||||
{{ trans_choice('storefront.shop.showing_results', $products->total(), [
|
||||
'first' => $products->firstItem() ?? 0,
|
||||
'last' => $products->lastItem() ?? 0,
|
||||
'total' => $products->total(),
|
||||
]) }}
|
||||
</p>
|
||||
|
||||
{{-- Sort options are plain links carrying the rest of the applied state;
|
||||
following one navigates the frame and advances the URL. Changing
|
||||
sort drops back to page 1. --}}
|
||||
<x-ui.dropdown
|
||||
id="category-sort"
|
||||
:label="$sortLabels[$currentSort]"
|
||||
:ariaLabel="__('storefront.shop.sort_label')"
|
||||
triggerClass="min-w-48"
|
||||
>
|
||||
@foreach ($sortLabels as $value => $label)
|
||||
<x-ui.dropdown.item
|
||||
:href="route('category.show', ['id' => $collection['id']] + $listing->query([
|
||||
'sort' => $value === 'popularity' ? null : $value,
|
||||
'page' => null,
|
||||
]))"
|
||||
:current="$value === $currentSort"
|
||||
>{{ $label }}</x-ui.dropdown.item>
|
||||
@endforeach
|
||||
</x-ui.dropdown>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
|
||||
|
||||
{{-- Products --}}
|
||||
<div>
|
||||
@if($products->isEmpty())
|
||||
<p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p>
|
||||
@else
|
||||
<x-product-grid :products="$products->items()" cols="3" />
|
||||
|
||||
<div class="mt-12">
|
||||
<x-ui.pagination :paginator="$products" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Sidebar — sort, price and in-stock are wired to the back-end; the
|
||||
search box is still a placeholder. --}}
|
||||
<aside class="flex flex-col gap-10">
|
||||
|
||||
{{-- Placeholder — not wired yet. --}}
|
||||
<div class="-mt-2">
|
||||
<label for="shop-search" class="sr-only">{{ __('storefront.shop.search_label') }}</label>
|
||||
<div class="relative">
|
||||
<x-ui.input
|
||||
type="search"
|
||||
id="shop-search"
|
||||
:placeholder="__('storefront.shop.search_placeholder')"
|
||||
class="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
aria-label="{{ __('storefront.shop.search_label') }}"
|
||||
>
|
||||
<x-ui.icon name="search" :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filter form: a plain GET form whose fields ARE the state. Changing
|
||||
any control auto-submits (auto-submit controller); Turbo captures
|
||||
the GET, navigates the frame, and advances the URL. `sort` rides
|
||||
along as a hidden field so it survives a filter change; `page` is
|
||||
deliberately absent, so filtering drops back to page 1. Without JS
|
||||
the sr-only submit button applies the range inputs. --}}
|
||||
<form
|
||||
method="get"
|
||||
action="{{ route('category.show', ['id' => $collection['id']]) }}"
|
||||
data-controller="auto-submit"
|
||||
data-action="change->auto-submit#submit range-slider:change->auto-submit#submit"
|
||||
class="contents"
|
||||
>
|
||||
@if ($listing->sort)
|
||||
<input type="hidden" name="sort" value="{{ $listing->sort->value }}">
|
||||
@endif
|
||||
|
||||
@if ($priceFloor !== null && $priceCeil !== null && $priceCeil > $priceFloor)
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="font-extrabold text-h4">{{ __('storefront.shop.filter_price') }}</p>
|
||||
|
||||
<x-ui.range-slider
|
||||
name="price"
|
||||
:min="$priceFloor"
|
||||
:max="$priceCeil"
|
||||
:min-value="max($priceFloor, $listing->minPrice ?? $priceFloor)"
|
||||
:max-value="min($priceCeil, $listing->maxPrice ?? $priceCeil)"
|
||||
prefix="€"
|
||||
separator=" - "
|
||||
:legend="__('storefront.shop.filter_price')"
|
||||
:min-label="__('storefront.shop.price_min')"
|
||||
:max-label="__('storefront.shop.price_max')"
|
||||
>
|
||||
{{-- Clear the price filter — a plain link back to the URL
|
||||
without price_*; only shown when actually filtered. --}}
|
||||
@if ($priceFiltered)
|
||||
<a
|
||||
href="{{ route('category.show', ['id' => $collection['id']] + $listing->query(['price_min' => null, 'price_max' => null, 'page' => null])) }}"
|
||||
class="underline-slide [--slide-h:1px] font-display text-sm font-bold uppercase italic"
|
||||
>{{ __('storefront.shop.reset') }}</a>
|
||||
@endif
|
||||
</x-ui.range-slider>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-4 [&_label]:text-base">
|
||||
<p class="font-extrabold text-h4">{{ __('storefront.shop.availability') }}</p>
|
||||
<x-ui.checkbox id="shop-in-stock" name="in_stock" :checked="$listing->inStockOnly">
|
||||
{{ __('storefront.shop.in_stock_only') }}
|
||||
</x-ui.checkbox>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="sr-only">{{ __('storefront.shop.apply') }}</button>
|
||||
</form>
|
||||
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
@@ -32,8 +32,13 @@
|
||||
refresh or bookmark reproduces the exact same view. With no JS the
|
||||
frame is just a block and the links do full-page navigations to the
|
||||
same URLs. --}}
|
||||
<turbo-frame id="category-listing" data-turbo-action="advance">
|
||||
@include('category.partials.listing')
|
||||
<turbo-frame
|
||||
id="category-listing"
|
||||
data-turbo-action="advance"
|
||||
data-controller="frame-scroll"
|
||||
class="scroll-mt-28"
|
||||
>
|
||||
@include('shop.partials.listing')
|
||||
</turbo-frame>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
{{-- Products (CSS-only hover dropdown) --}}
|
||||
<div class="group relative h-full flex items-center">
|
||||
<a href="{{ url('/'.app()->getLocale().'/products') }}" class="nav-link uppercase inline-flex items-center gap-1 relative font-display font-extrabold italic text-black no-underline py-8">
|
||||
<a href="{{ route('products') }}" class="nav-link uppercase inline-flex items-center gap-1 relative font-display font-extrabold italic text-black no-underline py-8">
|
||||
<span>{{ __('storefront.nav.products') }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4 not-italic transition-transform group-hover:rotate-180">
|
||||
<path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
|
||||
@@ -47,10 +47,18 @@
|
||||
</a>
|
||||
|
||||
{{-- Search --}}
|
||||
<button type="button" class="flex items-center justify-center w-10 h-10 hover:opacity-70 transition-opacity" aria-label="Search">
|
||||
<button
|
||||
type="button"
|
||||
popovertarget="search-overlay"
|
||||
popovertargetaction="toggle"
|
||||
class="flex items-center justify-center w-10 h-10 hover:opacity-70 transition-opacity"
|
||||
aria-label="{{ __('storefront.shop.search_label') }}"
|
||||
>
|
||||
<x-ui.icon name="search" :size="40" />
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-search-overlay />
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{{--
|
||||
Full-width search bar that covers the header. A [popover] (top layer, so it
|
||||
sits over the sticky header with no z-index juggling; Escape and click-away
|
||||
close come for free). The header's search button opens it via popovertarget.
|
||||
The `nav-search` controller measures the header on open (→ --search-overlay-h,
|
||||
since the header isn't a fixed height below `lg`) and focuses the field.
|
||||
Fade transition lives in app.css (#search-overlay).
|
||||
--}}
|
||||
<div
|
||||
id="search-overlay"
|
||||
popover
|
||||
data-controller="nav-search"
|
||||
class="fixed w-full inset-x-0 top-0 bottom-auto m-0 h-[var(--search-overlay-h)] border-0 bg-brand p-0"
|
||||
>
|
||||
<form
|
||||
method="get"
|
||||
action="{{ route('search') }}"
|
||||
class="flex h-full items-center gap-6 px-10"
|
||||
>
|
||||
<label for="search-overlay-input" class="sr-only">{{ __('storefront.shop.search_label') }}</label>
|
||||
<input
|
||||
id="search-overlay-input"
|
||||
type="search"
|
||||
name="q"
|
||||
required
|
||||
autocomplete="off"
|
||||
data-nav-search-target="input"
|
||||
placeholder="{{ __('storefront.search.placeholder') }}"
|
||||
class="min-w-0 flex-1 appearance-none border-0 border-b border-black bg-transparent pb-2 placeholder:text-black/60 focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
popovertarget="search-overlay"
|
||||
popovertargetaction="hide"
|
||||
aria-label="{{ __('storefront.nav.close') }}"
|
||||
class="shrink-0 text-black transition-opacity hover:opacity-70"
|
||||
>
|
||||
<x-ui.icon name="close" :size="44" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
@props(['paginator'])
|
||||
|
||||
{{-- "Showing 1–12 of 48 products" — takes any LengthAwarePaginator. Shared by
|
||||
the category listing and the search results page. --}}
|
||||
<p {{ $attributes }}>
|
||||
{{ trans_choice('storefront.shop.showing_results', $paginator->total(), [
|
||||
'first' => $paginator->firstItem() ?? 0,
|
||||
'last' => $paginator->lastItem() ?? 0,
|
||||
'total' => $paginator->total(),
|
||||
]) }}
|
||||
</p>
|
||||
@@ -0,0 +1,24 @@
|
||||
@props([
|
||||
'options', // [['label' => string, 'href' => string, 'current' => bool], ...] — build with App\Catalog\ProductSortOptions
|
||||
'id' => 'sort',
|
||||
])
|
||||
|
||||
{{-- Product sort dropdown. Options are plain links (built per page, so each can
|
||||
carry its own state); following one navigates the enclosing turbo-frame and
|
||||
advances the URL. Shared by the category listing and search results. --}}
|
||||
@php
|
||||
$active = collect($options)->firstWhere('current', true) ?? ($options[0] ?? ['label' => '']);
|
||||
@endphp
|
||||
|
||||
<x-ui.dropdown
|
||||
:id="$id"
|
||||
:label="$active['label']"
|
||||
:ariaLabel="__('storefront.shop.sort_label')"
|
||||
triggerClass="min-w-48"
|
||||
>
|
||||
@foreach ($options as $option)
|
||||
<x-ui.dropdown.item :href="$option['href']" :current="$option['current'] ?? false">
|
||||
{{ $option['label'] }}
|
||||
</x-ui.dropdown.item>
|
||||
@endforeach
|
||||
</x-ui.dropdown>
|
||||
@@ -5,9 +5,14 @@
|
||||
'href' => '#',
|
||||
])
|
||||
|
||||
{{-- data-turbo-frame="_top" on the links: this card renders inside the
|
||||
category page's <turbo-frame id="category-listing">, so without it a click
|
||||
would try to load the product page *into* that frame, not find a matching
|
||||
frame, and fail with "content missing". It's a no-op anywhere there's no
|
||||
frame (homepage grids, related products). --}}
|
||||
<div class="group">
|
||||
<div class="relative flex items-center justify-center mb-4">
|
||||
<a href="{{ $href }}" class="block w-full border border-black overflow-hidden bg-white aspect-[251/335] flex items-center justify-center">
|
||||
<a href="{{ $href }}" data-turbo-frame="_top" class="block w-full border border-black overflow-hidden bg-white aspect-[251/335] flex items-center justify-center">
|
||||
@if($image)
|
||||
<img
|
||||
src="{{ $image }}"
|
||||
@@ -28,7 +33,7 @@ class="w-full h-auto block"
|
||||
</div>
|
||||
|
||||
<div class="flex items-baseline justify-between gap-4">
|
||||
<a href="{{ $href }}" class="font-bold text-xl leading-snug hover:text-brand">{{ $name }}</a>
|
||||
<a href="{{ $href }}" data-turbo-frame="_top" class="font-bold text-xl leading-snug hover:text-brand">{{ $name }}</a>
|
||||
@if($price !== null)
|
||||
<span class="font-bold text-xl shrink-0"><x-ui.price :amount="$price" /></span>
|
||||
@endif
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
data-controller="appear"
|
||||
data-appear-visible-class="is-visible">
|
||||
<span class="text-dance">Επικοινώνησε</span><br>
|
||||
μαζί μας <span class="text-dance">άμεσα</span>
|
||||
μαζί μας <span class="text-dance [--dance-delay:200ms]">άμεσα</span>
|
||||
</h1>
|
||||
|
||||
<p class="max-w-md ">
|
||||
|
||||
@@ -138,7 +138,7 @@ class="max-w-xs"
|
||||
<div class="border-b border-black" data-controller="carousel">
|
||||
<div class="flex items-center justify-between gap-4 px-8 py-12 border-b border-black">
|
||||
<h2 class="font-display font-extrabold text-h2" data-stoic="pages/home#classics_title">{{ $page->classics_title }}</h2>
|
||||
<x-ui.button size="lg" :href="url('/'.app()->getLocale().'/products')">Όλα τα προϊόντα</x-ui.button>
|
||||
<x-ui.button size="lg" :href="route('products')">Όλα τα προϊόντα</x-ui.button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 px-8 py-12">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<x-breadcrumb class="mb-14 justify-end" :items="[
|
||||
$collection
|
||||
? ['label' => $collection['name'], 'href' => route('category.show', ['id' => $collection['id']])]
|
||||
: ['label' => __('storefront.nav.products'), 'href' => '/products'],
|
||||
: ['label' => __('storefront.nav.products'), 'href' => route('products')],
|
||||
['label' => $product['name']],
|
||||
]" />
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', __('storefront.shop.all_products') . ' — ' . config('app.name'))
|
||||
|
||||
@push('seo')
|
||||
{{-- The bare listing is indexable; filtered / sorted / paged variants
|
||||
consolidate onto it and are kept out of the index. --}}
|
||||
<link rel="canonical" href="{{ route('products') }}">
|
||||
@if($listing->isRefined())
|
||||
<meta name="robots" content="noindex,follow">
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-5xl mx-auto pt-26 pb-12">
|
||||
|
||||
<div class="flex items-end justify-between gap-6 flex-wrap mb-16">
|
||||
<h1 class="font-medium text-h2">{{ __('storefront.shop.all_products') }}</h1>
|
||||
|
||||
<x-breadcrumb :items="[
|
||||
['label' => __('storefront.nav.home'), 'href' => route('home')],
|
||||
['label' => __('storefront.shop.all_products')],
|
||||
]" />
|
||||
</div>
|
||||
|
||||
{{-- Same reloadable listing body as the category page; sort/filter/page
|
||||
navigate this frame and advance the URL. --}}
|
||||
<turbo-frame
|
||||
id="products-listing"
|
||||
data-turbo-action="advance"
|
||||
data-controller="frame-scroll"
|
||||
class="scroll-mt-28"
|
||||
>
|
||||
@include('shop.partials.listing')
|
||||
</turbo-frame>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,100 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Αναζήτηση')
|
||||
|
||||
@section('content')
|
||||
|
||||
<section class="px-8 py-16 lg:px-16">
|
||||
|
||||
<form method="GET" action="{{ route('search') }}" class="max-w-xl mb-12">
|
||||
<x-ui.field label="Αναζήτηση" for="search-q">
|
||||
<x-ui.input id="search-q" name="q" value="{{ $query }}" autocomplete="off" />
|
||||
</x-ui.field>
|
||||
</form>
|
||||
|
||||
@if ($query === '')
|
||||
<p class="text-neutral-500">Πληκτρολόγησε κάτι για αναζήτηση.</p>
|
||||
@else
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
|
||||
|
||||
<div>
|
||||
@if ($results->isEmpty())
|
||||
<p class="text-neutral-500">Δεν βρέθηκαν αποτελέσματα για "{{ $query }}".</p>
|
||||
@else
|
||||
<p class="mb-6 text-neutral-500">{{ $results->count() }} αποτελέσματα για "{{ $query }}"</p>
|
||||
|
||||
<ul>
|
||||
@foreach ($results as $product)
|
||||
<li>
|
||||
#{{ $product->id }} —
|
||||
<a href="{{ route('product.show', ['id' => $product->id]) }}">
|
||||
{{ $product->translateAttribute('name') ?? '(no name — id: ' . $product->id . ')' }}
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Filter sidebar — same shape/components as category/partials/listing.blade.php,
|
||||
adapted to SearchListing's query() (which always carries `q`) instead of
|
||||
CategoryListing's collection-scoped one. --}}
|
||||
<aside class="flex flex-col gap-10">
|
||||
|
||||
<form
|
||||
method="get"
|
||||
action="{{ route('search') }}"
|
||||
data-controller="auto-submit"
|
||||
data-action="change->auto-submit#submit range-slider:change->auto-submit#submit"
|
||||
class="contents"
|
||||
>
|
||||
<input type="hidden" name="q" value="{{ $listing->query }}">
|
||||
|
||||
@if ($listing->sort)
|
||||
<input type="hidden" name="sort" value="{{ $listing->sort->value }}">
|
||||
@endif
|
||||
|
||||
@if ($priceFloor !== null && $priceCeil !== null && $priceCeil > $priceFloor)
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="font-extrabold text-h4">{{ __('storefront.shop.filter_price') }}</p>
|
||||
|
||||
<x-ui.range-slider
|
||||
name="price"
|
||||
:min="$priceFloor"
|
||||
:max="$priceCeil"
|
||||
:min-value="max($priceFloor, $listing->minPrice ?? $priceFloor)"
|
||||
:max-value="min($priceCeil, $listing->maxPrice ?? $priceCeil)"
|
||||
prefix="€"
|
||||
separator=" - "
|
||||
:legend="__('storefront.shop.filter_price')"
|
||||
:min-label="__('storefront.shop.price_min')"
|
||||
:max-label="__('storefront.shop.price_max')"
|
||||
>
|
||||
@if ($priceFiltered)
|
||||
<a
|
||||
href="{{ route('search', $listing->query(['price_min' => null, 'price_max' => null, 'page' => null])) }}"
|
||||
class="underline-slide [--slide-h:1px] font-display text-sm font-bold uppercase italic"
|
||||
>{{ __('storefront.shop.reset') }}</a>
|
||||
@endif
|
||||
</x-ui.range-slider>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-4 [&_label]:text-base">
|
||||
<p class="font-extrabold text-h4">{{ __('storefront.shop.availability') }}</p>
|
||||
<x-ui.checkbox id="search-in-stock" name="in_stock" :checked="$listing->inStockOnly">
|
||||
{{ __('storefront.shop.in_stock_only') }}
|
||||
</x-ui.checkbox>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="sr-only">{{ __('storefront.shop.apply') }}</button>
|
||||
</form>
|
||||
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</section>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,36 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@php
|
||||
// $heading = __('storefront.search.results_for') . ' "' . $query . '"';
|
||||
$heading = '"' . $query . '"';
|
||||
@endphp
|
||||
|
||||
@section('title', $query . ' — ' . config('app.name'))
|
||||
|
||||
@push('seo')
|
||||
<meta name="robots" content="noindex,follow">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-5xl mx-auto pt-26 pb-12">
|
||||
|
||||
<div class="flex items-end justify-between gap-6 flex-wrap mb-16">
|
||||
<h1 class="font-medium text-h2">{{ $heading }}</h1>
|
||||
|
||||
<x-breadcrumb :items="[
|
||||
['label' => __('storefront.nav.home'), 'href' => route('home')],
|
||||
['label' => __('storefront.search.results_for') . $heading],
|
||||
]" />
|
||||
</div>
|
||||
|
||||
<turbo-frame
|
||||
id="search-listing"
|
||||
data-turbo-action="advance"
|
||||
data-controller="frame-scroll"
|
||||
class="scroll-mt-28"
|
||||
>
|
||||
@include('search.partials.results')
|
||||
</turbo-frame>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,22 @@
|
||||
{{--
|
||||
Reloadable body of the search results page — result count + sort, product
|
||||
grid, pagination. Re-rendered on full load and on every
|
||||
<turbo-frame id="search-listing"> navigation, straight from ?q= and ?sort=.
|
||||
|
||||
Vars: $query (non-empty string), $products (LengthAwarePaginator), $sortOptions (array)
|
||||
--}}
|
||||
|
||||
@if ($products->isEmpty())
|
||||
<p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p>
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-6 flex-wrap mb-7">
|
||||
<x-shop.result-count :paginator="$products" />
|
||||
<x-shop.sort :options="$sortOptions" id="search-sort" />
|
||||
</div>
|
||||
|
||||
<x-product-grid :products="$products->items()" cols="4" />
|
||||
|
||||
<div class="mt-12">
|
||||
<x-ui.pagination :paginator="$products" />
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,96 @@
|
||||
{{--
|
||||
Reloadable body of a product listing — result count + sort, product grid +
|
||||
pagination, and the filter sidebar. Shared by the category page (scoped to a
|
||||
collection) and the all-products page. Rendered on full load and on every
|
||||
turbo-frame navigation, straight from the query string.
|
||||
|
||||
Vars (all from App\Catalog\ProductListingPage::build):
|
||||
$products (LengthAwarePaginator), $sortOptions (array),
|
||||
$listing (App\Catalog\ProductListing), $listingAction (string, GET form target),
|
||||
$clearPriceUrl (?string), $priceFloor / $priceCeil (?int)
|
||||
--}}
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
|
||||
|
||||
<div class="flex items-center justify-between gap-6 flex-wrap mb-7">
|
||||
<x-shop.result-count :paginator="$products" />
|
||||
<x-shop.sort :options="$sortOptions" id="shop-sort" />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
|
||||
|
||||
{{-- Products --}}
|
||||
<div>
|
||||
@if($products->isEmpty())
|
||||
<p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p>
|
||||
@else
|
||||
<x-product-grid :products="$products->items()" cols="3" />
|
||||
|
||||
<div class="mt-12">
|
||||
<x-ui.pagination :paginator="$products" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Sidebar — sort, price and in-stock are wired to the back-end; product
|
||||
search lives in the nav overlay, not here. --}}
|
||||
<aside class="flex flex-col gap-10">
|
||||
|
||||
{{-- Filter form: a plain GET form whose fields ARE the state. Changing
|
||||
any control auto-submits (auto-submit controller); Turbo captures
|
||||
the GET, navigates the frame, and advances the URL. `sort` rides
|
||||
along as a hidden field so it survives a filter change; `page` is
|
||||
deliberately absent, so filtering drops back to page 1. Without JS
|
||||
the sr-only submit button applies the range inputs. --}}
|
||||
<form
|
||||
method="get"
|
||||
action="{{ $listingAction }}"
|
||||
data-controller="auto-submit"
|
||||
data-action="change->auto-submit#submit range-slider:change->auto-submit#submit"
|
||||
class="contents"
|
||||
>
|
||||
@if ($listing->sort)
|
||||
<input type="hidden" name="sort" value="{{ $listing->sort->value }}" />
|
||||
@endif
|
||||
|
||||
@if ($priceFloor !== null && $priceCeil !== null && $priceCeil > $priceFloor)
|
||||
<div class="flex flex-col gap-4 -mt-2">
|
||||
<p class="font-extrabold text-h4">{{ __('storefront.shop.filter_price') }}</p>
|
||||
|
||||
<x-ui.range-slider
|
||||
name="price"
|
||||
:min="$priceFloor"
|
||||
:max="$priceCeil"
|
||||
:min-value="max($priceFloor, $listing->minPrice ?? $priceFloor)"
|
||||
:max-value="min($priceCeil, $listing->maxPrice ?? $priceCeil)"
|
||||
prefix="€"
|
||||
separator=" - "
|
||||
:legend="__('storefront.shop.filter_price')"
|
||||
:min-label="__('storefront.shop.price_min')"
|
||||
:max-label="__('storefront.shop.price_max')"
|
||||
>
|
||||
@if ($clearPriceUrl)
|
||||
<a
|
||||
href="{{ $clearPriceUrl }}"
|
||||
class="underline-slide [--slide-h:1px] font-display text-sm font-bold uppercase italic"
|
||||
>{{ __('storefront.shop.reset') }}</a>
|
||||
@endif
|
||||
</x-ui.range-slider>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-4 [&_label]:text-base">
|
||||
<p class="font-extrabold text-h4">{{ __('storefront.shop.availability') }}</p>
|
||||
<x-ui.checkbox id="shop-in-stock" name="in_stock" :checked="$listing->inStockOnly">
|
||||
{{ __('storefront.shop.in_stock_only') }}
|
||||
</x-ui.checkbox>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="sr-only">{{ __('storefront.shop.apply') }}</button>
|
||||
</form>
|
||||
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>Hi,</p>
|
||||
|
||||
<p>Payment of <strong>{{ $amount }}</strong> for your order <strong>{{ $reference }}</strong> has been captured.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>Hi,</p>
|
||||
|
||||
<p>Good news — your order <strong>{{ $reference }}</strong> has been delivered.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>Hi,</p>
|
||||
|
||||
<p>A refund of <strong>{{ $amount }}</strong> has been issued for your order <strong>{{ $reference }}</strong>.</p>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p>Hi,</p>
|
||||
|
||||
<p>Your order <strong>{{ $reference }}</strong> is now: <strong>{{ $statusLabel }}</strong></p>
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\LegalPageController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
use App\Http\Controllers\SearchController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Bare `/` has no {locale} segment to prefix-match against, so it's declared outside
|
||||
@@ -22,6 +23,8 @@
|
||||
->group(function () {
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
|
||||
Route::get('/products', [ProductController::class, 'index'])->name('products');
|
||||
|
||||
Route::get('/products/{id}', [ProductController::class, 'show'])->name(
|
||||
'product.show',
|
||||
);
|
||||
@@ -30,6 +33,8 @@
|
||||
'category.show',
|
||||
);
|
||||
|
||||
Route::get('/search', [SearchController::class, 'show'])->name('search');
|
||||
|
||||
Route::get('/contact', [ContactController::class, 'index'])->name('contact');
|
||||
|
||||
Route::get('/terms-and-conditions', [LegalPageController::class, 'terms'])->name(
|
||||
|
||||
Reference in New Issue
Block a user