From 08ad871e0c7fd3e4ba497447f9b7d995af449dd8 Mon Sep 17 00:00:00 2001 From: gcch Date: Sat, 11 Apr 2026 10:53:06 +0200 Subject: [PATCH] 2026-04-13 --- cfg/prettierignore | 4 +- composer.json | 6 +- composer.lock | 100 +++--- justfile | 6 +- package.json | 15 +- tests/playwright/capture.spec.ts | 8 +- .../src/scripts/scripts-page-panier.ts | 1 - .../scripts/scripts-page-produit-service.ts | 159 ++++++++- .../src/scripts/scripts-page-produit.ts | 306 +++++------------- 9 files changed, 316 insertions(+), 289 deletions(-) diff --git a/cfg/prettierignore b/cfg/prettierignore index 4310c33f..a2c7e7e7 100755 --- a/cfg/prettierignore +++ b/cfg/prettierignore @@ -1,12 +1,14 @@ -# Tout ce qui est traité par dprint +# Tout ce qui est traité par treefmt *.css *.html *.js +*.json *.md *.mjs *.mts *.php *.scss +*.sh *.ts *.xml *.yaml diff --git a/composer.json b/composer.json index 7accacd9..b9ada9b9 100755 --- a/composer.json +++ b/composer.json @@ -73,12 +73,12 @@ "composer/installers": "^2.3", "crell/fp": "^1.0", "htmlburger/carbon-fields": "^3.6.9", - "illuminate/support": "^13.3", + "illuminate/support": "^13.4", "laravel/helpers": "^1.8.3", "log1x/wp-smtp": "^1.0.2", "lstrojny/functional-php": "^1.18", "mnsami/composer-custom-directory-installer": "^2.0", - "nesbot/carbon": "^3.11.3", + "nesbot/carbon": "^3.11.4", "oscarotero/env": "^2.1.1", "php": ">=8.5", "php-standard-library/php-standard-library": "^6.1.1", @@ -92,7 +92,7 @@ "vlucas/phpdotenv": "^5.6.3", "wpackagist-plugin/falcon": "^2.9.3", "wpackagist-plugin/force-regenerate-thumbnails": "^2.3.0", - "wpackagist-plugin/query-monitor": "^3.20.4", + "wpackagist-plugin/query-monitor": "^4.0.5", "wpackagist-plugin/redis-cache": "^2.7.0", "wpackagist-plugin/wc-multishipping": "^3.0.2", "wpackagist-plugin/woo-preview-emails": "^2.2.14", diff --git a/composer.lock b/composer.lock index 3efc525e..9a85f36f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bf6e098198b957782555eeb97479b37e", + "content-hash": "3144138aa029c01a516e9b6ce664271b", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -2506,16 +2506,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -2565,7 +2565,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0" }, "funding": [ { @@ -2585,20 +2585,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -2650,7 +2650,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0" }, "funding": [ { @@ -2670,20 +2670,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -2734,7 +2734,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.34.0" }, "funding": [ { @@ -2754,20 +2754,20 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { @@ -2814,7 +2814,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.34.0" }, "funding": [ { @@ -2834,20 +2834,20 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + "reference": "2c408a6bb0313e6001a83628dc5506100474254e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", + "reference": "2c408a6bb0313e6001a83628dc5506100474254e", "shasum": "" }, "require": { @@ -2894,7 +2894,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0" }, "funding": [ { @@ -2914,20 +2914,20 @@ "type": "tidelift" } ], - "time": "2025-06-23T16:12:55+00:00" + "time": "2026-04-10T16:50:15+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", "shasum": "" }, "require": { @@ -2977,7 +2977,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.34.0" }, "funding": [ { @@ -2997,7 +2997,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/translation", @@ -3629,15 +3629,15 @@ }, { "name": "wpackagist-plugin/query-monitor", - "version": "3.20.4", + "version": "4.0.5", "source": { "type": "svn", "url": "https://plugins.svn.wordpress.org/query-monitor/", - "reference": "tags/3.20.4" + "reference": "tags/4.0.5" }, "dist": { "type": "zip", - "url": "https://downloads.wordpress.org/plugin/query-monitor.3.20.4.zip" + "url": "https://downloads.wordpress.org/plugin/query-monitor.4.0.5.zip" }, "require": { "composer/installers": "^1.0 || ^2.0" @@ -6715,16 +6715,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", "shasum": "" }, "require": { @@ -6773,7 +6773,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0" }, "funding": [ { @@ -6793,11 +6793,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -6858,7 +6858,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0" }, "funding": [ { @@ -6882,7 +6882,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -6938,7 +6938,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0" }, "funding": [ { diff --git a/justfile b/justfile index ca7b666c..defdae1d 100755 --- a/justfile +++ b/justfile @@ -28,8 +28,10 @@ format: -vendor/bin/twig-cs-fixer fix web/app/themes/haiku-atelier-2024/ # PhpCsFixer # -vendor/bin/php-cs-fixer fix --allow-risky yes - dprint --config "~/.config/dprint/dprint.jsonc" fmt - fish scripts/format-sort-files.fish + treefmt \ + --config-file ~/.config/treefmt/treefmt.toml \ + --tree-root . \ + . # Compile, minifie et optimise Sass vers CSS. [group('css')] diff --git a/package.json b/package.json index 91a32c56..9b9db90b 100755 --- a/package.json +++ b/package.json @@ -73,8 +73,17 @@ "ios >0 and last 3 years" ], "knip": { - "entry": ["web/app/themes/haiku-atelier-2024/src/scripts/*.ts"], - "project": ["web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}"] + "entry": [ + "web/app/themes/haiku-atelier-2024/src/scripts/*.ts" + ], + "project": [ + "web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}" + ] }, - "trustedDependencies": ["@parcel/watcher", "core-js", "lightningcss-cli", "msgpackr-extract"] + "trustedDependencies": [ + "@parcel/watcher", + "core-js", + "lightningcss-cli", + "msgpackr-extract" + ] } diff --git a/tests/playwright/capture.spec.ts b/tests/playwright/capture.spec.ts index 7b54806e..2a6b0fa2 100644 --- a/tests/playwright/capture.spec.ts +++ b/tests/playwright/capture.spec.ts @@ -37,13 +37,15 @@ Array.from([ }, ]).forEach(({ pageName, url }) => { test.skip(pageName, async ({ page }, testInfo) => { - await page["goto"](url); + await page.goto(url); const projectName = testInfo.project.name; const timestamp: string = genTimestamp(); - const viewport = page.viewportSize(); + const viewportSize = page.viewportSize() ?? { height: 0, width: 0 }; - const captureName = `${pageName}/${projectName}-${viewport?.width}-${viewport?.height} ${timestamp}.png`; + const captureName = `${pageName}/${projectName}-${String(viewportSize.width)}-${ + String(viewportSize.height) + } ${timestamp}.png`; await takeFullPageScreenshot(page, captureName); await expect(page).toHaveURL(url); diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-panier.ts b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-panier.ts index d5256518..5689e189 100755 --- a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-panier.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-panier.ts @@ -8,7 +8,6 @@ import type { MessageMajContenuPanierSchema } from "./lib/schemas/messages.ts"; import type { WCStoreCartItem } from "./lib/types/api/cart"; import type { MessageMajBoutonPanierDonnees, MessageMajContenuPanierDonnees } from "./lib/types/messages"; -import { Effect } from "effect"; import { Effect } from "effect"; import { ATTRIBUT_CLE_PANIER, diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit-service.ts b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit-service.ts index 22b563d0..72469a32 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit-service.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit-service.ts @@ -1,10 +1,23 @@ -import { Array as FxArray, Console, Context, Effect, HashMap, Layer, ManagedRuntime, Option, pipe } from "effect"; +// oxlint-disable typescript/dot-notation +import { + Array as FxArray, + Console, + Context, + Effect, + HashMap, + Layer, + ManagedRuntime, + Option, + pipe, + Stream, +} from "effect"; import type { NonEmptyReadonlyArray } from "effect/Array"; import type { NoSuchElementError } from "effect/Cause"; import { getAllSelectorFromDocument, getFirstSelectorFromDocument } from "../scripts-effect/lib/dom.ts"; import { ATTRIBUT_ARIA_CONTROLS, ATTRIBUT_ARIA_EXPANDED, + ATTRIBUT_DESACTIVE, ATTRIBUT_HIDDEN, DOM_BOUTON_AJOUT_PANIER, DOM_BOUTONS_ACCORDEON, @@ -22,7 +35,7 @@ type DetailEnsemble = { class ProductPageElements extends Context.Service< ProductPageElements, { - AddProductButton: HTMLButtonElement; + AddToCartButton: HTMLButtonElement; Details: HashMap.HashMap; DetailsButtons: NonEmptyReadonlyArray; DetailsContents: NonEmptyReadonlyArray; @@ -35,7 +48,7 @@ class ProductPageElements extends Context.Service< static readonly layer = Layer.effect( ProductPageElements, Effect.gen(function*() { - const AddProductButton = yield* getFirstSelectorFromDocument(DOM_BOUTON_AJOUT_PANIER); + const AddToCartButton = yield* getFirstSelectorFromDocument(DOM_BOUTON_AJOUT_PANIER); const DetailsButtons = yield* getAllSelectorFromDocument(DOM_BOUTONS_ACCORDEON); const DetailsContents = yield* getAllSelectorFromDocument(DOM_CONTENUS_ACCORDEON); const ProductPrice = yield* getFirstSelectorFromDocument(DOM_PRIX_PRODUIT); @@ -62,7 +75,7 @@ class ProductPageElements extends Context.Service< ); return ProductPageElements.of({ - AddProductButton, + AddToCartButton, Details, DetailsButtons, DetailsContents, @@ -78,10 +91,32 @@ class ProductPageElements extends Context.Service< class ProductPageDOM extends Context.Service< ProductPageDOM, { + initPriceUpdatesOnVariationChange: () => Effect.Effect; + onVariationChangeHandler: () => Effect.Effect; /** * Récupère les Attributs du Produit depuis les Elements au sein du DOM. */ getProductAttributesFromDOM: () => Effect.Effect>; + /** + * Initialise l'état initial du Bouton d'ajout au Panier. + */ + initAddToCartButtonInitialState: () => Effect.Effect; + /** + * Initialise les mises à jour du Bouton d'ajout au Panier en fonction des interactions de l'Utilisateur. + */ + initAddToCartButtonUpdates: () => Effect.Effect; + /** + * Initialise les interactions des Sections de la Description du Produit. + */ + initDetailInteractions: () => Effect.Effect; + /** + * Met à jour l'état des Sections de la Description du Produit. + */ + onDetailButtonClickHandler: (evt: Event) => Effect.Effect; + /** + * Met à jour l'état du Bouton d'ajout au Panier. + */ + onFormChangeHandler: (evt: Event) => Effect.Effect; /** * Replie toutes les sections de la description du Produit. */ @@ -91,7 +126,19 @@ class ProductPageDOM extends Context.Service< static readonly layer = Layer.effect( ProductPageDOM, Effect.gen(function*() { - const { Details, VariationSelectors } = yield* ProductPageElements; + const { AddToCartButton, Details, ProductPrice, DetailsButtons, ProductRawJson, VariationChoiceForm, VariationSelectors } = + yield* ProductPageElements; + + const onFormChangeHandler = Effect.fnUntraced(function*(evt: Event) { + // La cible ne peut qu'être un Formulaire. + const target: HTMLFormElement = evt.target as HTMLFormElement; + const isClickAllowed = target.checkValidity() === false; + + // Active/désactive le Bouton en fonction de la validité du Formulaire du Produit. + AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, isClickAllowed); + + return yield* Effect.void; + }); const toggleAllDetails: () => Effect.Effect = () => Effect.sync((): void => { @@ -105,6 +152,37 @@ class ProductPageDOM extends Context.Service< ); }); + const onDetailButtonClickHandler = Effect.fnUntraced(function*(evt: Event) { + // Empêche la pollution de l'historique de navigation + evt.preventDefault(); + + // La cible est connue. + const target = evt.target as HTMLButtonElement; + + // Récupère le contenu correspondant au Bouton. + const linkedSection = yield* pipe( + Option.fromNullishOr(target.getAttribute(ATTRIBUT_ARIA_CONTROLS)), + Option.flatMap((contentId: string) => HashMap.get(Details, contentId)), + ); + + // Sauvegarde l'état d'ouverture de la Section avant de toutes les fermer. + const wasCurrentSection: boolean = target.getAttribute(ATTRIBUT_ARIA_EXPANDED) === "true"; + + // Replie toutes les Sections. + yield* toggleAllDetails(); + + // Ne fais rien de plus si l'onglet sélectionné était le courant + if (wasCurrentSection === true) { + return yield* Effect.void; + } + + // Ouvre le nouvel onglet sélectionné + target.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, true); + linkedSection.content.toggleAttribute(ATTRIBUT_HIDDEN, false); + + return yield* Effect.void; + }); + const getProductAttributesFromDOM: () => Effect.Effect> = () => Effect.sync(() => FxArray.map(VariationSelectors, (select: HTMLSelectElement) => ({ @@ -113,8 +191,79 @@ class ProductPageDOM extends Context.Service< })) ); + const initAddToCartButtonInitialState = Effect.fn("initAddToCartButtonInitialState")(function*() { + /** Est-ce que le Produit affiché est en stock ? */ + const isProductInStock = AddToCartButton.hasAttribute("data-in-stock") === true; + + // S'il n'y a pas de stock, ne rien faire. + if (isProductInStock === false) { + return yield* Effect.void; + } + + // S'il n'y a pas de Sélecteurs de variations, activer le Bouton d'ajout au Panier. + if (FxArray.isReadonlyArrayEmpty(VariationSelectors)) { + AddToCartButton.removeAttribute(ATTRIBUT_DESACTIVE); + } + + return yield* Effect.void; + }); + + const initAddToCartButtonUpdates = Effect.fn("initAddToCartInteractionUpdates")(function*() { + return yield* pipe( + Stream.fromEventListener(VariationChoiceForm, "change"), + Stream.tap(onFormChangeHandler), + Stream.runDrain, + ); + }); + + const initPriceUpdatesOnVariationChange = Effect.fn("initPriceUpdatesOnVariationChange")(function*(){ + return yield* pipe( + Stream.fromEventListener(VariationChoiceForm, "change"), + Stream.tap(onVariationChangeHandler), + Stream.runDrain, + ) + }); + + const onVariationChangeHandler = Effect.fn("onVariationChangeHandler")(function*(){ + if (VariationChoiceForm.checkValidity() === false) { + return yield* Effect.void; + } + + const variations = JSON.parse(ProductRawJson.textContent)?.variations as ReadonlyArray; + const chosenAttributes = yield* getProductAttributesFromDOM(); + + const equivalence = FxArray.makeEquivalence<{attribute: string,value: string}>((a,b) => { + return a.attribute === b.attribute && a.value === b.value; + }); + const chosenVariation = yield* FxArray.findFirst(variations, variation => equivalence(variation.attributes, chosenAttributes)); + const newPrice = chosenVariation.price; + + ProductPrice.textContent = `${newPrice}€`; + return yield* Effect.void; + }); + + const initDetailInteractions = Effect.fn("initDetailInteractions")(function*() { + return yield* pipe( + // Créé un Stream par Bouton de Section. + FxArray.map( + DetailsButtons, + (button: HTMLButtonElement) => + pipe(Stream.fromEventListener(button, "click"), Stream.tap(onDetailButtonClickHandler)), + ), + Stream.mergeAll({ concurrency: "unbounded" }), + Stream.runDrain, + ); + }); + return ProductPageDOM.of({ getProductAttributesFromDOM, + initAddToCartButtonInitialState, + initAddToCartButtonUpdates, + initDetailInteractions, + initPriceUpdatesOnVariationChange, + onDetailButtonClickHandler, + onFormChangeHandler, + onVariationChangeHandler, toggleAllDetails, }); }), diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit.ts b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit.ts index 4a284ab1..74a5dc35 100755 --- a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-produit.ts @@ -1,40 +1,10 @@ // Scripts pour la Page Produit import { pipe } from "@mobily/ts-belt"; -import { get as dictGet } from "@mobily/ts-belt/Dict"; -import { tap as optionTap } from "@mobily/ts-belt/Option"; -import { Array as FxArray, Console, Effect, HashMap, Option, pipe as epipe, Stream } from "effect"; -import { EitherAsync } from "purify-ts"; -import { match, P } from "ts-pattern"; -import { ValiError } from "valibot"; -import type { AnySchema } from "valibot"; +import { Console, Effect, pipe as epipe } from "effect"; -import type { WCStoreCartAddItemArgs, WCStoreCartAddItemArgsItems } from "./lib/types/api/cart-add-item.ts"; -import type { WCStoreCart } from "./lib/types/api/cart.ts"; -import type { FetchErrors } from "./lib/types/reseau.ts"; -import { ROUTE_API_AJOUTE_ARTICLE_PANIER } from "./constantes/api.ts"; -import { - ATTRIBUT_ARIA_CONTROLS, - ATTRIBUT_ARIA_EXPANDED, - ATTRIBUT_CHARGEMENT, - ATTRIBUT_DESACTIVE, - ATTRIBUT_HIDDEN, - DOM_BOUTON_AJOUT_PANIER, - DOM_BOUTONS_ACCORDEON, - DOM_CONTENUS_ACCORDEON, - DOM_DOM_QUANTITE, - DOM_PRIX_PRODUIT, -} from "./constantes/dom.ts"; -import { lanceAnimationCycleLoading } from "./lib/animations.ts"; -import { mustGetEleInDocument, mustGetElesInDocument, recupereElementDocumentEither } from "./lib/dom.ts"; -import { BadRequestError, reporteErreur, ServerError } from "./lib/erreurs.ts"; -import { emetMessageMajBoutonPanier } from "./lib/messages.ts"; -import { newPartialResponse, postBackend, safeFetch } from "./lib/reseau.ts"; -import { WCStoreCartAddItemArgsSchema } from "./lib/schemas/api/cart-add-item.ts"; -import { WCStoreCartSchema } from "./lib/schemas/api/cart.ts"; -import { safeSchemaParse } from "./lib/validation.ts"; -import { ProductPageElements, ProductPageRuntime } from "./scripts-page-produit-service.ts"; +import { ProductPageRuntime } from "./scripts-page-produit-service.ts"; /** États utiles pour les scripts de la page. */ type EtatsPage = { @@ -72,199 +42,93 @@ const updatePriceOnAttributeChange = (): void => { }); }; -const ajouteProduitAuPanier = (event: MouseEvent): void => { - event.preventDefault(); - console.debug("getAttributeValuesFromDom", getAttributesFromDom()); +// const ajouteProduitAuPanier = (event: MouseEvent): void => { +// event.preventDefault(); +// console.debug("getAttributeValuesFromDom", getAttributesFromDom()); - // Construis les arguments de la requête au backend - const argsRequete: WCStoreCartAddItemArgs = { - id: E.DOM_VARIATION.map((selecteur: HTMLSelectElement): number => Number(selecteur.value)) - // Récupère l'ID du Produit de la Page pour les Produits simples - .orDefault(ETATS_PAGE.idProduit), - quantity: 1, - variation: getAttributesFromDom(), - }; +// // Construis les arguments de la requête au backend +// const argsRequete: WCStoreCartAddItemArgs = { +// id: E.DOM_VARIATION.map((selecteur: HTMLSelectElement): number => Number(selecteur.value)) +// // Récupère l'ID du Produit de la Page pour les Produits simples +// .orDefault(ETATS_PAGE.idProduit), +// quantity: 1, +// variation: getAttributesFromDom(), +// }; - // Réalise la Requête et traite sa Réponse - void EitherAsync - // 1. Valide les arguments de la Requête - .liftEither(safeSchemaParse(argsRequete, WCStoreCartAddItemArgsSchema)) - // 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement - .ifRight(() => { - // Désactive le Bouton pour empêcher des requêtes concurrentes - E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_DESACTIVE, ""); - E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_CHARGEMENT, ""); +// // Réalise la Requête et traite sa Réponse +// void EitherAsync +// // 1. Valide les arguments de la Requête +// .liftEither(safeSchemaParse(argsRequete, WCStoreCartAddItemArgsSchema)) +// // 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement +// .ifRight(() => { +// // Désactive le Bouton pour empêcher des requêtes concurrentes +// E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_DESACTIVE, ""); +// E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_CHARGEMENT, ""); - // Lance un cycle d'animation sur le texte de chargement - lanceAnimationCycleLoading(E.BOUTON_AJOUT_PANIER, 500); - }) - // 3. Exécute la requête via fetch sous forme d'EitherAsync - .chain((args: WCStoreCartAddItemArgs) => - safeFetch( - postBackend({ - corps: JSON.stringify(args), - nonce: ETATS_PAGE.nonce, - route: ROUTE_API_AJOUTE_ARTICLE_PANIER, - }), - ) - ) - // 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse - .chain((reponse: Response) => - EitherAsync(async ({ throwE }) => - // Simplifie les données à matcher - match(await newPartialResponse(reponse)) - .with({ status: 500 }, () => throwE(new ServerError("500 Server Error"))) - .with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error"))) - .with({ status: 201 }, r => r.body) - .otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`))) - ) - ) - // 5. Vérifie le Schéma de la Réponse - .chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema))) - // 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats - .ifRight((panier: WCStoreCart) => - pipe( - dictGet(panier, "items_count"), - optionTap((totalArticles: number) => { - E.BOUTON_AJOUT_PANIER.textContent = "Added to cart!"; - emetMessageMajBoutonPanier({ quantiteProduits: totalArticles }); - }), - ) - ) - .ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError) => { - match(erreur) - .with(P.instanceOf(ValiError), e => { - reporteErreur(e); - console.error(e.issues); - // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES; - }) - .with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => { - reporteErreur(e); - console.error(e); - // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES; - }) - .with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => { - reporteErreur(e); - console.error(e); - // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU; - }) - .exhaustive(); +// // Lance un cycle d'animation sur le texte de chargement +// lanceAnimationCycleLoading(E.BOUTON_AJOUT_PANIER, 500); +// }) +// // 3. Exécute la requête via fetch sous forme d'EitherAsync +// .chain((args: WCStoreCartAddItemArgs) => +// safeFetch( +// postBackend({ +// corps: JSON.stringify(args), +// nonce: ETATS_PAGE.nonce, +// route: ROUTE_API_AJOUTE_ARTICLE_PANIER, +// }), +// ) +// ) +// // 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse +// .chain((reponse: Response) => +// EitherAsync(async ({ throwE }) => +// // Simplifie les données à matcher +// match(await newPartialResponse(reponse)) +// .with({ status: 500 }, () => throwE(new ServerError("500 Server Error"))) +// .with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error"))) +// .with({ status: 201 }, r => r.body) +// .otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`))) +// ) +// ) +// // 5. Vérifie le Schéma de la Réponse +// .chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema))) +// // 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats +// .ifRight((panier: WCStoreCart) => +// pipe( +// dictGet(panier, "items_count"), +// optionTap((totalArticles: number) => { +// E.BOUTON_AJOUT_PANIER.textContent = "Added to cart!"; +// emetMessageMajBoutonPanier({ quantiteProduits: totalArticles }); +// }), +// ) +// ) +// .ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError) => { +// match(erreur) +// .with(P.instanceOf(ValiError), e => { +// reporteErreur(e); +// console.error(e.issues); +// // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES; +// }) +// .with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => { +// reporteErreur(e); +// console.error(e); +// // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES; +// }) +// .with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => { +// reporteErreur(e); +// console.error(e); +// // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU; +// }) +// .exhaustive(); - E.BOUTON_AJOUT_PANIER.textContent = "Add to cart"; - }) - .finally((): void => { - // Désactive l'animation de chargement et rend le Bouton de nouveau cliquable - E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_CHARGEMENT); - E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE); - }) - .run(); -}; - -/** - * Initialise l'état initial d'interactivité du Bouton d'ajout de Produit au Panier. - */ -const initAddToCartButton = Effect.fn("initAddToCartButton")(function*() { - const { AddProductButton, VariationSelectors } = yield* ProductPageElements; - /** Est-ce que le Produit affiché est en stock ? */ - const isProductInStock = AddProductButton.hasAttribute("data-in-stock") === true; - - // S'il n'y a pas de stock, ne rien faire. - if (isProductInStock === false) { - console.debug("initAddToCartButton", "Pas de stock."); - return yield* Effect.void; - } - - // S'il n'y a pas de Sélecteurs de variations, activer le Bouton d'ajout au Panier. - if (FxArray.isReadonlyArrayEmpty(VariationSelectors)) { - console.debug("initAddToCartButton", "Produt simple."); - E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE); - } - - return yield* Effect.void; -}); - -const onFormChange = Effect.fnUntraced(function*(evt: Event) { - const { AddProductButton } = yield* ProductPageElements; - // La cible ne peut qu'être un Formulaire. - const target: HTMLFormElement = evt.target as HTMLFormElement; - const isClickAllowed = target.checkValidity() === false; - - // Active/désactive le Bouton en fonction de la validité du Formulaire du Produit. - AddProductButton.toggleAttribute(ATTRIBUT_DESACTIVE, isClickAllowed); - - return yield* Effect.void; -}); - -/** - * Initialise la mise à jour de l'état d'interactivité du Bouton d'ajout de Produit au Panier en fonction des actions de l'Utilisateur. - */ -const initAddToCartInteractionUpdates = Effect.fn("initAddToCartInteractionUpdates")(function*() { - return yield* pipe( - Stream.fromEventListener(E.VARIATION_CHOICE_FORM, "change"), - Stream.tap(onFormChange), - Stream.runDrain, - ); -}); - -const onDetailButtonClick = Effect.fnUntraced(function*(evt: Event) { - const { Details } = yield* ProductPageElements; - // Empêche la pollution de l'historique de navigation - evt.preventDefault(); - - // La cible est connue. - const target = evt.target as HTMLButtonElement; - // Récupère le contenu correspondant. - const linkedSection = yield* pipe( - Option.fromNullishOr(target.getAttribute(ATTRIBUT_ARIA_CONTROLS)), - Option.flatMap((contentId: string) => HashMap.get(Details, contentId)), - ); - // Sauvegarde l'état d'ouverture de la Section avant de toutes les fermer. - const wasCurrentSection: boolean = target.getAttribute(ATTRIBUT_ARIA_EXPANDED) === "true"; - - // Replie toutes les Sections. - yield* toggleAllDetails(); - - // Ne fais rien de plus si l'onglet sélectionné était le courant - if (wasCurrentSection === true) { - return yield* Effect.void; - } - - // Ouvre le nouvel onglet sélectionné - target.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, true); - linkedSection.content.toggleAttribute(ATTRIBUT_HIDDEN, false); - - return yield* Effect.void; -}); - -const initDetailInteractions = Effect.fn("initDetailInteractions")(function*() { - const PageElements = yield* ProductPageElements; - - return yield* pipe( - FxArray.map( - PageElements.DetailsButtons, - (button: HTMLButtonElement) => pipe(Stream.fromEventListener(button, "click"), Stream.tap(onDetailButtonClick)), - ), - Stream.mergeAll({ concurrency: "unbounded" }), - Stream.runDrain, - ); -}); - -const getAttributesFromDom = (): ReadonlyArray => { - const selectElements = epipe( - document.querySelectorAll(".selecteur-produit select"), - Array.from, - ); - if (selectElements.length === 0) { - return []; - } - - const attributes = selectElements.map((select: HTMLSelectElement) => ({ - attribute: select.id, - value: select.value, - })); - - return attributes; -}; +// E.BOUTON_AJOUT_PANIER.textContent = "Add to cart"; +// }) +// .finally((): void => { +// // Désactive l'animation de chargement et rend le Bouton de nouveau cliquable +// E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_CHARGEMENT); +// E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE); +// }) +// .run(); +// }; document.addEventListener("DOMContentLoaded", (): void => { ProductPageRuntime.runFork(pipe(initAddToCartButton(), Effect.tapCause(Console.error)));