diff --git a/web/app/themes/haiku-atelier-2024/src/scripts-effect/api-service.ts b/web/app/themes/haiku-atelier-2024/src/scripts-effect/api-service.ts new file mode 100644 index 00000000..f5492385 --- /dev/null +++ b/web/app/themes/haiku-atelier-2024/src/scripts-effect/api-service.ts @@ -0,0 +1,50 @@ +import { Context, Effect, Layer, Schema } from "effect"; +import { ENTETE_WC_NONCE, ROUTE_API_AJOUTE_ARTICLE_PANIER } from "../scripts/constantes/api.ts"; +import { CartProduct } from "./schemas/api.ts"; + +const REQUEST_TIMEOUT = 5_000; + +class APIError extends Schema.TaggedErrorClass()("APIError", { + cause: Schema.Defect, +}) {} + +class WooCommerceAPI extends Context.Service< + WooCommerceAPI, + { + AddProductToCart: (nonce: string, requestBody: CartProduct) => Effect.Effect; + } +>()("haikuatelier.fr/WooCommerceAPI") { + static readonly layer = Layer.effect( + WooCommerceAPI, + // oxlint-disable-next-line require-yield + Effect.gen(function*() { + const AddProductToCart = Effect.fn("AddProductToCart")(function*(nonce: string, product: CartProduct) { + const response = yield* Effect.tryPromise({ + catch: error => new APIError({ cause: error }), + try: async () => + fetch(ROUTE_API_AJOUTE_ARTICLE_PANIER, { + // Convertis en chaîne de caractères (équivalent à JSON.stringify). + body: Schema.encodeSync(Schema.fromJsonString(CartProduct))(product), + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + [ENTETE_WC_NONCE]: nonce, + }, + method: "POST", + mode: "same-origin", + signal: AbortSignal.timeout(REQUEST_TIMEOUT), + }), + }); + + return response; + }); + + return WooCommerceAPI.of({ + AddProductToCart, + }); + }), + ); +} + +export { WooCommerceAPI }; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/api.ts b/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/api.ts index 97c8b1ff..e3480331 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/api.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/api.ts @@ -1,10 +1,10 @@ import { Schema } from "effect"; -import { ProductAttribute } from "./product.ts"; +import { ProductId, ProductQuantity, ProductVariationAttribute } from "./product.ts"; -class AddProductToCart extends Schema.Class("AddProductToCart")({ - id: Schema.Int, - quantity: Schema.Int.check(Schema.isGreaterThan(0)), - variation: Schema.Array(ProductAttribute), +class CartProduct extends Schema.Class("CartProduct")({ + id: ProductId, + quantity: ProductQuantity, + variation: Schema.Array(ProductVariationAttribute), }) {} -export { AddProductToCart }; +export { CartProduct }; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/product.ts b/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/product.ts index 1c8055a4..80cb0405 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/product.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts-effect/schemas/product.ts @@ -1,7 +1,49 @@ // oxlint-disable no-magic-numbers -- Pas besoin ici. -import { Schema } from "effect"; +import { Effect, Option, pipe, Schema, SchemaIssue, SchemaTransformation } from "effect"; +import type { SchemaError } from "effect/Schema"; + +/** Représente l'identifiant numérique unique d'un Produit. */ +const ProductId = Schema.Int.pipe(Schema.brand("ProductId")).check(Schema.isGreaterThan(0)); + +/** Représente l'identifiant numérique unique d'un Attribut. */ +const AttributeId = Schema.Int.pipe(Schema.brand("AttributeId")).check(Schema.isGreaterThan(0)); + +/** Réprésente une quantité (nombre d'unités) de Produit. */ +const ProductQuantity = Schema.Int.pipe(Schema.brand("ProductQuantity")).check(Schema.isGreaterThanOrEqualTo(0)); +/** Schéma transformant une chaîne de caractères en `ProductId` avec validation. */ +const ProductQuantityFromString = Schema.String.pipe( + Schema.decodeTo( + ProductQuantity, + SchemaTransformation.transformOrFail({ + decode: (value: string) => + pipe( + globalThis.Number(value), + (number: number) => ProductQuantity.makeEffect(number), + Effect.mapError((error: SchemaError) => new SchemaIssue.InvalidValue(Option.some(value), { cause: error })), + ), + encode: (value: number) => Effect.succeed(String(value)), + }), + ), +); + +class AttributeValue extends Schema.Class("AttributeValue")({ + /** L'identifiant numérique de la Valeur de l'Attribut. */ + id: AttributeId, + /** Le nom (texte affiché) de la Valeur de l'Attribut. */ + name: Schema.String, + /** L'identifiant alphanumérique _(slug)_ de la Valeur de l'Attribut. */ + slug: Schema.String, +}) {} class ProductAttribute extends Schema.Class("ProductAttribute")({ + /** Le nom de l'Attribut. */ + name: Schema.String, + options: Schema.Array(AttributeValue), + /** L'identifiant _(slug)_ de l'Attribut. */ + slug: Schema.String, +}) {} + +class ProductVariationAttribute extends Schema.Class("ProductVariationAttribute")({ /** L'identifiant _(slug)_ de l'Attribut. */ attribute: Schema.String, /** La valeur de l'attribut. */ @@ -10,11 +52,32 @@ class ProductAttribute extends Schema.Class("ProductAttribute" class ProductVariation extends Schema.Class("ProductVariation")({ /** Les Attributs présents pour cette Variation. */ - attributes: Schema.Array(ProductAttribute), + attributes: Schema.Array(ProductVariationAttribute), /** L'identifiant numérique unique de la Variation. */ - id: Schema.Int.check(Schema.isGreaterThan(0)), + id: ProductId, /** Le prix de la Variation. */ - price: Schema.NonEmptyString, + price: ProductQuantityFromString, }) {} -export { ProductAttribute, ProductVariation }; +class Product extends Schema.Class("Product")({ + /** Les Attributs applicables au Produit (en cas de Produit simple). */ + attributes: Schema.Union([Schema.Record(Schema.String, ProductAttribute), Schema.Array(ProductAttribute)]), + /** L'identifiant numérique unique du Produit. */ + id: ProductId, + /** Le prix du Produit. */ + price: ProductQuantityFromString, + /** Les Variations existantes du Produit (en cas de Produit variable). */ + variations: Schema.Array(ProductVariation), +}) {} + +export { + AttributeId, + AttributeValue, + Product, + ProductAttribute, + ProductId, + ProductQuantity, + ProductQuantityFromString, + ProductVariation, + ProductVariationAttribute, +}; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/errors.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/errors.ts new file mode 100644 index 00000000..ce6e4f35 --- /dev/null +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/errors.ts @@ -0,0 +1,18 @@ +import { Schema, SchemaIssue } from "effect"; +import { NoSuchElementError } from "effect/Cause"; +import { SchemaError } from "effect/Schema"; + +class IncoherentDOMError extends Schema.TaggedErrorClass()("IncoherentDOMError", { + cause: Schema.String, +}) { + static readonly fromSchemaError = (error: SchemaError): IncoherentDOMError => + new IncoherentDOMError({ + cause: SchemaIssue.makeFormatterDefault()(error.issue), + }); + static readonly fromNoSuchElementError = (error: NoSuchElementError): IncoherentDOMError => + new IncoherentDOMError({ + cause: `Impossible de trouver l'Element suivant dans le DOM : ${error.message}.`, + }); +} + +export { IncoherentDOMError }; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/runtime.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/runtime.ts index 72941df3..0564730d 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/runtime.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/runtime.ts @@ -1,4 +1,5 @@ import { Console, Layer, ManagedRuntime, pipe } from "effect"; +import { WooCommerceAPI } from "../../scripts-effect/api-service.ts"; import ProductPageDOM from "./service-dom.ts"; import ProductPageElements from "./service-elements.ts"; @@ -6,6 +7,7 @@ const ProductPageRuntime = ManagedRuntime.make( pipe( ProductPageDOM.layer, Layer.provide(ProductPageElements.layer), + Layer.provide(WooCommerceAPI.layer), Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)), ), ); diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-dom.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-dom.ts index 672b94d0..8e323b67 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-dom.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-dom.ts @@ -14,17 +14,36 @@ import { Stream, } from "effect"; import type { NoSuchElementError } from "effect/Cause"; -import { AddProductToCart } from "../../scripts-effect/schemas/api.ts"; -import { ProductAttribute, ProductVariation } from "../../scripts-effect/schemas/product.ts"; +import type { SchemaError } from "effect/Schema"; +import { WooCommerceAPI } from "../../scripts-effect/api-service.ts"; +import { AddProductToCart, CartProduct } from "../../scripts-effect/schemas/api.ts"; +import { Product, ProductVariation, ProductVariationAttribute } from "../../scripts-effect/schemas/product.ts"; import { ATTRIBUT_ARIA_CONTROLS, ATTRIBUT_ARIA_EXPANDED, + ATTRIBUT_CHARGEMENT, ATTRIBUT_DESACTIVE, ATTRIBUT_HIDDEN, } from "../constantes/dom.ts"; +import { lanceAnimationCycleLoading } from "../lib/animations.ts"; +import { IncoherentDOMError } from "./errors.ts"; import ProductPageElements from "./service-elements.ts"; import type { DetailEnsemble } from "./types.d.ts"; +const PageStatesSchema = Schema.Struct({ + nonce: Schema.NonEmptyString, + product: Product, +}); + +class InvalidPageStateError extends Schema.TaggedErrorClass()("InvalidPageStateError", { + cause: Schema.String, +}) { + static readonly fromSchemaError = (schemaError: SchemaError): InvalidPageStateError => + new InvalidPageStateError({ + cause: SchemaIssue.makeFormatterDefault()(schemaError.issue), + }); +} + class ProductPageDOM extends Context.Service< ProductPageDOM, { @@ -48,7 +67,7 @@ class ProductPageDOM extends Context.Service< * Replie toutes les sections de la description du Produit. */ initAddToCartButtonClicks: () => unknown; - ProductVariations: ReadonlyArray; + PageStates: typeof PageStatesSchema.Type; CurrentVariation: Ref.Ref>; } >()("haikuatelier.fr/Produit/ProductPageDOM") { @@ -60,11 +79,23 @@ class ProductPageDOM extends Context.Service< Details, DetailsButtons, ProductPrice, - ProductRawJson, VariationChoiceForm, VariationSelectors, + PageStatesRawJson, } = yield* ProductPageElements; - const onFormChangeHandler = Effect.fn("onFormChangeHandler")(function*(evt: Event): Effect.fn.Return { + const API = yield* WooCommerceAPI; + + const PageStates = yield* pipe( + PageStatesRawJson.textContent, + (textContent: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(PageStatesSchema))(textContent, { errors: "all" }), + Effect.mapError(InvalidPageStateError.fromSchemaError), + Effect.tapError(Console.error), + ); + + const CurrentProduct = yield* Ref.make(Option.none()); + + const onFormChangeHandler = Effect.fn("onFormChangeHandler")(function*(evt: Event) { // La cible ne peut qu'être un Formulaire. const target: HTMLFormElement = evt.target as HTMLFormElement; const isClickAllowed = target.checkValidity() === false; @@ -75,56 +106,57 @@ class ProductPageDOM extends Context.Service< return yield* Effect.void; }); - const toggleAllDetails: () => Effect.Effect = () => + const toggleAllDetails: (shouldOpen: boolean) => Effect.Effect = (shouldOpen: boolean) => Effect.sync((): void => { + console.debug("toggleAllDetails"); pipe( // Récupère les Sections sous forme d'Ensembles. [...HashMap.values(Details)], FxArray.forEach((detail: DetailEnsemble) => { - detail.button.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, false); - detail.content.toggleAttribute(ATTRIBUT_HIDDEN, true); + detail.button.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, shouldOpen); + detail.content.toggleAttribute(ATTRIBUT_HIDDEN, !shouldOpen); }), ); }); - const onDetailButtonClickHandler = Effect.fn("onDetailButtonClickHandler")(function*( - evt: Event, - ): Effect.fn.Return { - // Empêche la pollution de l'historique de navigation - evt.preventDefault(); + const onDetailButtonClickHandler = Effect.fn("onDetailButtonClickHandler")( + function*(evt: Event) { + // Empêche la pollution de l'historique de navigation + evt.preventDefault(); - // La cible est connue. - const target = evt.target as HTMLButtonElement; + // 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)), - ); + // 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)), + Effect.fromOption, + Effect.mapError( + () => new IncoherentDOMError({ cause: "Un bouton de section ne correspond à aucun contenu." }), + ), + ); - // Sauvegarde l'état d'ouverture de la Section avant de toutes les fermer. - const wasCurrentSection: boolean = target.getAttribute(ATTRIBUT_ARIA_EXPANDED) === "true"; + // 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(); + // Replie toutes les Sections. + yield* toggleAllDetails(false); + + // 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); - // 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 ProductVariations: ReadonlyArray = yield* pipe( - JSON.parse(ProductRawJson.textContent)?.variations, - json => Schema.decodeUnknownEffect(Schema.Array(ProductVariation))(json, { onExcessProperty: "ignore" }), - Effect.mapError(error => SchemaIssue.makeFormatterStandardSchemaV1()(error.issue)), - Effect.tapCause(Console.error), + }, + Effect.tapError(Console.error), + // Ouvre toutes les Sections en cas d'erreur. + Effect.catch(() => toggleAllDetails(true)), ); const getChosenProductAttributesFromDOM = Effect.fn("getChosenProductAttributesFromDOM")(function*() { @@ -133,60 +165,90 @@ class ProductPageDOM extends Context.Service< attribute: select.id, value: select.value, })), - variations => Schema.decodeEffect(Schema.Array(ProductAttribute))(variations), - Effect.mapError(error => SchemaIssue.makeFormatterDefault()(error.issue)), - Effect.tapCause(Console.error), + variations => + Schema.decodeEffect(Schema.Array(ProductVariationAttribute))(variations, { + errors: "all", + onExcessProperty: "error", + }), + Effect.mapError(IncoherentDOMError.fromSchemaError), + Effect.tapError(Console.error), ); }); - const CurrentVariation = yield* Ref.make(Option.none()); - - const onVariationChangeHandler = Effect.fn("onVariationChangeHandler")(function*(): Effect.fn.Return< - void, - NoSuchElementError | string - > { - yield* Console.debug("onVariationChangeHandler"); + const onVariationChangeHandler = Effect.fn("onVariationChangeHandler")(function*() { + yield* Console.log("onVariationChangeHandler"); // Ne fais rien si le Formulaire n'est pas valide. if (VariationChoiceForm.checkValidity() === false) { yield* Console.debug("onVariationChangeHandler", "Le formulaire est invalide."); return yield* Effect.void; } - const equivalence = Schema.toEquivalence(Schema.Array(ProductAttribute)); + const equivalence = Schema.toEquivalence(Schema.Array(ProductVariationAttribute)); const chosenProductAttributes = yield* getChosenProductAttributesFromDOM(); - const chosenVariation: ProductVariation = yield* FxArray.findFirst( - ProductVariations, - (variation: ProductVariation) => equivalence(variation.attributes, chosenProductAttributes), + yield* Console.debug("onVariationChangeHandler", "chosenProductAttributes", chosenProductAttributes); + const chosenVariation = yield* pipe( + FxArray.findFirst( + PageStates.product.variations, + variation => equivalence(variation.attributes, chosenProductAttributes), + ), + Effect.fromOption, + Effect.mapError(error => new Error("Impossible de trouver la variation demandée.", { cause: error })), ); + yield* Console.debug("onVariationChangeHandler", "chosenVariation", chosenVariation); // Met à jour la valeur de la Variation choisie dans le Service. - yield* Ref.set(Option.some(chosenVariation))(CurrentVariation); + yield* Ref.set(Option.some(chosenVariation))(CurrentProduct); - const newPrice = chosenVariation.price; + const newPrice = String(chosenVariation.price); ProductPrice.textContent = `${newPrice}€`; return yield* Effect.void; - }, Effect.tapCause(Console.error)); + }, Effect.tapError(Console.error)); - const onAddToCartButtonHandler = Effect.fn("onAddToCartButtonHandler")(function*() { - const chosenVariation = yield* Ref.getUnsafe(CurrentVariation); - const productDetails = yield* Schema.decodeEffect(AddProductToCart)( - { - id: chosenVariation.id, - quantity: 1, - variation: chosenVariation.attributes, - }, - { errors: "all" }, + /** + * Déclenche une ajout du Produit demandé au Panier auprès du Backend. + */ + const addToCartButtonClickHandler = Effect.fn("addToCartButtonClickHandler")(function*() { + yield* Console.log("addToCartButtonClickHandler"); + + // Créé le corps de la requête + const requestBody: CartProduct = yield* pipe( + Ref.get(CurrentProduct), + Effect.flatMap(Effect.fromOption), + // Pour un Produit simple, le Ref sera vide. + Effect.orElseSucceed(() => + ProductVariation.make({ + attributes: [], + id: PageStates.product.id, + price: PageStates.product.price, + }) + ), + Effect.map(({ id, attributes }) => + // Les données ont été validées en amont. + Schema.decodeSync(CartProduct)({ id: id, quantity: 1, variation: attributes }) + ), + Effect.tap(body => Console.debug("addToCartButtonClickHandler", "requestBody", body)), ); - console.debug(productDetails); + // Désactive les interactions le temps de la requête. + AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE); + AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT); + lanceAnimationCycleLoading(AddToCartButton, 500); + + const responseBody = yield* pipe( + API.AddProductToCart(PageStates.nonce, requestBody), + Effect.flatMap((response: Response) => Effect.tryPromise(async () => response.json())), + Effect.tap((response: JSONValue) => Console.debug("addToCartButtonClickHandler", "response", response)), + ); + + return responseBody; }); 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. + // S'('y a pas de stock, ne rien faire. if (isProductInStock === false) { return yield* Effect.void; } @@ -199,32 +261,32 @@ class ProductPageDOM extends Context.Service< return yield* Effect.void; }); - const initAddToCartButtonUpdates = Effect.fn("initAddToCartInteractionUpdates")(function*() { - return yield* pipe( - Stream.fromEventListener(VariationChoiceForm, "change"), - Stream.tap(onFormChangeHandler), - Stream.runDrain, - ); - }); - - const initAddToCartButtonClicks = Effect.fn("initAddToCartButtonClicks")(function*() { - return yield* pipe( - Stream.fromEventListener(AddToCartButton, "click"), - Stream.tap(onAddToCartButtonHandler), - Stream.runDrain, - ); - }); - - const initPriceUpdatesOnVariationChange = Effect.fn("initPriceUpdatesOnVariationChange")( - function*(): Effect.fn.Return { + const initAddToCartButtonUpdates = Effect.fn("initAddToCartInteractionUpdates")( + function*(): Effect.fn.Return { return yield* pipe( Stream.fromEventListener(VariationChoiceForm, "change"), - Stream.tap(onVariationChangeHandler), + Stream.tap(onFormChangeHandler), Stream.runDrain, ); }, ); + const initAddToCartButtonClicks = Effect.fn("initAddToCartButtonClicks")(function*() { + return yield* pipe( + Stream.fromEventListener(AddToCartButton, "click"), + Stream.tap(addToCartButtonClickHandler), + Stream.runDrain, + ); + }); + + const initPriceUpdatesOnVariationChange = Effect.fn("initPriceUpdatesOnVariationChange")(function*() { + return yield* pipe( + Stream.fromEventListener(VariationChoiceForm, "change"), + Stream.tap(onVariationChangeHandler), + Stream.runDrain, + ); + }); + const initDetailInteractions = Effect.fn("initDetailInteractions")(function*() { return yield* pipe( // Créé un Stream par Bouton de Section. @@ -239,13 +301,13 @@ class ProductPageDOM extends Context.Service< }); return ProductPageDOM.of({ - CurrentVariation, - ProductVariations, + CurrentVariation: CurrentProduct, initAddToCartButtonClicks, initAddToCartButtonInitialState, initAddToCartButtonUpdates, initDetailInteractions, initPriceUpdatesOnVariationChange, + PageStates, }); }), ); diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-elements.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-elements.ts index bb673c5f..4bacd823 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-elements.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/service-elements.ts @@ -9,6 +9,7 @@ import { DOM_CONTENUS_ACCORDEON, DOM_PRIX_PRODUIT, } from "../constantes/dom.ts"; +import { IncoherentDOMError } from "./errors.ts"; import type { DetailEnsemble } from "./types.d.ts"; class ProductPageElements extends Context.Service< @@ -18,8 +19,8 @@ class ProductPageElements extends Context.Service< Details: HashMap.HashMap; DetailsButtons: NonEmptyReadonlyArray; DetailsContents: NonEmptyReadonlyArray; + PageStatesRawJson: HTMLScriptElement; ProductPrice: HTMLParagraphElement; - ProductRawJson: HTMLScriptElement; VariationChoiceForm: HTMLFormElement; VariationSelectors: ReadonlyArray; } @@ -30,8 +31,8 @@ class ProductPageElements extends Context.Service< const AddToCartButton = yield* getFirstSelectorFromDocument(DOM_BOUTON_AJOUT_PANIER); const DetailsButtons = yield* getAllSelectorFromDocument(DOM_BOUTONS_ACCORDEON); const DetailsContents = yield* getAllSelectorFromDocument(DOM_CONTENUS_ACCORDEON); + const PageStatesRawJson = yield* getFirstSelectorFromDocument("#page-states"); const ProductPrice = yield* getFirstSelectorFromDocument(DOM_PRIX_PRODUIT); - const ProductRawJson = yield* getFirstSelectorFromDocument("#product-json"); const VariationChoiceForm = yield* getFirstSelectorFromDocument("#variation-choice"); const VariationSelectors = yield* pipe( getAllSelectorFromDocument(".selecteur-produit select"), @@ -58,12 +59,12 @@ class ProductPageElements extends Context.Service< Details, DetailsButtons, DetailsContents, + PageStatesRawJson, ProductPrice, - ProductRawJson, VariationChoiceForm, VariationSelectors, }); - }), + }).pipe(Effect.mapError(IncoherentDOMError.fromNoSuchElementError)), ); } diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/types.d.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/types.d.ts index f1940311..e2d10ebd 100644 --- a/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/types.d.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-produit/types.d.ts @@ -3,4 +3,5 @@ type DetailEnsemble = { button: HTMLButtonElement; content: HTMLDivElement; }; + export { DetailEnsemble }; 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 28116d1a..574086ef 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 @@ -4,30 +4,17 @@ import { Console, Effect } from "effect"; import ProductPageRuntime from "./page-produit/runtime.ts"; import ProductPageDOM from "./page-produit/service-dom.ts"; -/** États utiles pour les scripts de la page. */ -type EtatsPage = { - /** L'ID en base de données du Produit. */ - idProduit: number; - /** Un nonce pour l'authentification de requêtes API vers le backend WooCommerce. */ - nonce: string; -}; - -// @ts-expect-error -- États injectés par le modèle PHP -const ETATS_PAGE: EtatsPage = _etats; - document.addEventListener("DOMContentLoaded", (): void => { - console.debug("oups"); - Effect.gen(function* () { + Effect.gen(function*() { const DOM = yield* ProductPageDOM; - console.debug("oups"); const effects = Effect.all( [ DOM.initAddToCartButtonInitialState(), DOM.initAddToCartButtonUpdates(), + DOM.initAddToCartButtonClicks(), DOM.initDetailInteractions(), DOM.initPriceUpdatesOnVariationChange(), - DOM.initAddToCartButtonClicks(), ], { concurrency: "unbounded",