generated from boboko/starter
Compare commits
12
Commits
8a07c772f8
...
elv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9abe45c5f7 | ||
|
|
e57bfe3d42 | ||
|
|
72b9abf92f | ||
|
|
1f0612861b | ||
|
|
b2207a622c | ||
|
|
b87e22381f | ||
|
|
53f30ff51a | ||
|
|
210ed3b094 | ||
|
|
5e2ec7a60a | ||
|
|
2d1624bcb2 | ||
|
|
ea6ebe435e | ||
|
|
2d75bb9e01 |
@@ -19,6 +19,9 @@
|
||||
/public/sitemap.xml
|
||||
/public/logos/core
|
||||
/public/storage
|
||||
/public/css/filament
|
||||
/public/js/filament
|
||||
/public/fonts/filament
|
||||
/storage/*.key
|
||||
/storage/framework/migrated
|
||||
/storage/pail
|
||||
|
||||
@@ -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}
|
||||
|
||||
+32
-11
@@ -5,17 +5,38 @@ set -e
|
||||
# by app only) — just wait for app's migration to finish, then start the process.
|
||||
mkdir -p storage/app/public storage/framework/cache storage/framework/sessions storage/framework/views storage/logs storage/framework bootstrap/cache
|
||||
|
||||
if [ "$APP_ENV" != "production" ]; then
|
||||
echo "[entrypoint] Waiting for migrations to complete..."
|
||||
timeout=60
|
||||
while [ ! -f storage/framework/migrated ] && [ "$timeout" -gt 0 ]; do
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
if [ ! -f storage/framework/migrated ]; then
|
||||
echo "[entrypoint] Timed out waiting for migrations" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Same reasoning as entrypoint.sh: this container gets replaced on every deploy
|
||||
# (dev and production alike), so this is a fresh boot clearing stale artifacts
|
||||
# left on disk (cached config, compiled views), not a running process being
|
||||
# told to forget in-memory code. queue:work/schedule:work themselves still
|
||||
# can't pick up a later code change without an actual process restart — this
|
||||
# only fixes what's stale on disk at boot.
|
||||
echo "[entrypoint] Clearing cached config/routes/views..."
|
||||
php artisan optimize:clear --quiet
|
||||
echo "[entrypoint] Caches cleared"
|
||||
|
||||
# Universal, in both dev and production — app's entrypoint always writes this
|
||||
# marker after `migrate --force` completes (single app instance, so there's
|
||||
# exactly one writer), and queue/scheduler must never start against a database
|
||||
# schema that migration hasn't finished bringing up to date yet.
|
||||
echo "[entrypoint] Waiting for migrations to complete..."
|
||||
timeout=60
|
||||
while [ ! -f storage/framework/migrated ] && [ "$timeout" -gt 0 ]; do
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
if [ ! -f storage/framework/migrated ]; then
|
||||
echo "[entrypoint] Timed out waiting for migrations" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$APP_ENV" = "production" ]; then
|
||||
# Same reasoning as entrypoint.sh's own production-only optimize step —
|
||||
# queue:work/schedule:work read config on every job/tick too, so this
|
||||
# avoids paying the same uncached-config cost app pays per request.
|
||||
echo "[entrypoint] Caching config/routes/views for production..."
|
||||
php artisan optimize --quiet
|
||||
echo "[entrypoint] Production caches built"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
+70
-22
@@ -4,6 +4,12 @@ set -e
|
||||
# Only the app container runs setup; queue/scheduler use entrypoint-worker.sh instead
|
||||
# and just wait on the migrated marker this script writes below.
|
||||
if [ "$APP_ENV" != "production" ]; then
|
||||
# Dev-only: the app dir is bind-mounted from a fresh checkout, so there's no
|
||||
# image-build step that already installed vendor/ or published assets — this
|
||||
# container has to do it at boot instead. In production the Dockerfile already
|
||||
# runs composer install and asset publishing at IMAGE BUILD time (see
|
||||
# Dockerfile's `production` stage), so repeating them here would be wasteful,
|
||||
# not just redundant-but-safe.
|
||||
echo "[entrypoint] Installing Composer dependencies..."
|
||||
git config --global --add safe.directory /var/www/html 2>/dev/null || true
|
||||
git config --global --add safe.directory /var/www/boboko-core 2>/dev/null || true
|
||||
@@ -21,38 +27,80 @@ mkdir -p storage/app/public storage/framework/cache storage/framework/sessions s
|
||||
chown -R www-data:www-data storage bootstrap/cache
|
||||
chmod -R 775 storage bootstrap/cache
|
||||
|
||||
# Composer packages (dev) may have just changed above, or (production) this is a
|
||||
# freshly built image — either way, clear any cached config/routes/compiled views
|
||||
# left over from a previous boot before anything below reads them. Production
|
||||
# runs a single app instance that gets replaced on every deploy, not a running
|
||||
# process being told to forget in-memory code — the actual bug this fixes is
|
||||
# stale artifacts still sitting in bootstrap/cache or storage/framework/views on
|
||||
# a fresh boot (e.g. after the Lunar 1.5/Filament v4 upgrade, a leftover compiled
|
||||
# view referenced a class that upgrade removed).
|
||||
echo "[entrypoint] Clearing cached config/routes/views..."
|
||||
php artisan optimize:clear --quiet
|
||||
echo "[entrypoint] Caches cleared"
|
||||
|
||||
php artisan storage:link --quiet 2>/dev/null || true
|
||||
|
||||
if [ "$APP_ENV" != "production" ]; then
|
||||
# The app dir is bind-mounted from a fresh checkout, so package assets (which the
|
||||
# Dockerfile publishes at build time in production) need to be generated here
|
||||
# instead. Cheap and idempotent, safe to repeat on every boot.
|
||||
# Dev-only for the same reason as the composer step above — production's
|
||||
# image already has these published at build time.
|
||||
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
|
||||
|
||||
echo "[entrypoint] Running migrations..."
|
||||
rm -f storage/framework/migrated
|
||||
php artisan migrate --force
|
||||
echo "[entrypoint] Touching migrated file"
|
||||
touch storage/framework/migrated
|
||||
echo "[entrypoint] Touched migrated file"
|
||||
# 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
|
||||
# that must be current on every boot, not a build-time concern composer/assets
|
||||
# are. Safe to run unconditionally on every boot because production runs a
|
||||
# single app instance — no concurrent replicas that would race each other
|
||||
# running `migrate --force` at the same time.
|
||||
|
||||
# boboko/core overrides lunar:install to skip the interactive prompts (migrate
|
||||
# confirm, admin creation, GitHub star) and just seed the idempotent store
|
||||
# defaults: countries, channel, language, currency, tax zone, attributes,
|
||||
# product type. queue/scheduler wait on the marker above rather than running
|
||||
# this themselves, since the country import's check-then-insert isn't safe to
|
||||
# run concurrently.
|
||||
echo "[entrypoint] Trying Lunar install"
|
||||
php artisan lunar:install --quiet || true
|
||||
echo "[entrypoint] Running migrations..."
|
||||
rm -f storage/framework/migrated
|
||||
php artisan migrate --force
|
||||
echo "[entrypoint] Touching migrated file"
|
||||
touch storage/framework/migrated
|
||||
echo "[entrypoint] Touched migrated file"
|
||||
|
||||
# Upserts by primary key (no --refresh), so this stays cheap and idempotent on
|
||||
# every boot rather than flushing and rebuilding the whole index each time.
|
||||
echo "[entrypoint] Syncing search indexes..."
|
||||
php artisan lunar:meilisearch:setup
|
||||
php artisan lunar:search:index --quiet || true
|
||||
# boboko/core overrides lunar:install to skip the interactive prompts (migrate
|
||||
# confirm, admin creation, GitHub star) and just seed the idempotent store
|
||||
# defaults: countries, channel, language, currency, tax zone, attributes,
|
||||
# product type. queue/scheduler wait on the marker above rather than running
|
||||
# this themselves, since the country import's check-then-insert isn't safe to
|
||||
# run concurrently. A fresh production install needs this seeding the same as
|
||||
# a fresh dev one does — and re-running it against an already-seeded store is
|
||||
# a no-op per key (see InstallLunarCommand's idempotent upserts).
|
||||
echo "[entrypoint] Trying Lunar install"
|
||||
php artisan lunar:install --quiet || true
|
||||
|
||||
# Upserts by primary key (no --refresh), so this stays cheap and idempotent on
|
||||
# every boot rather than flushing and rebuilding the whole index each time. Runs
|
||||
# in production too — a deploy that changed an indexer's field list needs this
|
||||
# to keep Meilisearch's index settings and documents in sync with the code that
|
||||
# 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
|
||||
# The counterpart to optimize:clear above: config/routes/views/events get
|
||||
# compiled once here, at the end of boot, after everything that could
|
||||
# change them (migrations, lunar:install, index sync) has already run —
|
||||
# so production actually gets the request-time performance win caching is
|
||||
# for, rather than staying permanently uncached. Dev deliberately never
|
||||
# does this: caching config here would mean .env/config edits stop taking
|
||||
# effect until the next optimize:clear, which is the opposite of what dev
|
||||
# needs on every iteration.
|
||||
echo "[entrypoint] Caching config/routes/views for production..."
|
||||
php artisan optimize --quiet
|
||||
echo "[entrypoint] Production caches built"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.fi-pagination-items,.fi-pagination-overview,.fi-pagination-records-per-page-select:not(.fi-compact){display:none}@supports (container-type:inline-size){.fi-pagination{container-type:inline-size}@container (min-width: 28rem){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@container (min-width: 56rem){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}@supports not (container-type:inline-size){@media(min-width:640px){.fi-pagination-records-per-page-select.fi-compact{display:none}.fi-pagination-records-per-page-select:not(.fi-compact){display:inline}}@media(min-width:768px){.fi-pagination:not(.fi-simple)>.fi-pagination-previous-btn{display:none}.fi-pagination-overview{display:inline}.fi-pagination:not(.fi-simple)>.fi-pagination-next-btn{display:none}.fi-pagination-items{display:flex}}}.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}.tippy-box[data-theme~=light]{color:#26323d;box-shadow:0 0 20px 4px #9aa1b126,0 4px 80px -8px #24282f40,0 4px 4px -2px #5b5e6926;background-color:#fff}.tippy-box[data-theme~=light][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=light][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff}.tippy-box[data-theme~=light][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=light][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff}.tippy-box[data-theme~=light]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=light]>.tippy-svg-arrow{fill:#fff}.fi-sortable-ghost{opacity:.3}
|
||||
@@ -1 +0,0 @@
|
||||
@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-ext-wght-normal-IYF56FF6.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-wght-normal-JEOLYBOO.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-ext-wght-normal-EOVOK2B5.woff2") format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-wght-normal-IRE366VL.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-vietnamese-wght-normal-CE5GGD3W.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-ext-wght-normal-HA22NDSG.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-wght-normal-NRMW37G5.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
(()=>{var o=({livewireId:s})=>({actionNestingIndex:null,shouldOverlayParentActions:!1,closedActionNestingIndexes:[],focusTargetsByNestingIndex:{},boundSyncActionModals:null,boundOnModalClosed:null,init(){this.boundSyncActionModals=e=>{e.detail.id===s&&this.syncActionModals(e.detail.newActionNestingIndex,e.detail.shouldOverlayParentActions??!1)},this.boundOnModalClosed=e=>{let t=this.getActionNestingIndexFromModalId(e.detail.id);t!==null&&((this.shouldOverlayParentActions||t===0)&&this.restorePreviouslyFocusedElement(t-1),this.closedActionNestingIndexes.push(t))},window.addEventListener("sync-action-modals",this.boundSyncActionModals),window.addEventListener("modal-closed",this.boundOnModalClosed)},destroy(){this.boundSyncActionModals&&(window.removeEventListener("sync-action-modals",this.boundSyncActionModals),this.boundSyncActionModals=null),this.boundOnModalClosed&&(window.removeEventListener("modal-closed",this.boundOnModalClosed),this.boundOnModalClosed=null)},syncActionModals(e,t=!1){if(this.actionNestingIndex===e){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}let n=this.actionNestingIndex!==null&&e!==null&&e>this.actionNestingIndex,i=this.actionNestingIndex!==null&&e!==null&&e<this.actionNestingIndex,d=this.actionNestingIndex===null&&e!==null;if((n||d)&&this.rememberPreviouslyFocusedElement(),this.actionNestingIndex!==null&&!(t&&n)&&this.closeModal(),this.actionNestingIndex=e,this.actionNestingIndex===null){this.restorePreviouslyFocusedElement(-1),this.closedActionNestingIndexes=[],this.focusTargetsByNestingIndex={},this.shouldOverlayParentActions=!1;return}if(this.shouldOverlayParentActions=t,this.closedActionNestingIndexes=this.closedActionNestingIndexes.filter(l=>l<=this.actionNestingIndex),!this.closedActionNestingIndexes.includes(this.actionNestingIndex)){if(!this.$el.querySelector(`#${this.generateModalId(e)}`)){this.$nextTick(()=>{this.openModal(),i&&this.restorePreviouslyFocusedElement()});return}this.openModal(),i&&this.restorePreviouslyFocusedElement()}},rememberPreviouslyFocusedElement(){let e=this.$focus.focused();if(!e)return;if(this.actionNestingIndex===null){this.focusTargetsByNestingIndex[-1]=e;return}this.$el.querySelector(`#${this.generateModalId(this.actionNestingIndex)}`)?.contains(e)&&(this.focusTargetsByNestingIndex[this.actionNestingIndex]=e)},restorePreviouslyFocusedElement(e=this.actionNestingIndex){let t=this.focusTargetsByNestingIndex[e];if(t){for(let n in this.focusTargetsByNestingIndex)Number(n)>=e&&delete this.focusTargetsByNestingIndex[n];requestAnimationFrame(()=>requestAnimationFrame(()=>this.$nextTick(()=>{t.focus({preventScroll:!0})})))}},generateModalId(e){return`fi-${s}-action-`+e},getActionNestingIndexFromModalId(e){let t=`fi-${s}-action-`;if(!e?.startsWith(t))return null;let n=Number(e.slice(t.length));return Number.isInteger(n)?n:null},openModal(){let e=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:e}}))},closeModal(){let e=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:e}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",o)});})();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",unsubscribeLivewireHook:null,visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))},destroy(){this.unsubscribeLivewireHook?.()}}}export{c as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function a({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{if(!Array.isArray(e))return;let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{a as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function r({state:n,splitKeys:i,tagAddedMessage:a,tagRemovedMessage:s}){return{newTag:"",state:n,liveRegionClearTimeout:null,announce(e){let t=this.$refs.liveRegion;t&&(this.liveRegionClearTimeout!==null&&clearTimeout(this.liveRegionClearTimeout),t.textContent=e,this.liveRegionClearTimeout=setTimeout(()=>{t.textContent="",this.liveRegionClearTimeout=null},3e3))},createTag(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.announce(a?.replace(":tag",()=>this.newTag)),this.newTag=""}},deleteTag(e){this.state=this.state.filter(t=>t!==e),this.announce(s?.replace(":tag",()=>e))},reorderTags(e){let t=this.state.splice(e.oldIndex,1)[0];this.state.splice(e.newIndex,0,t),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(e){["Enter",...i].includes(e.key)&&(e.preventDefault(),e.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(i.length===0){this.createTag();return}let e=i.map(t=>t.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(e,"g")).forEach(t=>{this.newTag=t,this.createTag()})})}}}}export{r as default};
|
||||
@@ -1 +0,0 @@
|
||||
function n({initialHeight:e,shouldAutosize:i,state:h}){return{state:h,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=e+"rem")},resize(){if(this.$el.scrollHeight<=0)return;let t=this.$el.style.height;this.$el.style.height="0px";let r=this.$el.scrollHeight;this.$el.style.height=t;let l=parseFloat(e)*parseFloat(getComputedStyle(document.documentElement).fontSize),s=Math.max(r,l)+"px";this.wrapperEl.style.height!==s&&(this.wrapperEl.style.height=s)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{n as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
var i=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let e=this.$el.parentElement;e&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(e),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let e=this.$el.parentElement;if(!e)return;let t=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=e.offsetWidth+parseInt(t.marginInlineStart,10)*-1+parseInt(t.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});export{i as default};
|
||||
@@ -1 +0,0 @@
|
||||
function x({activeTab:p,isScrollable:m,isTabPersisted:T,isTabPersistedInQueryString:w,livewireId:g,schemaKey:D,tab:W,tabQueryStringKey:r}){return{boundResizeHandler:null,boundResetHandler:null,isScrollable:m,resizeDebounceTimer:null,tab:W,unsubscribeLivewireHook:null,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);w&&e.has(r)&&t.includes(e.get(r))&&(this.tab=e.get(r)),(!this.tab||!t.includes(this.tab))&&(this.tab=t[p-1]),this.$watch("tab",()=>{this.updateQueryString(),this.autofocusFields()}),this.autofocusFields(!0),this.unsubscribeLivewireHook=Livewire.hook("commit",({component:i,commit:d,succeed:c,fail:h,respond:u})=>{c(({snapshot:b,effect:n})=>{this.$nextTick(()=>{if(i.id!==g)return;let o=this.getTabs();o.includes(this.tab)||(this.tab=o[p-1]??this.tab)})})}),this.boundResetHandler=i=>{i.detail.livewireId!==g||i.detail.schemaKey!==D||T||w||this.$nextTick(()=>{this.tab=this.getTabs()[p-1]??this.tab})},window.addEventListener("reset-schema-component-state",this.boundResetHandler),m||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,i,d,c,h){let u=t.map(n=>Math.ceil(n.clientWidth)),b=t.map(n=>{let o=n.querySelector(".fi-tabs-item-label"),s=n.querySelector(".fi-badge"),a=Math.ceil(o.clientWidth),l=s?Math.ceil(s.clientWidth):0;return{label:a,badge:l,total:a+(l>0?d+l:0)}});for(let n=0;n<t.length;n++){let o=u.slice(0,n+1).reduce((f,I)=>f+I,0),s=n*i,a=b.slice(n+1),l=a.length>0,v=l?Math.max(...a.map(f=>f.total)):0,y=l?c+v+d+h+i:0;if(o+s+y>e)return n}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!w)return;let t=new URL(window.location.href);t.searchParams.set(r,this.tab),history.replaceState(null,document.title,t.toString())},autofocusFields(t=!1){this.$nextTick(()=>{if(t&&document.activeElement&&document.activeElement!==document.body&&this.$el.compareDocumentPosition(document.activeElement)&Node.DOCUMENT_POSITION_PRECEDING)return;let e=this.$el.querySelectorAll(".fi-sc-tabs-tab.fi-active [autofocus]");for(let i of e)if(i.focus(),document.activeElement===i)break})},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),i=Array.from(t.children).slice(0,-1),d=i.map(s=>s.style.display);i.forEach(s=>s.style.display=""),t.offsetHeight;let c=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),b=this.calculateTabItemGap(i[0]),n=this.calculateTabItemPadding(i[0]),o=this.findOverflowIndex(i,c,h,b,n,u);i.forEach((s,a)=>s.style.display=d[a]),o!==-1&&(this.withinDropdownIndex=o),this.withinDropdownMounted=!0},destroy(){this.unsubscribeLivewireHook?.(),this.boundResetHandler&&window.removeEventListener("reset-schema-component-state",this.boundResetHandler),this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{x as default};
|
||||
@@ -1 +0,0 @@
|
||||
function l({isSkippable:i,isStepPersistedInQueryString:n,key:o,livewireId:h,schemaKey:p,startStep:r,stepQueryStringKey:d}){return{boundResetHandler:null,step:null,init(){this.step=this.getSteps().at(r-1),this.$watch("step",()=>{this.updateQueryString(),this.autofocusFields()}),this.autofocusFields(!0),this.boundResetHandler=t=>{t.detail.livewireId!==h||t.detail.schemaKey!==p||n||this.$nextTick(()=>{this.step=this.getSteps().at(r-1)??this.step})},window.addEventListener("reset-schema-component-state",this.boundResetHandler)},async requestNextStep(){await this.$wire.callSchemaComponentMethod(o,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.scroll())},goToStep(t){let e=this.getStepIndex(t);e<=-1||!i&&e>this.getStepIndex(this.step)||(this.step=t,this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(t=!1){this.$nextTick(()=>{if(t&&document.activeElement&&document.activeElement!==document.body&&this.$el.compareDocumentPosition(document.activeElement)&Node.DOCUMENT_POSITION_PRECEDING)return;let e=this.$refs[`step-${this.step}`]?.querySelectorAll("[autofocus]")??[];for(let s of e)if(s.focus(),document.activeElement===s)break})},getStepIndex(t){let e=this.getSteps().findIndex(s=>s===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return i||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!n)return;let t=new URL(window.location.href);t.searchParams.set(d,this.step),history.replaceState(null,document.title,t.toString())},destroy(){this.boundResetHandler&&window.removeEventListener("reset-schema-component-state",this.boundResetHandler)}}}export{l as default};
|
||||
@@ -1 +0,0 @@
|
||||
(()=>{var d=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let i=this.$el.parentElement;i&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(i),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let i=this.$el.parentElement;if(!i)return;let e=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=i.offsetWidth+parseInt(e.marginInlineStart,10)*-1+parseInt(e.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});var u=function(i,e,n){let t=i;if(e.startsWith("/")&&(n=!0,e=e.slice(1)),n)return e;for(;e.startsWith("../");)t=t.includes(".")?t.slice(0,t.lastIndexOf(".")):null,e=e.slice(3);return["",null,void 0].includes(t)?e:["",null,void 0].includes(e)?t:`${t}.${e}`},h=i=>{let e=Alpine.findClosest(i,n=>n.__livewire);if(!e)throw"Could not find Livewire component in DOM tree.";return e.__livewire};document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentSchema",({livewireId:i,schemaKey:e})=>({handleFormValidationError(n){n.detail.livewireId===i&&this.$nextTick(()=>{let t=this.$el.querySelector("[data-validation-error]");if(!t)return;let r=t;for(;r;)r.dispatchEvent(new CustomEvent("expand")),r=r.parentNode;setTimeout(()=>t.closest("[data-field-wrapper]").scrollIntoView({behavior:"smooth",block:"start",inline:"start"}),200)})},handleClientSideStateReset(n){n.detail.livewireId!==i||n.detail.schemaKey!==e||this.$nextTick(()=>{let t=this.$el.querySelectorAll("[autofocus]");for(let r of t)if(r.offsetParent!==null&&(r.focus(),document.activeElement===r))break})},isStateChanged(n,t){if(n===void 0)return!1;try{return JSON.stringify(n)!==JSON.stringify(t)}catch{return n!==t}}})),window.Alpine.data("filamentSchemaComponent",({path:i,containerPath:e,$wire:n})=>({$statePath:i,$get:(t,r)=>n.$get(u(e,t,r)),$set:(t,r,a,o=!1)=>n.$set(u(e,t,a),r,o),get $state(){return n.$get(i)}})),window.Alpine.data("filamentActionsSchemaComponent",d),Livewire.hook("commit",({component:i,commit:e,respond:n,succeed:t,fail:r})=>{t(({snapshot:a,effects:o})=>{o.dispatches?.forEach(s=>{if(!s.params?.awaitSchemaComponent)return;let l=Array.from(i.el.querySelectorAll(`[wire\\:partial="schema-component::${s.params.awaitSchemaComponent}"]`)).filter(c=>h(c)===i);if(l.length!==1){if(l.length>1)throw`Multiple schema components found with key [${s.params.awaitSchemaComponent}].`;window.addEventListener(`schema-component-${i.id}-${s.params.awaitSchemaComponent}-loaded`,()=>{window.dispatchEvent(new CustomEvent(s.name,{detail:s.params}))},{once:!0})}})})})});})();
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function o({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:i,succeed:a,fail:u,respond:h})=>{a(({snapshot:d,effect:f})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let i=await this.$wire.updateTableColumnState(r,s,this.state);this.error=i?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
function o({name:i,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:r,succeed:a,fail:u,respond:d})=>{a(({snapshot:h,effect:l})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
function o({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:i,succeed:a,fail:u,respond:h})=>{a(({snapshot:d,effect:f})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let i=await this.$wire.updateTableColumnState(r,s,this.state);this.error=i?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
function d(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){this.isLoading=!0;let t=await this.$wire.getGroupedSelectableTableRecordKeys(e);this.areRecordsSelected(this.getRecordsInGroupOnPage(e))?this.deselectRecords(t):this.selectRecords(t),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let l=s.indexOf(this.lastChecked),r=s.indexOf(t),o=[l,r].sort((c,n)=>c-n),i=[];for(let c=o[0];c<=o[1];c++)s[c].checked=t.checked,i.push(s[c].value);t.checked?this.selectRecords(i):this.deselectRecords(i)}this.lastChecked=t}}}export{d as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
|
||||
@@ -4,7 +4,7 @@ @font-face {
|
||||
font-family: "Manrope";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("/fonts/manrope/manrope-v20-greek_latin-300.woff2")
|
||||
src: url("../fonts/manrope/manrope-v20-greek_latin-300.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* manrope-regular - greek_latin */
|
||||
@@ -13,7 +13,7 @@ @font-face {
|
||||
font-family: "Manrope";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("/fonts/manrope/manrope-v20-greek_latin-regular.woff2")
|
||||
src: url("../fonts/manrope/manrope-v20-greek_latin-regular.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* manrope-500 - greek_latin */
|
||||
@@ -22,7 +22,7 @@ @font-face {
|
||||
font-family: "Manrope";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("/fonts/manrope/manrope-v20-greek_latin-500.woff2")
|
||||
src: url("../fonts/manrope/manrope-v20-greek_latin-500.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* manrope-600 - greek_latin */
|
||||
@@ -31,7 +31,7 @@ @font-face {
|
||||
font-family: "Manrope";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("/fonts/manrope/manrope-v20-greek_latin-600.woff2")
|
||||
src: url("../fonts/manrope/manrope-v20-greek_latin-600.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* manrope-700 - greek_latin */
|
||||
@@ -40,7 +40,7 @@ @font-face {
|
||||
font-family: "Manrope";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("/fonts/manrope/manrope-v20-greek_latin-700.woff2")
|
||||
src: url("../fonts/manrope/manrope-v20-greek_latin-700.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* manrope-800 - greek_latin */
|
||||
@@ -49,6 +49,6 @@ @font-face {
|
||||
font-family: "Manrope";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url("/fonts/manrope/manrope-v20-greek_latin-800.woff2")
|
||||
src: url("../fonts/manrope/manrope-v20-greek_latin-800.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
doesn't reflow the header and headings on load — which otherwise
|
||||
throws off scroll restoration on refresh. Same URLs as the @font-face
|
||||
rules in fonts.css, so each is fetched once. --}}
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/manrope/manrope-v20-greek_latin-regular.woff2">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/manrope/manrope-v20-greek_latin-500.woff2">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/manrope/manrope-v20-greek_latin-700.woff2">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/manrope/manrope-v20-greek_latin-800.woff2">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="{{ \Illuminate\Support\Facades\Vite::asset('resources/fonts/manrope/manrope-v20-greek_latin-regular.woff2') }}">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="{{ \Illuminate\Support\Facades\Vite::asset('resources/fonts/manrope/manrope-v20-greek_latin-500.woff2') }}">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="{{ \Illuminate\Support\Facades\Vite::asset('resources/fonts/manrope/manrope-v20-greek_latin-700.woff2') }}">
|
||||
<link rel="preload" as="font" type="font/woff2" crossorigin href="{{ \Illuminate\Support\Facades\Vite::asset('resources/fonts/manrope/manrope-v20-greek_latin-800.woff2') }}">
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
|
||||
@@ -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']],
|
||||
]" />
|
||||
|
||||
@@ -219,16 +219,18 @@ class="absolute bottom-6 right-8 text-white text-sm"
|
||||
</x-slot>
|
||||
</x-ui.tabs>
|
||||
|
||||
<x-product-grid
|
||||
class="mt-20"
|
||||
title="Σχετικά προϊόντα"
|
||||
:products="[
|
||||
['name' => 'Camper', 'price' => '30', 'image' => null, 'href' => '#'],
|
||||
['name' => 'Ponderer IPA','price' => '25', 'image' => null, 'href' => '#'],
|
||||
['name' => 'Squish Red', 'price' => '35', 'image' => null, 'href' => '#'],
|
||||
['name' => 'Red Light', 'price' => '25', 'image' => null, 'href' => '#'],
|
||||
]"
|
||||
/>
|
||||
@if(!empty($product['recommendations']))
|
||||
<x-product-grid
|
||||
class="mt-20"
|
||||
title="Σχετικά προϊόντα"
|
||||
:products="collect($product['recommendations'])->map(fn (array $recommendation) => [
|
||||
'name' => $recommendation['name'],
|
||||
'price' => $recommendation['price'],
|
||||
'image' => $recommendation['image'],
|
||||
'href' => route('product.show', ['id' => $recommendation['id']]),
|
||||
])->all()"
|
||||
/>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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