2026-04-13

This commit is contained in:
gcch 2026-04-11 10:53:06 +02:00
commit 08ad871e0c
9 changed files with 320 additions and 293 deletions

View file

@ -1,12 +1,14 @@
# Tout ce qui est traité par dprint # Tout ce qui est traité par treefmt
*.css *.css
*.html *.html
*.js *.js
*.json
*.md *.md
*.mjs *.mjs
*.mts *.mts
*.php *.php
*.scss *.scss
*.sh
*.ts *.ts
*.xml *.xml
*.yaml *.yaml

View file

@ -73,12 +73,12 @@
"composer/installers": "^2.3", "composer/installers": "^2.3",
"crell/fp": "^1.0", "crell/fp": "^1.0",
"htmlburger/carbon-fields": "^3.6.9", "htmlburger/carbon-fields": "^3.6.9",
"illuminate/support": "^13.3", "illuminate/support": "^13.4",
"laravel/helpers": "^1.8.3", "laravel/helpers": "^1.8.3",
"log1x/wp-smtp": "^1.0.2", "log1x/wp-smtp": "^1.0.2",
"lstrojny/functional-php": "^1.18", "lstrojny/functional-php": "^1.18",
"mnsami/composer-custom-directory-installer": "^2.0", "mnsami/composer-custom-directory-installer": "^2.0",
"nesbot/carbon": "^3.11.3", "nesbot/carbon": "^3.11.4",
"oscarotero/env": "^2.1.1", "oscarotero/env": "^2.1.1",
"php": ">=8.5", "php": ">=8.5",
"php-standard-library/php-standard-library": "^6.1.1", "php-standard-library/php-standard-library": "^6.1.1",
@ -92,7 +92,7 @@
"vlucas/phpdotenv": "^5.6.3", "vlucas/phpdotenv": "^5.6.3",
"wpackagist-plugin/falcon": "^2.9.3", "wpackagist-plugin/falcon": "^2.9.3",
"wpackagist-plugin/force-regenerate-thumbnails": "^2.3.0", "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/redis-cache": "^2.7.0",
"wpackagist-plugin/wc-multishipping": "^3.0.2", "wpackagist-plugin/wc-multishipping": "^3.0.2",
"wpackagist-plugin/woo-preview-emails": "^2.2.14", "wpackagist-plugin/woo-preview-emails": "^2.2.14",

100
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "bf6e098198b957782555eeb97479b37e", "content-hash": "3144138aa029c01a516e9b6ce664271b",
"packages": [ "packages": [
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
@ -2506,16 +2506,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2565,7 +2565,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {
@ -2585,20 +2585,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-09T11:45:10+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2650,7 +2650,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {
@ -2670,20 +2670,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-12-23T08:48:59+00:00" "time": "2026-04-10T17:25:58+00:00"
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2734,7 +2734,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {
@ -2754,20 +2754,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-02T08:10:11+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/polyfill-php84", "name": "symfony/polyfill-php84",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php84.git", "url": "https://github.com/symfony/polyfill-php84.git",
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191" "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06",
"reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2814,7 +2814,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-php84/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {
@ -2834,20 +2834,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-24T13:30:11+00:00" "time": "2026-04-10T18:47:49+00:00"
}, },
{ {
"name": "symfony/polyfill-php85", "name": "symfony/polyfill-php85",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php85.git", "url": "https://github.com/symfony/polyfill-php85.git",
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" "reference": "2c408a6bb0313e6001a83628dc5506100474254e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", "reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2894,7 +2894,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {
@ -2914,20 +2914,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-23T16:12:55+00:00" "time": "2026-04-10T16:50:15+00:00"
}, },
{ {
"name": "symfony/polyfill-uuid", "name": "symfony/polyfill-uuid",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git", "url": "https://github.com/symfony/polyfill-uuid.git",
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2977,7 +2977,7 @@
"uuid" "uuid"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-uuid/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {
@ -2997,7 +2997,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-09T11:45:10+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
@ -3629,15 +3629,15 @@
}, },
{ {
"name": "wpackagist-plugin/query-monitor", "name": "wpackagist-plugin/query-monitor",
"version": "3.20.4", "version": "4.0.5",
"source": { "source": {
"type": "svn", "type": "svn",
"url": "https://plugins.svn.wordpress.org/query-monitor/", "url": "https://plugins.svn.wordpress.org/query-monitor/",
"reference": "tags/3.20.4" "reference": "tags/4.0.5"
}, },
"dist": { "dist": {
"type": "zip", "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": { "require": {
"composer/installers": "^1.0 || ^2.0" "composer/installers": "^1.0 || ^2.0"
@ -6715,16 +6715,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6773,7 +6773,7 @@
"shim" "shim"
], ],
"support": { "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": [ "funding": [
{ {
@ -6793,11 +6793,11 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-27T09:58:17+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -6858,7 +6858,7 @@
"shim" "shim"
], ],
"support": { "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": [ "funding": [
{ {
@ -6882,7 +6882,7 @@
}, },
{ {
"name": "symfony/polyfill-php81", "name": "symfony/polyfill-php81",
"version": "v1.33.0", "version": "v1.34.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php81.git", "url": "https://github.com/symfony/polyfill-php81.git",
@ -6938,7 +6938,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
}, },
"funding": [ "funding": [
{ {

View file

@ -28,8 +28,10 @@ format:
-vendor/bin/twig-cs-fixer fix web/app/themes/haiku-atelier-2024/ -vendor/bin/twig-cs-fixer fix web/app/themes/haiku-atelier-2024/
# PhpCsFixer # PhpCsFixer
# -vendor/bin/php-cs-fixer fix --allow-risky yes # -vendor/bin/php-cs-fixer fix --allow-risky yes
dprint --config "~/.config/dprint/dprint.jsonc" fmt treefmt \
fish scripts/format-sort-files.fish --config-file ~/.config/treefmt/treefmt.toml \
--tree-root . \
.
# Compile, minifie et optimise Sass vers CSS. # Compile, minifie et optimise Sass vers CSS.
[group('css')] [group('css')]

View file

@ -73,8 +73,17 @@
"ios >0 and last 3 years" "ios >0 and last 3 years"
], ],
"knip": { "knip": {
"entry": ["web/app/themes/haiku-atelier-2024/src/scripts/*.ts"], "entry": [
"project": ["web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}"] "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"
]
} }

View file

@ -37,13 +37,15 @@ Array.from<TestPage>([
}, },
]).forEach(({ pageName, url }) => { ]).forEach(({ pageName, url }) => {
test.skip(pageName, async ({ page }, testInfo) => { test.skip(pageName, async ({ page }, testInfo) => {
await page["goto"](url); await page.goto(url);
const projectName = testInfo.project.name; const projectName = testInfo.project.name;
const timestamp: string = genTimestamp(); 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 takeFullPageScreenshot(page, captureName);
await expect(page).toHaveURL(url); await expect(page).toHaveURL(url);

View file

@ -8,7 +8,6 @@ import type { MessageMajContenuPanierSchema } from "./lib/schemas/messages.ts";
import type { WCStoreCartItem } from "./lib/types/api/cart"; import type { WCStoreCartItem } from "./lib/types/api/cart";
import type { MessageMajBoutonPanierDonnees, MessageMajContenuPanierDonnees } from "./lib/types/messages"; import type { MessageMajBoutonPanierDonnees, MessageMajContenuPanierDonnees } from "./lib/types/messages";
import { Effect } from "effect";
import { Effect } from "effect"; import { Effect } from "effect";
import { import {
ATTRIBUT_CLE_PANIER, ATTRIBUT_CLE_PANIER,

View file

@ -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 { NonEmptyReadonlyArray } from "effect/Array";
import type { NoSuchElementError } from "effect/Cause"; import type { NoSuchElementError } from "effect/Cause";
import { getAllSelectorFromDocument, getFirstSelectorFromDocument } from "../scripts-effect/lib/dom.ts"; import { getAllSelectorFromDocument, getFirstSelectorFromDocument } from "../scripts-effect/lib/dom.ts";
import { import {
ATTRIBUT_ARIA_CONTROLS, ATTRIBUT_ARIA_CONTROLS,
ATTRIBUT_ARIA_EXPANDED, ATTRIBUT_ARIA_EXPANDED,
ATTRIBUT_DESACTIVE,
ATTRIBUT_HIDDEN, ATTRIBUT_HIDDEN,
DOM_BOUTON_AJOUT_PANIER, DOM_BOUTON_AJOUT_PANIER,
DOM_BOUTONS_ACCORDEON, DOM_BOUTONS_ACCORDEON,
@ -22,7 +35,7 @@ type DetailEnsemble = {
class ProductPageElements extends Context.Service< class ProductPageElements extends Context.Service<
ProductPageElements, ProductPageElements,
{ {
AddProductButton: HTMLButtonElement; AddToCartButton: HTMLButtonElement;
Details: HashMap.HashMap<string, DetailEnsemble>; Details: HashMap.HashMap<string, DetailEnsemble>;
DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>; DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>;
DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>; DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>;
@ -35,7 +48,7 @@ class ProductPageElements extends Context.Service<
static readonly layer = Layer.effect( static readonly layer = Layer.effect(
ProductPageElements, ProductPageElements,
Effect.gen(function*() { Effect.gen(function*() {
const AddProductButton = yield* getFirstSelectorFromDocument<HTMLButtonElement>(DOM_BOUTON_AJOUT_PANIER); const AddToCartButton = yield* getFirstSelectorFromDocument<HTMLButtonElement>(DOM_BOUTON_AJOUT_PANIER);
const DetailsButtons = yield* getAllSelectorFromDocument<HTMLButtonElement>(DOM_BOUTONS_ACCORDEON); const DetailsButtons = yield* getAllSelectorFromDocument<HTMLButtonElement>(DOM_BOUTONS_ACCORDEON);
const DetailsContents = yield* getAllSelectorFromDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON); const DetailsContents = yield* getAllSelectorFromDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON);
const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT); const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT);
@ -62,7 +75,7 @@ class ProductPageElements extends Context.Service<
); );
return ProductPageElements.of({ return ProductPageElements.of({
AddProductButton, AddToCartButton,
Details, Details,
DetailsButtons, DetailsButtons,
DetailsContents, DetailsContents,
@ -78,10 +91,32 @@ class ProductPageElements extends Context.Service<
class ProductPageDOM extends Context.Service< class ProductPageDOM extends Context.Service<
ProductPageDOM, ProductPageDOM,
{ {
initPriceUpdatesOnVariationChange: () => Effect.Effect<void>;
onVariationChangeHandler: () => Effect.Effect<void>;
/** /**
* Récupère les Attributs du Produit depuis les Elements au sein du DOM. * Récupère les Attributs du Produit depuis les Elements au sein du DOM.
*/ */
getProductAttributesFromDOM: () => Effect.Effect<ReadonlyArray<WCStoreCartAddItemArgsItems>>; getProductAttributesFromDOM: () => Effect.Effect<ReadonlyArray<WCStoreCartAddItemArgsItems>>;
/**
* Initialise l'état initial du Bouton d'ajout au Panier.
*/
initAddToCartButtonInitialState: () => Effect.Effect<void>;
/**
* Initialise les mises à jour du Bouton d'ajout au Panier en fonction des interactions de l'Utilisateur.
*/
initAddToCartButtonUpdates: () => Effect.Effect<void>;
/**
* Initialise les interactions des Sections de la Description du Produit.
*/
initDetailInteractions: () => Effect.Effect<void, NoSuchElementError>;
/**
* Met à jour l'état des Sections de la Description du Produit.
*/
onDetailButtonClickHandler: (evt: Event) => Effect.Effect<void, NoSuchElementError>;
/**
* Met à jour l'état du Bouton d'ajout au Panier.
*/
onFormChangeHandler: (evt: Event) => Effect.Effect<void>;
/** /**
* Replie toutes les sections de la description du Produit. * Replie toutes les sections de la description du Produit.
*/ */
@ -91,7 +126,19 @@ class ProductPageDOM extends Context.Service<
static readonly layer = Layer.effect( static readonly layer = Layer.effect(
ProductPageDOM, ProductPageDOM,
Effect.gen(function*() { 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<void> = () => const toggleAllDetails: () => Effect.Effect<void> = () =>
Effect.sync((): void => { 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<ReadonlyArray<WCStoreCartAddItemArgsItems>> = () => const getProductAttributesFromDOM: () => Effect.Effect<ReadonlyArray<WCStoreCartAddItemArgsItems>> = () =>
Effect.sync(() => Effect.sync(() =>
FxArray.map(VariationSelectors, (select: HTMLSelectElement) => ({ 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<unknown>;
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({ return ProductPageDOM.of({
getProductAttributesFromDOM, getProductAttributesFromDOM,
initAddToCartButtonInitialState,
initAddToCartButtonUpdates,
initDetailInteractions,
initPriceUpdatesOnVariationChange,
onDetailButtonClickHandler,
onFormChangeHandler,
onVariationChangeHandler,
toggleAllDetails, toggleAllDetails,
}); });
}), }),

View file

@ -1,40 +1,10 @@
// Scripts pour la Page Produit // Scripts pour la Page Produit
import { pipe } from "@mobily/ts-belt"; import { pipe } from "@mobily/ts-belt";
import { get as dictGet } from "@mobily/ts-belt/Dict"; import { Console, Effect, pipe as epipe } from "effect";
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 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 { ProductPageRuntime } from "./scripts-page-produit-service.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";
/** États utiles pour les scripts de la page. */ /** États utiles pour les scripts de la page. */
type EtatsPage = { type EtatsPage = {
@ -72,199 +42,93 @@ const updatePriceOnAttributeChange = (): void => {
}); });
}; };
const ajouteProduitAuPanier = (event: MouseEvent): void => { // const ajouteProduitAuPanier = (event: MouseEvent): void => {
event.preventDefault(); // event.preventDefault();
console.debug("getAttributeValuesFromDom", getAttributesFromDom()); // console.debug("getAttributeValuesFromDom", getAttributesFromDom());
// Construis les arguments de la requête au backend // // Construis les arguments de la requête au backend
const argsRequete: WCStoreCartAddItemArgs = { // const argsRequete: WCStoreCartAddItemArgs = {
id: E.DOM_VARIATION.map((selecteur: HTMLSelectElement): number => Number(selecteur.value)) // 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 // // Récupère l'ID du Produit de la Page pour les Produits simples
.orDefault(ETATS_PAGE.idProduit), // .orDefault(ETATS_PAGE.idProduit),
quantity: 1, // quantity: 1,
variation: getAttributesFromDom(), // variation: getAttributesFromDom(),
}; // };
// Réalise la Requête et traite sa Réponse // // Réalise la Requête et traite sa Réponse
void EitherAsync // void EitherAsync
// 1. Valide les arguments de la Requête // // 1. Valide les arguments de la Requête
.liftEither(safeSchemaParse(argsRequete, WCStoreCartAddItemArgsSchema)) // .liftEither(safeSchemaParse(argsRequete, WCStoreCartAddItemArgsSchema))
// 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement // // 2. Exécute un Effet pour empêcher les requêtes concurrentes et lancer une animation de chargement
.ifRight(() => { // .ifRight(() => {
// Désactive le Bouton pour empêcher des requêtes concurrentes // // Désactive le Bouton pour empêcher des requêtes concurrentes
E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_DESACTIVE, ""); // E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_DESACTIVE, "");
E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_CHARGEMENT, ""); // E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_CHARGEMENT, "");
// Lance un cycle d'animation sur le texte de chargement // // Lance un cycle d'animation sur le texte de chargement
lanceAnimationCycleLoading(E.BOUTON_AJOUT_PANIER, 500); // lanceAnimationCycleLoading(E.BOUTON_AJOUT_PANIER, 500);
}) // })
// 3. Exécute la requête via fetch sous forme d'EitherAsync // // 3. Exécute la requête via fetch sous forme d'EitherAsync
.chain((args: WCStoreCartAddItemArgs) => // .chain((args: WCStoreCartAddItemArgs) =>
safeFetch( // safeFetch(
postBackend({ // postBackend({
corps: JSON.stringify(args), // corps: JSON.stringify(args),
nonce: ETATS_PAGE.nonce, // nonce: ETATS_PAGE.nonce,
route: ROUTE_API_AJOUTE_ARTICLE_PANIER, // route: ROUTE_API_AJOUTE_ARTICLE_PANIER,
}), // }),
) // )
) // )
// 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse // // 4. Traite les cas d'Erreurs et récupère le Corps de la Réponse
.chain((reponse: Response) => // .chain((reponse: Response) =>
EitherAsync<BadRequestError | ServerError, unknown>(async ({ throwE }) => // EitherAsync<BadRequestError | ServerError, unknown>(async ({ throwE }) =>
// Simplifie les données à matcher // // Simplifie les données à matcher
match(await newPartialResponse(reponse)) // match(await newPartialResponse(reponse))
.with({ status: 500 }, () => throwE(new ServerError("500 Server Error"))) // .with({ status: 500 }, () => throwE(new ServerError("500 Server Error")))
.with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error"))) // .with({ status: 400 }, () => throwE(new BadRequestError("400 Bad Request Error")))
.with({ status: 201 }, r => r.body) // .with({ status: 201 }, r => r.body)
.otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`))) // .otherwise(erreur => throwE(new Error(`Erreur inconnue ${String(erreur.status)}`)))
) // )
) // )
// 5. Vérifie le Schéma de la Réponse // // 5. Vérifie le Schéma de la Réponse
.chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema))) // .chain((corpsReponse: unknown) => EitherAsync.liftEither(safeSchemaParse(corpsReponse, WCStoreCartSchema)))
// 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats // // 6. Exécute un Effet pour la mise à jour du DOM avec les Résultats
.ifRight((panier: WCStoreCart) => // .ifRight((panier: WCStoreCart) =>
pipe( // pipe(
dictGet(panier, "items_count"), // dictGet(panier, "items_count"),
optionTap((totalArticles: number) => { // optionTap((totalArticles: number) => {
E.BOUTON_AJOUT_PANIER.textContent = "Added to cart!"; // E.BOUTON_AJOUT_PANIER.textContent = "Added to cart!";
emetMessageMajBoutonPanier({ quantiteProduits: totalArticles }); // emetMessageMajBoutonPanier({ quantiteProduits: totalArticles });
}), // }),
) // )
) // )
.ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError<AnySchema>) => { // .ifLeft((erreur: BadRequestError | FetchErrors | ServerError | ValiError<AnySchema>) => {
match(erreur) // match(erreur)
.with(P.instanceOf(ValiError), e => { // .with(P.instanceOf(ValiError), e => {
reporteErreur(e); // reporteErreur(e);
console.error(e.issues); // console.error(e.issues);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES; // // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
}) // })
.with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => { // .with(P.instanceOf(ServerError), P.instanceOf(BadRequestError), e => {
reporteErreur(e); // reporteErreur(e);
console.error(e); // console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES; // // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_SOUMISSION_ADRESSES;
}) // })
.with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => { // .with(P.instanceOf(DOMException), P.instanceOf(TypeError), P.instanceOf(Error), e => {
reporteErreur(e); // reporteErreur(e);
console.error(e); // console.error(e);
// E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU; // // E.MESSAGE_ADRESSES.textContent = ERREUR_GENERIQUE_RESEAU;
}) // })
.exhaustive(); // .exhaustive();
E.BOUTON_AJOUT_PANIER.textContent = "Add to cart"; // E.BOUTON_AJOUT_PANIER.textContent = "Add to cart";
}) // })
.finally((): void => { // .finally((): void => {
// Désactive l'animation de chargement et rend le Bouton de nouveau cliquable // // 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_CHARGEMENT);
E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE); // E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE);
}) // })
.run(); // .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<WCStoreCartAddItemArgsItems> => {
const selectElements = epipe(
document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"),
Array.from<HTMLSelectElement>,
);
if (selectElements.length === 0) {
return [];
}
const attributes = selectElements.map((select: HTMLSelectElement) => ({
attribute: select.id,
value: select.value,
}));
return attributes;
};
document.addEventListener("DOMContentLoaded", (): void => { document.addEventListener("DOMContentLoaded", (): void => {
ProductPageRuntime.runFork(pipe(initAddToCartButton(), Effect.tapCause(Console.error))); ProductPageRuntime.runFork(pipe(initAddToCartButton(), Effect.tapCause(Console.error)));