diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 3252786..315d2bd 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -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 diff --git a/composer.lock b/composer.lock index fb66c1d..7bca38a 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/public/css/lunarphp/panel/lunar-panel.css b/public/css/lunarphp/panel/lunar-panel.css old mode 100644 new mode 100755 diff --git a/public/js/app/components/apexcharts.js b/public/js/app/components/apexcharts.js old mode 100644 new mode 100755 diff --git a/resources/js/stimulus/product-form-controller.js b/resources/js/stimulus/product-form-controller.js index c742af5..ca090fd 100644 --- a/resources/js/stimulus/product-form-controller.js +++ b/resources/js/stimulus/product-form-controller.js @@ -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
) — 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') } }) } diff --git a/resources/views/components/ui/color-swatch.blade.php b/resources/views/components/ui/color-swatch.blade.php index c9b171b..bfa6aa0 100644 --- a/resources/views/components/ui/color-swatch.blade.php +++ b/resources/views/components/ui/color-swatch.blade.php @@ -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])
@if($option)

- {{ $option }}: + {{ $option }}:

@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) @endforeach
diff --git a/resources/views/components/ui/option-buttons.blade.php b/resources/views/components/ui/option-buttons.blade.php index 9867ec9..578fced 100644 --- a/resources/views/components/ui/option-buttons.blade.php +++ b/resources/views/components/ui/option-buttons.blade.php @@ -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 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 for options that aren't a color (no hex), + where a swatch dot has nothing meaningful to show. --}} +@props(['values', 'option' => null, 'optionHandle' => null])
@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) {{ $label }} + >{{ $value['label'] }} @endforeach
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 4495bf3..307ab0e 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -60,9 +60,16 @@ class="w-full h-full object-cover" @endif - - {{ __('storefront.product.add_to_cart') }} - + @if ($product['variantId'] ?? null) + + + {{ __('storefront.product.add_to_cart') }} + + + @endif
diff --git a/resources/views/product/show.blade.php b/resources/views/product/show.blade.php index fb728f4..f51b7ff 100644 --- a/resources/views/product/show.blade.php +++ b/resources/views/product/show.blade.php @@ -169,13 +169,13 @@ class="absolute bottom-6 right-8 text-white text-sm" @endif
- @if($option && !empty($product['variants'])) - @if($optionIsColor) - + @foreach($productOptions as $productOption) + @if($productOption['isColor']) + @else - + @endif - @endif + @endforeach translateAttribute('name') . ' — ' . config('app.name')) -@section('description', $product->translateAttribute('description')) - -@section('content') -
- - @php $collection = $product->collections->first(); @endphp - - - -
- - {{-- Image --}} -
- @if($product->media->isNotEmpty()) - {{-- Thumbnails --}} -
- - -
- @foreach($product->media as $i => $media) - - @endforeach -
- - -
- - {{-- Main image --}} - - - {{-- Lightbox --}} - - @else -
- No image -
- @endif -
- - {{-- Info --}} -
- -

- {{ $product->translateAttribute('name') }} -

- - - - @if($product->variants->first()?->prices->isNotEmpty()) -

- -

- @endif - - @php - $desc = strip_tags($product->translateAttribute('description') ?? ''); - $descTruncated = Str::limit($desc, 137); - $descNeedsMore = mb_strlen($desc) > mb_strlen(rtrim($descTruncated, '.')); - @endphp -
- {{ $descTruncated }} - @if($descNeedsMore) - Περισσότερα - @endif -
- - @if($option && $product->variants->count() >= 1) - - @endif - -
- - Προσθήκη στο καλάθι -
- -
- -
- - - -
- {!! $product->translateAttribute('description') !!} -
- @if($product->translateAttribute('details')) -
{!! $product->translateAttribute('details') !!}
- @endif -
    -
  • Όλα τα προϊόντα εκτυπώνονται και προετοιμάζονται κατά παραγγελία. Ο χρόνος προετοιμασίας κυμαίνεται μεταξύ 2 και 7 εργάσιμων ημερών.
  • -
  • Όλα τα προϊόντα κατασκευάζονται με τρισδιάστατη εκτύπωση σε ειδικούς εκτυπωτές πλαστικού υλικού. Πιθανώς να έχουν εμφανείς γραμμές ένωσης, στρώσεις εκτύπωσης υλικού και μικρές ατέλειες. Είναι φυσιολογικό για το αποτέλεσμα αυτής της δημιουργικής διαδικασίας.
  • -
  • Όλα τα προϊόντα είναι σχεδιασμένα και κατασκευασμένα είτε για διακόσμηση είτε για χρήση σε λογικά, ρεαλιστικά πλαίσια. Υπερβολική ισχύς, υψηλότατες θερμοκρασίες και αποσυναρμολόγηση μπορεί να προκαλέσουν ζημιά στο προϊόν για την οποία δεν ευθύνεται το 3Dealer.
  • -
-
- - {{-- @if(count($reviews) > 0) -
- @foreach($reviews as $review) - - @endforeach -
- @else -

Δεν υπάρχουν αξιολογήσεις ακόμα.

- @endif - -

- {{ count($reviews) > 0 ? 'Πρόσθεσε μια' : 'Γράψε την πρώτη' }} αξιολόγηση για το «{{ $product->translateAttribute('name') }}» -

--}} - - -
-
- - - -
- - - -@endsection