product options layout and add to cart button in homepage

This commit is contained in:
elvira
2026-09-21 18:53:45 +03:00
parent af0245de43
commit a107184010
10 changed files with 225 additions and 537 deletions
+64 -12
View File
@@ -40,26 +40,78 @@ public function show(string $locale, int $id)
$collection = $product['collections'][0] ?? null;
$variantsData = $this->products->variantSummaries($product);
$firstVariant = $product['variants'][0] ?? null;
$option = $firstVariant['options'][0]['option'] ?? null;
// The "Color" option type is the only one that writes a hex code into
// meta (see boboko/core's ColorOptionType) — its presence is how we
// tell a color option (swatches) from any other option (buttons).
$optionIsColor = collect($product['variants'])
->contains(fn (array $variant) => !empty($variant['options'][0]['meta']['hex'] ?? null));
[$productOptions, $variantsData] = $this->buildOptionPicker($id);
return view('product.show', [
'collection' => $collection,
'product' => $product,
'option' => $option,
'optionIsColor' => $optionIsColor,
'productOptions' => $productOptions,
'variantsData' => $variantsData,
]);
}
/**
* One button/swatch group per product option (a product can have several
* — e.g. a custom-photo product with size + style + person-count, each
* combination resolved client-side to one exact ProductVariant, see
* product-form-controller.js) plus the per-variant data the picker
* resolves a selection against.
*
* Reads live Eloquent rather than the Meilisearch-indexed $product array
* the rest of the page uses (same reasoning as checkStock() below): the
* index has no concept of option/value display order (Lunar's
* `position` column) or a stable value id, both of which the picker
* needs — to render values in the merchant's intended order, and to
* match a combination back to one exact variant without relying on
* translated label strings staying unique.
*
* @return array{0: array, 1: array}
*/
private function buildOptionPicker(int $productId): array
{
$variants = ProductVariant::query()
->where('product_id', $productId)
->with(['values.option', 'prices', 'images'])
->get();
$productOptions = $variants
->flatMap(fn (ProductVariant $variant) => $variant->values)
->unique('id')
->groupBy(fn ($value) => $value->option->handle)
->map(function ($values, $handle) {
$sorted = $values->sortBy([['position', 'asc'], ['id', 'asc']]);
return [
'handle' => $handle,
'label' => $sorted->first()->option->translate('name'),
// The "Color" option type is the only one that writes a
// hex code into meta (see boboko/core's ColorOptionType)
// — its presence is how we tell a color option (swatches)
// from any other option (buttons).
'isColor' => $sorted->contains(fn ($v) => !empty($v->meta['hex'] ?? null)),
'values' => $sorted->map(fn ($v) => [
'id' => $v->id,
'label' => $v->translate('name'),
'hex' => $v->meta['hex'] ?? null,
])->values()->all(),
];
})
->values()
->all();
$variantsData = $variants->map(fn (ProductVariant $variant) => [
'id' => $variant->id,
'price' => $variant->prices->first()?->price?->decimal(),
'image' => $variant->images->first()?->getUrl(),
// handle => selected value id, for matching a combination of
// selections back to this variant — see selectVariant() in
// product-form-controller.js.
'options' => $variant->values->mapWithKeys(fn ($v) => [$v->option->handle => $v->id])->all(),
])->values()->all();
return [$productOptions, $variantsData];
}
/**
* A storefront-owned, checkout-module-independent stock check — the
* product page's "Add to cart" calls this first and only submits to the
Generated
+94 -240
View File
@@ -515,11 +515,11 @@
},
{
"name": "boboko/core",
"version": "0.18.0",
"version": "0.19.0",
"source": {
"type": "git",
"url": "https://code.radical-elements.com/boboko/core.git",
"reference": "2cc6f5e5f0d43102001d65b60810423a35e83e1f"
"reference": "0437057e5d604e3c05479089071fcbe79ad69bb7"
},
"require": {
"laravel/framework": "^12.0",
@@ -558,7 +558,8 @@
"Modules\\Core\\Providers\\CartServiceProvider",
"Modules\\Core\\Providers\\ReviewServiceProvider",
"Modules\\Core\\Providers\\ShippingServiceProvider",
"Modules\\Core\\Providers\\OrderServiceProvider"
"Modules\\Core\\Providers\\OrderServiceProvider",
"Modules\\Core\\Providers\\PrivacyServiceProvider"
]
}
},
@@ -568,7 +569,7 @@
}
},
"description": "Core module — authentication and shared panel behaviour",
"time": "2026-09-15T21:06:04+00:00"
"time": "2026-09-17T22:30:37+00:00"
},
{
"name": "brick/math",
@@ -1062,16 +1063,16 @@
},
{
"name": "danharrin/livewire-rate-limiting",
"version": "v2.2.1",
"version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/danharrin/livewire-rate-limiting.git",
"reference": "69436717dc70e30f80d7f8fd02504c22992a9ad5"
"reference": "46c4ceaf7997c713c79afe7c507a39649f6c9d6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/danharrin/livewire-rate-limiting/zipball/69436717dc70e30f80d7f8fd02504c22992a9ad5",
"reference": "69436717dc70e30f80d7f8fd02504c22992a9ad5",
"url": "https://api.github.com/repos/danharrin/livewire-rate-limiting/zipball/46c4ceaf7997c713c79afe7c507a39649f6c9d6f",
"reference": "46c4ceaf7997c713c79afe7c507a39649f6c9d6f",
"shasum": ""
},
"require": {
@@ -1112,7 +1113,7 @@
"type": "github"
}
],
"time": "2026-08-06T08:41:51+00:00"
"time": "2026-09-18T07:38:01+00:00"
},
{
"name": "dflydev/dot-access-data",
@@ -1329,27 +1330,25 @@
},
{
"name": "doctrine/lexer",
"version": "3.0.1",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
"reference": "e96fe45e92a54233726014a7cc7340abf29bb14c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
"reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/e96fe45e92a54233726014a7cc7340abf29bb14c",
"reference": "e96fe45e92a54233726014a7cc7340abf29bb14c",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^12",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5",
"psalm/plugin-phpunit": "^0.18.3",
"vimeo/psalm": "^5.21"
"doctrine/coding-standard": "^14",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^10.5.58 || ^12.5.4"
},
"type": "library",
"autoload": {
@@ -1386,7 +1385,7 @@
],
"support": {
"issues": "https://github.com/doctrine/lexer/issues",
"source": "https://github.com/doctrine/lexer/tree/3.0.1"
"source": "https://github.com/doctrine/lexer/tree/3.0.2"
},
"funding": [
{
@@ -1402,7 +1401,7 @@
"type": "tidelift"
}
],
"time": "2024-02-05T11:56:58+00:00"
"time": "2026-06-14T20:44:06+00:00"
},
{
"name": "dompdf/dompdf",
@@ -1692,7 +1691,7 @@
},
{
"name": "filament/actions",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
@@ -1741,16 +1740,16 @@
},
{
"name": "filament/filament",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "ff848c7750b49f0bb532d44c6768f0e601fb09d2"
"reference": "1215d7d79b5609f66c1e432933610eefa0ab3ad6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/ff848c7750b49f0bb532d44c6768f0e601fb09d2",
"reference": "ff848c7750b49f0bb532d44c6768f0e601fb09d2",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/1215d7d79b5609f66c1e432933610eefa0ab3ad6",
"reference": "1215d7d79b5609f66c1e432933610eefa0ab3ad6",
"shasum": ""
},
"require": {
@@ -1795,20 +1794,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T16:56:27+00:00"
"time": "2026-09-20T19:15:07+00:00"
},
{
"name": "filament/forms",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "18e3382115ed3062a074736c9e80cfcb3ffe2221"
"reference": "10d7ba430544ae706a406713105ba37b18f52008"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/18e3382115ed3062a074736c9e80cfcb3ffe2221",
"reference": "18e3382115ed3062a074736c9e80cfcb3ffe2221",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/10d7ba430544ae706a406713105ba37b18f52008",
"reference": "10d7ba430544ae706a406713105ba37b18f52008",
"shasum": ""
},
"require": {
@@ -1845,20 +1844,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T17:00:54+00:00"
"time": "2026-09-20T18:45:30+00:00"
},
{
"name": "filament/infolists",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
"reference": "b05093c8ff726d28eb75df29fa21bdd836386ded"
"reference": "1979591422ff823543622b87e0276cdca3f863e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/b05093c8ff726d28eb75df29fa21bdd836386ded",
"reference": "b05093c8ff726d28eb75df29fa21bdd836386ded",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/1979591422ff823543622b87e0276cdca3f863e8",
"reference": "1979591422ff823543622b87e0276cdca3f863e8",
"shasum": ""
},
"require": {
@@ -1890,20 +1889,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T16:59:31+00:00"
"time": "2026-09-20T18:45:56+00:00"
},
{
"name": "filament/notifications",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
"reference": "f481bcfb705ad650f96336e7496814419907639c"
"reference": "40989f93ec3dc36f5582024ca41f99333a4c0ea7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/f481bcfb705ad650f96336e7496814419907639c",
"reference": "f481bcfb705ad650f96336e7496814419907639c",
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/40989f93ec3dc36f5582024ca41f99333a4c0ea7",
"reference": "40989f93ec3dc36f5582024ca41f99333a4c0ea7",
"shasum": ""
},
"require": {
@@ -1937,20 +1936,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T16:55:37+00:00"
"time": "2026-09-20T18:48:58+00:00"
},
{
"name": "filament/query-builder",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/query-builder.git",
"reference": "f7c1eb9eeaded573838c1f3b8d0a63f55434331a"
"reference": "0428645f4cd0a232b6bc446437cbfafa366e56c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/query-builder/zipball/f7c1eb9eeaded573838c1f3b8d0a63f55434331a",
"reference": "f7c1eb9eeaded573838c1f3b8d0a63f55434331a",
"url": "https://api.github.com/repos/filamentphp/query-builder/zipball/0428645f4cd0a232b6bc446437cbfafa366e56c0",
"reference": "0428645f4cd0a232b6bc446437cbfafa366e56c0",
"shasum": ""
},
"require": {
@@ -1983,20 +1982,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T16:55:48+00:00"
"time": "2026-09-20T18:53:32+00:00"
},
{
"name": "filament/schemas",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/schemas.git",
"reference": "fec2747e8cf1e6ca76d6c883c7f1019da994f771"
"reference": "dc4eeb57671b104b9e60f6d3f435f803c705e6df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/schemas/zipball/fec2747e8cf1e6ca76d6c883c7f1019da994f771",
"reference": "fec2747e8cf1e6ca76d6c883c7f1019da994f771",
"url": "https://api.github.com/repos/filamentphp/schemas/zipball/dc4eeb57671b104b9e60f6d3f435f803c705e6df",
"reference": "dc4eeb57671b104b9e60f6d3f435f803c705e6df",
"shasum": ""
},
"require": {
@@ -2028,11 +2027,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-08T10:20:09+00:00"
"time": "2026-09-20T18:49:21+00:00"
},
{
"name": "filament/spatie-laravel-media-library-plugin",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git",
@@ -2069,16 +2068,16 @@
},
{
"name": "filament/support",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
"reference": "6ff50570ff1f2c6c1caf6ed1e7a2ce146b901106"
"reference": "6bd7bdb5f5ff3b1a70cac6dabe28f5d996a36bcc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/support/zipball/6ff50570ff1f2c6c1caf6ed1e7a2ce146b901106",
"reference": "6ff50570ff1f2c6c1caf6ed1e7a2ce146b901106",
"url": "https://api.github.com/repos/filamentphp/support/zipball/6bd7bdb5f5ff3b1a70cac6dabe28f5d996a36bcc",
"reference": "6bd7bdb5f5ff3b1a70cac6dabe28f5d996a36bcc",
"shasum": ""
},
"require": {
@@ -2124,20 +2123,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T16:59:53+00:00"
"time": "2026-09-20T19:14:52+00:00"
},
{
"name": "filament/tables",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "51f2f2d19eddae424b8b4a08ab9a14770e2acc9d"
"reference": "8a0c6634d752e0b71ed0871f92d533c0d03d7601"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/51f2f2d19eddae424b8b4a08ab9a14770e2acc9d",
"reference": "51f2f2d19eddae424b8b4a08ab9a14770e2acc9d",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/8a0c6634d752e0b71ed0871f92d533c0d03d7601",
"reference": "8a0c6634d752e0b71ed0871f92d533c0d03d7601",
"shasum": ""
},
"require": {
@@ -2170,20 +2169,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-15T16:56:17+00:00"
"time": "2026-09-20T18:53:09+00:00"
},
{
"name": "filament/widgets",
"version": "v4.13.2",
"version": "v4.13.4",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
"reference": "3d5a4dde469ad9f8e6cb2d5928e4882f6a92afe1"
"reference": "5ec044730102c3444d019232d8edc3d8922876c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/widgets/zipball/3d5a4dde469ad9f8e6cb2d5928e4882f6a92afe1",
"reference": "3d5a4dde469ad9f8e6cb2d5928e4882f6a92afe1",
"url": "https://api.github.com/repos/filamentphp/widgets/zipball/5ec044730102c3444d019232d8edc3d8922876c6",
"reference": "5ec044730102c3444d019232d8edc3d8922876c6",
"shasum": ""
},
"require": {
@@ -2214,7 +2213,7 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-09-07T18:36:33+00:00"
"time": "2026-09-20T18:53:38+00:00"
},
{
"name": "fruitcake/php-cors",
@@ -7844,20 +7843,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.3",
"version": "4.9.4",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "1df15849d00943a67d677dc9cfd80795f038c9f8"
"reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8",
"reference": "1df15849d00943a67d677dc9cfd80795f038c9f8",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2",
"reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2",
"shasum": ""
},
"require": {
"brick/math": ">=0.8.16 <=0.18",
"brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -7916,9 +7915,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.3"
"source": "https://github.com/ramsey/uuid/tree/4.9.4"
},
"time": "2026-06-18T03:57:49+00:00"
"time": "2026-09-16T11:39:30+00:00"
},
{
"name": "ryangjchandler/blade-capture-directive",
@@ -8000,35 +7999,33 @@
},
{
"name": "sabberworm/php-css-parser",
"version": "v9.4.0",
"version": "v9.5.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f"
"reference": "f284e63b6e891e0c28631e54ba06c3ed102a9ef3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f",
"reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/f284e63b6e891e0c28631e54ba06c3ed102a9ef3",
"reference": "f284e63b6e891e0c28631e54ba06c3ed102a9ef3",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "1.4.0",
"phpstan/extension-installer": "1.4.3",
"phpstan/phpstan": "1.12.33 || 2.2.2",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.16",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11",
"phpunit/phpunit": "8.5.52",
"phpstan/phpstan": "1.12.33 || 2.2.9",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.18",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.12",
"phpunit/phpunit": "8.5.54",
"rawr/phpunit-data-provider": "3.3.1",
"rector/rector": "1.2.10 || 2.4.6",
"rector/type-perfect": "1.0.0 || 2.1.3",
"squizlabs/php_codesniffer": "4.0.1",
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3"
"rector/rector": "1.2.10 || 2.6.2",
"rector/type-perfect": "1.0.0 || 2.1.4",
"squizlabs/php_codesniffer": "4.0.4"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
@@ -8036,7 +8033,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.5.x-dev"
"dev-main": "9.6.x-dev"
}
},
"autoload": {
@@ -8074,9 +8071,9 @@
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0"
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.5.0"
},
"time": "2026-06-18T15:10:53+00:00"
"time": "2026-09-20T15:02:00+00:00"
},
{
"name": "scrivo/highlight.php",
@@ -12603,16 +12600,16 @@
},
{
"name": "technikermathe/blade-lucide-icons",
"version": "v3.180.0",
"version": "v3.181.0",
"source": {
"type": "git",
"url": "https://github.com/PascaleBeier/blade-lucide-icons.git",
"reference": "adac9da3a65910fedc271129eea5340093910203"
"reference": "64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/adac9da3a65910fedc271129eea5340093910203",
"reference": "adac9da3a65910fedc271129eea5340093910203",
"url": "https://api.github.com/repos/PascaleBeier/blade-lucide-icons/zipball/64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98",
"reference": "64ccac4ecfe1b833e9a5cf1ebe7216acbe3cac98",
"shasum": ""
},
"require": {
@@ -12662,152 +12659,9 @@
],
"support": {
"issues": "https://github.com/PascaleBeier/blade-lucide-icons/issues",
"source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.180.0"
"source": "https://github.com/PascaleBeier/blade-lucide-icons/tree/v3.181.0"
},
"time": "2026-09-15T02:25:56+00:00"
},
{
"name": "thecodingmachine/safe",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/safe.git",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.2"
},
"type": "library",
"autoload": {
"files": [
"lib/special_cases.php",
"generated/apache.php",
"generated/apcu.php",
"generated/array.php",
"generated/bzip2.php",
"generated/calendar.php",
"generated/classobj.php",
"generated/com.php",
"generated/cubrid.php",
"generated/curl.php",
"generated/datetime.php",
"generated/dir.php",
"generated/eio.php",
"generated/errorfunc.php",
"generated/exec.php",
"generated/fileinfo.php",
"generated/filesystem.php",
"generated/filter.php",
"generated/fpm.php",
"generated/ftp.php",
"generated/funchand.php",
"generated/gettext.php",
"generated/gmp.php",
"generated/gnupg.php",
"generated/hash.php",
"generated/ibase.php",
"generated/ibmDb2.php",
"generated/iconv.php",
"generated/image.php",
"generated/imap.php",
"generated/info.php",
"generated/inotify.php",
"generated/json.php",
"generated/ldap.php",
"generated/libxml.php",
"generated/lzf.php",
"generated/mailparse.php",
"generated/mbstring.php",
"generated/misc.php",
"generated/mysql.php",
"generated/mysqli.php",
"generated/network.php",
"generated/oci8.php",
"generated/opcache.php",
"generated/openssl.php",
"generated/outcontrol.php",
"generated/pcntl.php",
"generated/pcre.php",
"generated/pgsql.php",
"generated/posix.php",
"generated/ps.php",
"generated/pspell.php",
"generated/readline.php",
"generated/rnp.php",
"generated/rpminfo.php",
"generated/rrd.php",
"generated/sem.php",
"generated/session.php",
"generated/shmop.php",
"generated/sockets.php",
"generated/sodium.php",
"generated/solr.php",
"generated/spl.php",
"generated/sqlsrv.php",
"generated/ssdeep.php",
"generated/ssh2.php",
"generated/stream.php",
"generated/strings.php",
"generated/swoole.php",
"generated/uodbc.php",
"generated/uopz.php",
"generated/url.php",
"generated/var.php",
"generated/xdiff.php",
"generated/xml.php",
"generated/xmlrpc.php",
"generated/yaml.php",
"generated/yaz.php",
"generated/zip.php",
"generated/zlib.php"
],
"classmap": [
"lib/DateTime.php",
"lib/DateTimeImmutable.php",
"lib/Exceptions/",
"generated/Exceptions/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": {
"issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
},
"funding": [
{
"url": "https://github.com/OskarStark",
"type": "github"
},
{
"url": "https://github.com/shish",
"type": "github"
},
{
"url": "https://github.com/silasjoisten",
"type": "github"
},
{
"url": "https://github.com/staabm",
"type": "github"
}
],
"time": "2026-02-04T18:08:13+00:00"
"time": "2026-09-18T02:09:04+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
View File
View File
@@ -16,13 +16,17 @@ export default class extends Controller {
}
connect() {
const params = new URLSearchParams(window.location.search)
const urlId = parseInt(params.get('variant'))
const defaultId = this.variantsValue[0]?.id
const params = new URLSearchParams(window.location.search)
const urlId = parseInt(params.get('variant'))
const urlVariant = this.variantsValue.find(v => v.id === urlId)
const initial = urlVariant ?? this.variantsValue[0]
this.selectedValue = urlId && this.variantsValue.find(v => v.id === urlId)
? urlId
: defaultId
// A product can have several independent options (e.g. size + style
// + person-count) — this tracks the currently-picked value id per
// option handle, and selectVariant() below resolves the full
// combination back to one exact variant on every change.
this.selections = { ...initial?.options }
this.selectedValue = initial?.id
// Capture phase, on this controller's own root element (an ancestor
// of the checkout module's add-to-cart <form>) — runs BEFORE that
@@ -108,11 +112,24 @@ export default class extends Controller {
}
selectVariant(event) {
const id = parseInt(event.currentTarget.dataset.variantId)
this.selectedValue = id
const option = event.currentTarget.dataset.option
const valueId = parseInt(event.currentTarget.dataset.valueId)
this.selections = { ...this.selections, [option]: valueId }
const match = this.variantsValue.find(variant =>
Object.keys(this.selections).every(key => variant.options?.[key] === this.selections[key])
)
// No variant exists for this combination (e.g. an option value that
// isn't offered together with another currently-selected value) —
// leave the previous selection in place rather than pointing the
// add-to-cart form at nothing.
if (!match) return
this.selectedValue = match.id
const url = new URL(window.location)
url.searchParams.set('variant', id)
url.searchParams.set('variant', match.id)
window.history.pushState({}, '', url)
}
@@ -140,12 +157,16 @@ export default class extends Controller {
if (purchasableInput) purchasableInput.value = id
this.swatchTargets.forEach(swatch => {
const isSelected = parseInt(swatch.dataset.variantId) === id
const isSelected = this.selections[swatch.dataset.option] === parseInt(swatch.dataset.valueId)
swatch.classList.toggle('is-selected', isSelected)
swatch.setAttribute('aria-pressed', String(isSelected))
if (isSelected && this.hasColorNameTarget) {
this.colorNameTarget.textContent = swatch.getAttribute('aria-label')
if (isSelected) {
// Each color-option group has its own colorName echo (see
// x-ui.color-swatch) — matched by option handle so a swatch
// in one group never overwrites another group's label.
const colorName = this.colorNameTargets.find(target => target.dataset.option === swatch.dataset.option)
if (colorName) colorName.textContent = swatch.getAttribute('aria-label')
}
})
}
@@ -1,12 +1,11 @@
{{-- $variants: array of Modules\Core\Catalog\Services\ProductIndexer's mapVariant()
shape (id, options: [{option, value, meta}], ...) — plain arrays, not Eloquent
models, since this is fed from Modules\Core\Catalog\Services\ProductService. --}}
@props(['variants', 'option' => null])
{{-- $values: one product option's de-duplicated, ordered values, as built by
ProductController::buildOptionPicker() — [{id, label, hex}]. --}}
@props(['values', 'option' => null, 'optionHandle' => null])
<div {{ $attributes }}>
@if($option)
<p class="font-bold mb-3 text-sm uppercase tracking-wide">
{{ $option }}: <span data-product-form-target="colorName" class="font-normal normal-case"></span>
{{ $option }}: <span data-product-form-target="colorName" data-option="{{ $optionHandle }}" class="font-normal normal-case"></span>
</p>
@endif
@@ -15,22 +14,17 @@ class="flex flex-wrap gap-2"
role="group"
aria-label="{{ $option ?? 'Color' }}"
>
@foreach($variants as $variant)
@php
$value = $variant['options'][0] ?? null;
$label = $value['value'] ?? '';
$bg = $value['meta']['hex'] ?? '#cccccc';
@endphp
@foreach($values as $value)
<button
type="button"
class="color-swatch"
data-product-form-target="swatch"
data-action="click->product-form#selectVariant"
data-variant-id="{{ $variant['id'] }}"
style="background-color: {{ $bg }};"
aria-label="{{ $label }}"
data-option="{{ $optionHandle }}"
data-value-id="{{ $value['id'] }}"
style="background-color: {{ $value['hex'] ?? '#cccccc' }};"
aria-label="{{ $value['label'] }}"
aria-pressed="false"
title="{{ $label }}"
></button>
@endforeach
</div>
@@ -1,9 +1,8 @@
{{-- $variants: array of Modules\Core\Catalog\Services\ProductIndexer's mapVariant()
shape (id, options: [{option, value, meta}], ...) — plain arrays, not Eloquent
models, since this is fed from Modules\Core\Catalog\Services\ProductService.
Use this instead of <x-ui.color-swatch> for options that aren't a color
(no meta.hex), where a swatch dot has nothing meaningful to show. --}}
@props(['variants', 'option' => null])
{{-- $values: one product option's de-duplicated, ordered values, as built by
ProductController::buildOptionPicker() — [{id, label, hex}]. Use this
instead of <x-ui.color-swatch> for options that aren't a color (no hex),
where a swatch dot has nothing meaningful to show. --}}
@props(['values', 'option' => null, 'optionHandle' => null])
<div {{ $attributes }}>
@if($option)
@@ -17,20 +16,17 @@ class="flex flex-wrap gap-2"
role="group"
aria-label="{{ $option ?? 'Option' }}"
>
@foreach($variants as $variant)
@php
$value = $variant['options'][0] ?? null;
$label = $value['value'] ?? '';
@endphp
@foreach($values as $value)
<x-ui.button
variant="secondary"
size="sm"
data-product-form-target="swatch"
data-action="click->product-form#selectVariant"
data-variant-id="{{ $variant['id'] }}"
aria-label="{{ $label }}"
data-option="{{ $optionHandle }}"
data-value-id="{{ $value['id'] }}"
aria-label="{{ $value['label'] }}"
aria-pressed="false"
>{{ $label }}</x-ui.button>
>{{ $value['label'] }}</x-ui.button>
@endforeach
</div>
</div>
+10 -3
View File
@@ -60,9 +60,16 @@ class="w-full h-full object-cover"
@endif
</a>
<x-ui.button size="md" position="absolute" class="opacity-0 group-hover:opacity-100 transition-opacity duration-100">
{{ __('storefront.product.add_to_cart') }}
</x-ui.button>
@if ($product['variantId'] ?? null)
<x-checkout::add-to-cart
:purchasable="$product['variantId']"
class="absolute opacity-0 group-hover:opacity-100 has-[.bbk-add-to-cart-error:not([hidden])]:opacity-100 transition-opacity duration-100"
>
<x-ui.button type="submit" size="md">
{{ __('storefront.product.add_to_cart') }}
</x-ui.button>
</x-checkout::add-to-cart>
@endif
</div>
<div class="flex items-baseline justify-between gap-4 absolute bottom-0 left-0 w-full">
+5 -5
View File
@@ -169,13 +169,13 @@ class="absolute bottom-6 right-8 text-white text-sm"
@endif
</div>
@if($option && !empty($product['variants']))
@if($optionIsColor)
<x-ui.color-swatch :variants="$product['variants']" :option="$option" />
@foreach($productOptions as $productOption)
@if($productOption['isColor'])
<x-ui.color-swatch :values="$productOption['values']" :option="$productOption['label']" :option-handle="$productOption['handle']" />
@else
<x-ui.option-buttons :variants="$product['variants']" :option="$option" />
<x-ui.option-buttons :values="$productOption['values']" :option="$productOption['label']" :option-handle="$productOption['handle']" />
@endif
@endif
@endforeach
<x-checkout::add-to-cart
:purchasable="$variantsData[0]['id'] ?? null"
-236
View File
@@ -1,236 +0,0 @@
@extends('layouts.app')
@section('title', $product->translateAttribute('name') . ' — ' . config('app.name'))
@section('description', $product->translateAttribute('description'))
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-8 py-12">
@php $collection = $product->collections->first(); @endphp
<x-breadcrumb class="mb-14 justify-end" :items="[
$collection
? ['label' => $collection->translateAttribute('name'), 'href' => '/category/' . $collection->id]
: ['label' => 'Products', 'href' => '/products'],
['label' => $product->translateAttribute('name')],
]" />
<div
class="grid grid-cols-1 md:grid-cols-2 gap-12"
data-controller="product-form"
data-product-form-variants-value="{{ json_encode($variantsData) }}"
>
{{-- Image --}}
<div
data-controller="product-gallery"
class="flex gap-4 items-start"
>
@if($product->media->isNotEmpty())
{{-- Thumbnails --}}
<div class="flex flex-col items-center gap-1 w-[116px] shrink-0 -mt-12.5">
<button
type="button"
data-action="click->product-gallery#scrollUp"
class="gallery-arrow w-full flex items-center justify-center py-2"
aria-label="Scroll thumbnails up"
><x-ui.icon name="arrow-up" :size="30" /></button>
<div
data-product-gallery-target="track"
class="flex flex-col gap-4 overflow-hidden"
style="max-height: var(--gallery-height, 600px)"
>
@foreach($product->media as $i => $media)
<button
type="button"
data-action="click->product-gallery#select"
data-product-gallery-target="thumb"
data-src="{{ $media->getUrl() }}"
data-alt="{{ $product->translateAttribute('name') }}"
class="block w-full shrink-0 border border-black overflow-hidden "
aria-label="View image {{ $i + 1 }}"
aria-pressed="{{ $i === 0 ? 'true' : 'false' }}"
>
<!-- opacity-50 transition-opacity {{ $i === 0 ? 'opacity-100' : '' }}" -->
<img src="{{ $media->getUrl() }}" alt="" class="w-full h-auto object-cover" aria-hidden="true">
</button>
@endforeach
</div>
<button
type="button"
data-action="click->product-gallery#scrollDown"
class="gallery-arrow w-full flex items-center justify-center py-2"
aria-label="Scroll thumbnails down"
><x-ui.icon name="arrow-down" :size="30" /></button>
</div>
{{-- Main image --}}
<button
type="button"
class="flex-1 border border-black bg-white cursor-zoom-in block p-0"
data-action="click->product-gallery#openLightbox"
aria-label="View full size image"
>
<img
data-product-form-target="image"
data-product-gallery-target="main"
src="{{ $product->media->first()->getUrl() }}"
alt="{{ $product->translateAttribute('name') }}"
class="w-full h-auto block"
>
</button>
{{-- Lightbox --}}
<div
id="product-lightbox"
popover
class="product-lightbox"
data-product-gallery-target="lightbox"
data-action="click->product-gallery#closeLightboxOnBackdrop"
role="dialog"
aria-label="Product image gallery"
aria-modal="true"
>
{{-- Prev --}}
<button
type="button"
data-action="click->product-gallery#prevImage click->product-gallery#noop"
class="lightbox-arrow absolute left-8 top-1/2 -translate-y-1/2"
aria-label="Previous image"
><x-ui.icon name="arrow-left" :size="72" color="white" /></button>
{{-- Image + close button as a unit --}}
<div class="relative" data-action="click->product-gallery#noop">
<img
data-product-gallery-target="lightboxImage"
src=""
alt=""
class="max-h-[90vh] max-w-[80vw] object-contain block"
>
<button
type="button"
popovertarget="product-lightbox"
popovertargetaction="hide"
class="lightbox-close absolute -top-10 -right-10"
aria-label="Close gallery"
><x-ui.icon name="close" :size="36" color="white" /></button>
</div>
{{-- Next --}}
<button
type="button"
data-action="click->product-gallery#nextImage click->product-gallery#noop"
class="lightbox-arrow absolute right-8 top-1/2 -translate-y-1/2"
aria-label="Next image"
><x-ui.icon name="arrow-right" :size="72" color="white" /></button>
{{-- Counter --}}
<p
data-product-gallery-target="lightboxCounter"
class="absolute bottom-6 right-8 text-white text-sm"
aria-live="polite"
></p>
</div>
@else
<div class="w-full bg-neutral-300 flex items-center justify-center text-neutral-500 min-h-64">
No image
</div>
@endif
</div>
{{-- Info --}}
<div class="flex flex-col gap-6">
<h1 class="font-display font-medium text-4xl lg:text-[54px]">
{{ $product->translateAttribute('name') }}
</h1>
<x-reviews-stars :rating="3" :count="24" :showCount="true" />
@if($product->variants->first()?->prices->isNotEmpty())
<p class="text-2xl font-bold" data-product-form-target="price">
<x-ui.price :amount="$product->variants->first()->prices->first()->price->decimal" />
</p>
@endif
@php
$desc = strip_tags($product->translateAttribute('description') ?? '');
$descTruncated = Str::limit($desc, 137);
$descNeedsMore = mb_strlen($desc) > mb_strlen(rtrim($descTruncated, '.'));
@endphp
<div class="text-base leading-relaxed">
{{ $descTruncated }}
@if($descNeedsMore)
<a href="#tab-panel-description" class="underline-slide font-semibold whitespace-nowrap">Περισσότερα</a>
@endif
</div>
@if($option && $product->variants->count() >= 1)
<x-ui.color-swatch :variants="$product->variants" :option="$option" />
@endif
<div class="flex items-stretch gap-10">
<x-ui.quantity name="quantity" />
<x-ui.button class="flex-1">Προσθήκη στο καλάθι</x-ui.button>
</div>
</div>
</div>
<x-ui.tabs class="mt-16" size="lg" :tabs="[
['id' => 'description', 'label' => 'Περιγραφή'],
// ['id' => 'reviews', 'label' => 'Αξιολογήσεις (' . count($reviews) . ')'],
['id' => 'reviews', 'label' => 'Αξιολογήσεις (3)'],
]">
<x-slot name="description">
<div class="leading-7 [&_p]:mt-4">
{!! $product->translateAttribute('description') !!}
</div>
@if($product->translateAttribute('details'))
<div class="mt-6">{!! $product->translateAttribute('details') !!}</div>
@endif
<ul class="mt-8 flex flex-col gap-3 text-neutral-600 list-disc list-outside pl-5">
<li>Όλα τα προϊόντα εκτυπώνονται και προετοιμάζονται κατά παραγγελία. Ο χρόνος προετοιμασίας κυμαίνεται μεταξύ 2 και 7 εργάσιμων ημερών.</li>
<li>Όλα τα προϊόντα κατασκευάζονται με τρισδιάστατη εκτύπωση σε ειδικούς εκτυπωτές πλαστικού υλικού. Πιθανώς να έχουν εμφανείς γραμμές ένωσης, στρώσεις εκτύπωσης υλικού και μικρές ατέλειες. Είναι φυσιολογικό για το αποτέλεσμα αυτής της δημιουργικής διαδικασίας.</li>
<li>Όλα τα προϊόντα είναι σχεδιασμένα και κατασκευασμένα είτε για διακόσμηση είτε για χρήση σε λογικά, ρεαλιστικά πλαίσια. Υπερβολική ισχύς, υψηλότατες θερμοκρασίες και αποσυναρμολόγηση μπορεί να προκαλέσουν ζημιά στο προϊόν για την οποία δεν ευθύνεται το 3Dealer.</li>
</ul>
</x-slot>
<x-slot name="reviews">
{{-- @if(count($reviews) > 0)
<div class="mb-10">
@foreach($reviews as $review)
<x-review-card :review="$review" />
@endforeach
</div>
@else
<p class="text-neutral-500 mb-10">Δεν υπάρχουν αξιολογήσεις ακόμα.</p>
@endif
<h3 class="text-h4 font-bold">
{{ count($reviews) > 0 ? 'Πρόσθεσε μια' : 'Γράψε την πρώτη' }} αξιολόγηση για το «{{ $product->translateAttribute('name') }}»
</h3> --}}
<x-review-form :product="$product" />
</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' => '#'],
]"
/>
</div>
<x-newsletter class="py-20" />
@endsection