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
*.html
*.js
*.json
*.md
*.mjs
*.mts
*.php
*.scss
*.sh
*.ts
*.xml
*.yaml

View file

@ -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",

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",
"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": [
{

View file

@ -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')]

View file

@ -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"
]
}

View file

@ -37,13 +37,15 @@ Array.from<TestPage>([
},
]).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);

View file

@ -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,

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 { 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<string, DetailEnsemble>;
DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>;
DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>;
@ -35,7 +48,7 @@ class ProductPageElements extends Context.Service<
static readonly layer = Layer.effect(
ProductPageElements,
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 DetailsContents = yield* getAllSelectorFromDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON);
const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(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<void>;
onVariationChangeHandler: () => Effect.Effect<void>;
/**
* Récupère les Attributs du Produit depuis les Elements au sein du DOM.
*/
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.
*/
@ -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<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>> = () =>
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<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({
getProductAttributesFromDOM,
initAddToCartButtonInitialState,
initAddToCartButtonUpdates,
initDetailInteractions,
initPriceUpdatesOnVariationChange,
onDetailButtonClickHandler,
onFormChangeHandler,
onVariationChangeHandler,
toggleAllDetails,
});
}),

View file

@ -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<BadRequestError | ServerError, unknown>(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<AnySchema>) => {
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<BadRequestError | ServerError, unknown>(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<AnySchema>) => {
// 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<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;
};
// 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)));