From 305fcce1ba22ef2fda8315c81541783432372dbc Mon Sep 17 00:00:00 2001 From: gcch Date: Wed, 29 Apr 2026 10:45:49 +0200 Subject: [PATCH] ref(boutique) wip scripts sous forme Effect --- .../src/scripts-effect/schemas/api.ts | 15 +- .../src/scripts-effect/schemas/product.ts | 3 + .../src/scripts/page-boutique/runtime.ts | 17 +++ .../src/scripts/page-boutique/service-dom.ts | 142 ++++++++++++++++++ .../scripts/page-boutique/service-elements.ts | 26 ++++ .../scripts/page-boutique/service-messages.ts | 29 ++++ .../src/scripts/scripts-page-boutique.ts | 87 +++-------- 7 files changed, 248 insertions(+), 71 deletions(-) create mode 100644 web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/runtime.ts create mode 100644 web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-dom.ts create mode 100644 web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-elements.ts create mode 100644 web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-messages.ts 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 e3480331..83653e17 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,5 +1,5 @@ import { Schema } from "effect"; -import { ProductId, ProductQuantity, ProductVariationAttribute } from "./product.ts"; +import { ProductId, ProductQuantity, ProductStatus, ProductVariationAttribute } from "./product.ts"; class CartProduct extends Schema.Class("CartProduct")({ id: ProductId, @@ -7,4 +7,15 @@ class CartProduct extends Schema.Class("CartProduct")({ variation: Schema.Array(ProductVariationAttribute), }) {} -export { CartProduct }; +class GetProducts extends Schema.Class("GetProducts")({ + /** L'ID de la Catégorie de Produits demandé. */ + category: Schema.Int.pipe(Schema.optional), + /** Le numéro de page demandé. */ + page: Schema.Int, + /** Le nombre de Produits par page demandé. */ + per_page: Schema.Int, + /** Le statut demandé des Produits. */ + status: ProductStatus, +}) {} + +export { CartProduct, GetProducts }; 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 80cb0405..825adeb7 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 @@ -2,6 +2,8 @@ import { Effect, Option, pipe, Schema, SchemaIssue, SchemaTransformation } from "effect"; import type { SchemaError } from "effect/Schema"; +const ProductStatus = Schema.Literals(["any", "draft", "future", "pending", "private", "publish", "trash"]); + /** Représente l'identifiant numérique unique d'un Produit. */ const ProductId = Schema.Int.pipe(Schema.brand("ProductId")).check(Schema.isGreaterThan(0)); @@ -78,6 +80,7 @@ export { ProductId, ProductQuantity, ProductQuantityFromString, + ProductStatus, ProductVariation, ProductVariationAttribute, }; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/runtime.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/runtime.ts new file mode 100644 index 00000000..5ae434cd --- /dev/null +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/runtime.ts @@ -0,0 +1,17 @@ +import { Console, Layer, ManagedRuntime, pipe } from "effect"; + +import { APIClient } from "../../scripts-effect/lib/api.ts"; +import ShopPageDOM from "./service-dom.ts"; +import ShopPageElements from "./service-elements.ts"; +import ShopPageMessages from "./service-messages.ts"; + +const ShopPageRuntime = ManagedRuntime.make( + pipe( + ShopPageDOM.Live, + Layer.provideMerge(ShopPageMessages.Live), + Layer.provideMerge(ShopPageElements.Live), + Layer.provide(APIClient.Live), + Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)), + ), +); +export default ShopPageRuntime; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-dom.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-dom.ts new file mode 100644 index 00000000..d8b228cb --- /dev/null +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-dom.ts @@ -0,0 +1,142 @@ +import { + Console, + Context, + Effect, + Layer, + Option, + pipe, + Ref, + Schema, + SchemaIssue, + Stream, + SubscriptionRef, +} from "effect"; +import { SchemaError } from "effect/Schema"; +import { APIClient } from "../../scripts-effect/lib/api.ts"; +import { setLoadingState } from "../../scripts-effect/lib/elements.ts"; +import { GetProducts } from "../../scripts-effect/schemas/api.ts"; +import { ATTRIBUT_ID_CATEGORIE_PRODUITS } from "../constantes/dom.ts"; +import ShopPageElements from "./service-elements.ts"; +import ShopPageMessages from "./service-messages.ts"; + +const PRODUCTS_PER_PAGE = 18; + +const PageStatesSchema = Schema.Struct({ + authString: Schema.NonEmptyString, + nonce: Schema.NonEmptyString, +}); + +class InvalidPageStateError extends Schema.TaggedErrorClass()("InvalidPageStateError", { + cause: Schema.String, +}) { + static readonly fromSchemaError = (schemaError: SchemaError): InvalidPageStateError => + new InvalidPageStateError({ + cause: SchemaIssue.makeFormatterDefault()(schemaError.issue), + }); +} + +class ShopPageDOM extends Context.Service()("haikuatelier.fr/Shop/ShopPageDOM", { + make: Effect.gen(function*() { + const Elements = yield* ShopPageElements; + const Messages = yield* ShopPageMessages; + const API = yield* APIClient; + + const PageStates = yield* pipe( + Elements.PageStatesRawJson.textContent, + (textContent: string) => + Schema.decodeUnknownEffect(Schema.fromJsonString(PageStatesSchema))(textContent, { errors: "all" }), + Effect.mapError(InvalidPageStateError.fromSchemaError), + ); + + /** ID de la Catégorie des Produits de la Page, si la Page courante est une Archive. */ + const ProductsCategoryId: Ref.Ref> = yield* Ref.make( + Option.fromNullishOr(Number(Elements.ProductsGrid.getAttribute(ATTRIBUT_ID_CATEGORIE_PRODUITS))), + ); + + // TODO: Créer une SubscriptionRef mettant à jour le DOM au changement de valeur. + const PageNumber = yield* Ref.make(1); + + const onMoreProductedWantedHandler = Effect.fn("onMoreProductedWantedHandler")(function*() { + yield* Console.debug("onMoreProductedWantedHandler"); + + const newPageNumber = yield* Ref.getAndUpdate(PageNumber, pageNumber => pageNumber + 1); + const categoryId = pipe(yield* Ref.get(ProductsCategoryId), Option.getOrUndefined); + const requestBody = yield* GetProducts.makeEffect({ + page: newPageNumber, + per_page: PRODUCTS_PER_PAGE, + status: "publish", + ...(categoryId && { category: categoryId }), + }); + + // Désactive les interactions et affiche un texte de chargement le temps de la requête. + yield* setLoadingState(Elements.ShowMoreButton, true); + yield* SubscriptionRef.set(Messages.ShowMoreButtonText, "Getting Products..."); + + const newProducts = yield* API.GetProducts(PageStates.nonce, PageStates.authString, requestBody); + yield* Console.debug("onMoreProductedWantedHandler", newProducts); + + // Rétablis le texte du Bouton et réactive les interactions. + yield* SubscriptionRef.set(Messages.ShowMoreButtonText, "Show more"); + yield* setLoadingState(Elements.ShowMoreButton, false); + + // Cache le bouton s'il y a moins de PRODUCTS_PER_PAGE Produits disponibles (que l'on est à la dernière page) + if (donnees.length < PRODUCTS_PER_PAGE) { + E.BOUTON_PLUS_DE_PRODUITS.toggleAttribute(ATTRIBUT_HIDDEN); + } + + // Créé un DocumentFragment qui recevra tous les nouveaux Produits + const fragment: DocumentFragment = document.createDocumentFragment(); + + // Créé les Éléments
à insérer + for (const produit of donnees.slice(0, PRODUCTS_PER_PAGE)) { + pipe( + html` + + `, + tap(article => fragment.append(article)), + ); + } + + // Ajoute les nouveaux Produits dans le DOM + E.GRILLE_PRODUITS.append(fragment); + E.GRILLE_PRODUITS.setAttribute(ATTRIBUT_PAGE, String(nouveauNumeroPage)); + }); + + const initLoadMoreProductsOnButtonClick = Effect.fn("initLoadMoreProductsOnButtonClick")(function*() { + return yield* pipe( + Stream.fromEventListener(Elements.ShowMoreButton, "click"), + Stream.tap(onMoreProductedWantedHandler), + Stream.runDrain, + ); + }); + + return { + ProductsCategoryId, + initLoadMoreProductsOnButtonClick, + }; + }), +}) { + static readonly Live = Layer.effect(this, this.make); +} +export default ShopPageDOM; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-elements.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-elements.ts new file mode 100644 index 00000000..208f4dbf --- /dev/null +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-elements.ts @@ -0,0 +1,26 @@ +import { Context, Effect, Layer } from "effect"; +import { getFirstSelectorFromDocument } from "../../scripts-effect/lib/dom.ts"; +import { IncoherentDOMError } from "../page-produit/errors.ts"; + +class ShopPageElements extends Context.Service()("haikuatelier.fr/Shop/ShopPageElements", { + make: Effect.gen(function*() { + const PageStatesRawJson = yield* getFirstSelectorFromDocument("#page-states"); + + /** Le Bouton « Show more » pour afficher plus de Produits à la suite de la Grille. */ + const ShowMoreButton = yield* getFirstSelectorFromDocument( + "#page-boutique #bouton-plus-de-produits", + ); + + /** Le conteneur de la Grille des Produits. */ + const ProductsGrid = yield* getFirstSelectorFromDocument("#page-boutique .grille-produits"); + + return { + PageStatesRawJson, + ProductsGrid, + ShowMoreButton, + }; + }).pipe(Effect.mapErrorEager(IncoherentDOMError.fromNoSuchElementError)), +}) { + static readonly Live = Layer.effect(this, this.make); +} +export default ShopPageElements; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-messages.ts b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-messages.ts new file mode 100644 index 00000000..bfcc2a5e --- /dev/null +++ b/web/app/themes/haiku-atelier-2024/src/scripts/page-boutique/service-messages.ts @@ -0,0 +1,29 @@ +import { Context, Effect, Layer, pipe, Stream, SubscriptionRef } from "effect"; + +import ShopPageElements from "./service-elements.ts"; + +class ShopPageMessages extends Context.Service()("haikuatelier.fr/Shop/Messages", { + make: Effect.gen(function*() { + const { ShowMoreButton } = yield* ShopPageElements; + + const ShowMoreButtonText = yield* SubscriptionRef.make("Add to cart"); + // Const ShowMoreErrorText = yield* SubscriptionRef.make>(Option.none()); + + const initShowMoreButtonUpdates = Effect.fn("initShowMoreButtonUpdates")(function*() { + return yield* pipe( + SubscriptionRef.changes(ShowMoreButtonText), + Stream.tap(newText => { + ShowMoreButton.textContent = newText; + return Effect.succeed(newText); + }), + Stream.runDrain, + ); + }); + + return { ShowMoreButtonText, initShowMoreButtonUpdates }; + }), +}) { + static readonly Live = Layer.effect(this, this.make); +} + +export default ShopPageMessages; diff --git a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-boutique.ts b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-boutique.ts index 5f8b9d50..99c6eea2 100755 --- a/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-boutique.ts +++ b/web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-boutique.ts @@ -2,75 +2,24 @@ * Scripts pour les fonctionnalités de la page Boutique. */ -import { pipe } from "@mobily/ts-belt"; -import { tap } from "@mobily/ts-belt/Function"; -import { EitherAsync } from "purify-ts"; -import { match, P } from "ts-pattern"; -import { ValiError } from "valibot"; - -import type { APIFetchErrors } from "./lib/types/api/erreurs"; -import type { WCV3Products, WCV3ProductsArgs } from "./lib/types/api/v3/products.ts"; -import type { GenericPageState } from "./lib/types/pages"; - -import { ROUTE_API_NOUVELLE_PRODUCTS } from "./constantes/api.ts"; -import { PRODUCT_STATUTES } from "./constantes/api/products.ts"; -import { - ATTRIBUT_CHARGEMENT, - ATTRIBUT_DESACTIVE, - ATTRIBUT_HIDDEN, - ATTRIBUT_ID_CATEGORIE_PRODUITS, - ATTRIBUT_PAGE, - DOM_BOUTON_PLUS_PRODUITS, - DOM_GRILLE_PRODUITS, -} from "./constantes/dom.ts"; -import { lanceAnimationCycleLoading } from "./lib/animations.ts"; -import { html, mustGetEleInDocument } from "./lib/dom.ts"; -import { BadRequestError, reporteErreur, ServerError } from "./lib/erreurs.ts"; -import { getBackendAvecParametresUrl, newPartialResponse } from "./lib/reseau.ts"; -import { WCV3ProductsArgsSchema, WCV3ProductsSchema } from "./lib/schemas/api/v3/products.ts"; -import { safeSchemaParse } from "./lib/validation.ts"; - -type APIProductsErrors = - | APIFetchErrors - | ValiError - | ValiError; - -// @ts-expect-error -- États injectés par le modèle PHP -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP -const ETATS_PAGE: GenericPageState = _etats; - -// Numéros magiques -const PRODUCTS_PER_PAGE = 12; - -// Éléments d'intérêt -const E = { - BOUTON_PLUS_DE_PRODUITS: mustGetEleInDocument(DOM_BOUTON_PLUS_PRODUITS), - GRILLE_PRODUITS: mustGetEleInDocument(DOM_GRILLE_PRODUITS), -}; - -/** - * TODO - */ -const initialisePageBoutique = (): void => { - /** ID de la Catégorie de Produits si la Page courante est l'Archive d'une Catégorie. */ - const idCategorieProduits: null | string = E.GRILLE_PRODUITS.getAttribute(ATTRIBUT_ID_CATEGORIE_PRODUITS); - - E.BOUTON_PLUS_DE_PRODUITS.addEventListener("click", (): void => { - /** Le numéro de page demandée par l'Utilisateur. */ - const nouveauNumeroPage = Number(E.GRILLE_PRODUITS.getAttribute(ATTRIBUT_PAGE)) + 1; - /** Les arguments passés à la requête auprès Backend pour la nouvelle page de Produits. */ - const args: WCV3ProductsArgs = { - page: nouveauNumeroPage, - per_page: PRODUCTS_PER_PAGE, - status: PRODUCT_STATUTES.PUBLISH, - // Ajoute conditionnellement la Catégorie de Produits - ...(idCategorieProduits && { category: idCategorieProduits }), - }; - - undefined; - }); -}; +import { Effect } from "effect"; +import ShopPageRuntime from "./page-boutique/runtime.ts"; +import ShopPageDOM from "./page-boutique/service-dom.ts"; +import ShopPageElements from "./page-boutique/service-elements.ts"; +import ShopPageMessages from "./page-boutique/service-messages.ts"; document.addEventListener("DOMContentLoaded", (): void => { - initialisePageBoutique(); + console.debug("scripts-page-boutique"); + // initialisePageBoutique(); + ShopPageRuntime.runFork(Effect.gen(function*() { + const Elements = yield* ShopPageElements; + const DOM = yield* ShopPageDOM; + const Messages = yield* ShopPageMessages; + + yield* Effect.all([DOM.initLoadMoreProductsOnButtonClick(), Messages.initShowMoreButtonUpdates()], { + concurrency: "unbounded", + }); + + console.debug(Elements.ProductsGrid); + })); });