ref: refactorise les scripts de la page Produit
This commit is contained in:
parent
2013b4e1cc
commit
89d0c80736
9 changed files with 301 additions and 117 deletions
|
|
@ -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>()("APIError", {
|
||||||
|
cause: Schema.Defect,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class WooCommerceAPI extends Context.Service<
|
||||||
|
WooCommerceAPI,
|
||||||
|
{
|
||||||
|
AddProductToCart: (nonce: string, requestBody: CartProduct) => Effect.Effect<Response, APIError>;
|
||||||
|
}
|
||||||
|
>()("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 };
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Schema } from "effect";
|
import { Schema } from "effect";
|
||||||
import { ProductAttribute } from "./product.ts";
|
import { ProductId, ProductQuantity, ProductVariationAttribute } from "./product.ts";
|
||||||
|
|
||||||
class AddProductToCart extends Schema.Class<AddProductToCart>("AddProductToCart")({
|
class CartProduct extends Schema.Class<CartProduct>("CartProduct")({
|
||||||
id: Schema.Int,
|
id: ProductId,
|
||||||
quantity: Schema.Int.check(Schema.isGreaterThan(0)),
|
quantity: ProductQuantity,
|
||||||
variation: Schema.Array(ProductAttribute),
|
variation: Schema.Array(ProductVariationAttribute),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export { AddProductToCart };
|
export { CartProduct };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,49 @@
|
||||||
// oxlint-disable no-magic-numbers -- Pas besoin ici.
|
// 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>("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>("ProductAttribute")({
|
class ProductAttribute extends Schema.Class<ProductAttribute>("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>("ProductVariationAttribute")({
|
||||||
/** L'identifiant _(slug)_ de l'Attribut. */
|
/** L'identifiant _(slug)_ de l'Attribut. */
|
||||||
attribute: Schema.String,
|
attribute: Schema.String,
|
||||||
/** La valeur de l'attribut. */
|
/** La valeur de l'attribut. */
|
||||||
|
|
@ -10,11 +52,32 @@ class ProductAttribute extends Schema.Class<ProductAttribute>("ProductAttribute"
|
||||||
|
|
||||||
class ProductVariation extends Schema.Class<ProductVariation>("ProductVariation")({
|
class ProductVariation extends Schema.Class<ProductVariation>("ProductVariation")({
|
||||||
/** Les Attributs présents pour cette Variation. */
|
/** Les Attributs présents pour cette Variation. */
|
||||||
attributes: Schema.Array(ProductAttribute),
|
attributes: Schema.Array(ProductVariationAttribute),
|
||||||
/** L'identifiant numérique unique de la Variation. */
|
/** L'identifiant numérique unique de la Variation. */
|
||||||
id: Schema.Int.check(Schema.isGreaterThan(0)),
|
id: ProductId,
|
||||||
/** Le prix de la Variation. */
|
/** Le prix de la Variation. */
|
||||||
price: Schema.NonEmptyString,
|
price: ProductQuantityFromString,
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export { ProductAttribute, ProductVariation };
|
class Product extends Schema.Class<Product>("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,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>()("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 };
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
||||||
|
import { WooCommerceAPI } from "../../scripts-effect/api-service.ts";
|
||||||
import ProductPageDOM from "./service-dom.ts";
|
import ProductPageDOM from "./service-dom.ts";
|
||||||
import ProductPageElements from "./service-elements.ts";
|
import ProductPageElements from "./service-elements.ts";
|
||||||
|
|
||||||
|
|
@ -6,6 +7,7 @@ const ProductPageRuntime = ManagedRuntime.make(
|
||||||
pipe(
|
pipe(
|
||||||
ProductPageDOM.layer,
|
ProductPageDOM.layer,
|
||||||
Layer.provide(ProductPageElements.layer),
|
Layer.provide(ProductPageElements.layer),
|
||||||
|
Layer.provide(WooCommerceAPI.layer),
|
||||||
Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)),
|
Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,36 @@ import {
|
||||||
Stream,
|
Stream,
|
||||||
} from "effect";
|
} from "effect";
|
||||||
import type { NoSuchElementError } from "effect/Cause";
|
import type { NoSuchElementError } from "effect/Cause";
|
||||||
import { AddProductToCart } from "../../scripts-effect/schemas/api.ts";
|
import type { SchemaError } from "effect/Schema";
|
||||||
import { ProductAttribute, ProductVariation } from "../../scripts-effect/schemas/product.ts";
|
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 {
|
import {
|
||||||
ATTRIBUT_ARIA_CONTROLS,
|
ATTRIBUT_ARIA_CONTROLS,
|
||||||
ATTRIBUT_ARIA_EXPANDED,
|
ATTRIBUT_ARIA_EXPANDED,
|
||||||
|
ATTRIBUT_CHARGEMENT,
|
||||||
ATTRIBUT_DESACTIVE,
|
ATTRIBUT_DESACTIVE,
|
||||||
ATTRIBUT_HIDDEN,
|
ATTRIBUT_HIDDEN,
|
||||||
} from "../constantes/dom.ts";
|
} from "../constantes/dom.ts";
|
||||||
|
import { lanceAnimationCycleLoading } from "../lib/animations.ts";
|
||||||
|
import { IncoherentDOMError } from "./errors.ts";
|
||||||
import ProductPageElements from "./service-elements.ts";
|
import ProductPageElements from "./service-elements.ts";
|
||||||
import type { DetailEnsemble } from "./types.d.ts";
|
import type { DetailEnsemble } from "./types.d.ts";
|
||||||
|
|
||||||
|
const PageStatesSchema = Schema.Struct({
|
||||||
|
nonce: Schema.NonEmptyString,
|
||||||
|
product: Product,
|
||||||
|
});
|
||||||
|
|
||||||
|
class InvalidPageStateError extends Schema.TaggedErrorClass<InvalidPageStateError>()("InvalidPageStateError", {
|
||||||
|
cause: Schema.String,
|
||||||
|
}) {
|
||||||
|
static readonly fromSchemaError = (schemaError: SchemaError): InvalidPageStateError =>
|
||||||
|
new InvalidPageStateError({
|
||||||
|
cause: SchemaIssue.makeFormatterDefault()(schemaError.issue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class ProductPageDOM extends Context.Service<
|
class ProductPageDOM extends Context.Service<
|
||||||
ProductPageDOM,
|
ProductPageDOM,
|
||||||
{
|
{
|
||||||
|
|
@ -48,7 +67,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
* Replie toutes les sections de la description du Produit.
|
* Replie toutes les sections de la description du Produit.
|
||||||
*/
|
*/
|
||||||
initAddToCartButtonClicks: () => unknown;
|
initAddToCartButtonClicks: () => unknown;
|
||||||
ProductVariations: ReadonlyArray<ProductVariation>;
|
PageStates: typeof PageStatesSchema.Type;
|
||||||
CurrentVariation: Ref.Ref<Option.Option<ProductVariation>>;
|
CurrentVariation: Ref.Ref<Option.Option<ProductVariation>>;
|
||||||
}
|
}
|
||||||
>()("haikuatelier.fr/Produit/ProductPageDOM") {
|
>()("haikuatelier.fr/Produit/ProductPageDOM") {
|
||||||
|
|
@ -60,11 +79,23 @@ class ProductPageDOM extends Context.Service<
|
||||||
Details,
|
Details,
|
||||||
DetailsButtons,
|
DetailsButtons,
|
||||||
ProductPrice,
|
ProductPrice,
|
||||||
ProductRawJson,
|
|
||||||
VariationChoiceForm,
|
VariationChoiceForm,
|
||||||
VariationSelectors,
|
VariationSelectors,
|
||||||
|
PageStatesRawJson,
|
||||||
} = yield* ProductPageElements;
|
} = yield* ProductPageElements;
|
||||||
const onFormChangeHandler = Effect.fn("onFormChangeHandler")(function*(evt: Event): Effect.fn.Return<void> {
|
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<ProductVariation>());
|
||||||
|
|
||||||
|
const onFormChangeHandler = Effect.fn("onFormChangeHandler")(function*(evt: Event) {
|
||||||
// La cible ne peut qu'être un Formulaire.
|
// La cible ne peut qu'être un Formulaire.
|
||||||
const target: HTMLFormElement = evt.target as HTMLFormElement;
|
const target: HTMLFormElement = evt.target as HTMLFormElement;
|
||||||
const isClickAllowed = target.checkValidity() === false;
|
const isClickAllowed = target.checkValidity() === false;
|
||||||
|
|
@ -75,56 +106,57 @@ class ProductPageDOM extends Context.Service<
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleAllDetails: () => Effect.Effect<void> = () =>
|
const toggleAllDetails: (shouldOpen: boolean) => Effect.Effect<void> = (shouldOpen: boolean) =>
|
||||||
Effect.sync((): void => {
|
Effect.sync((): void => {
|
||||||
|
console.debug("toggleAllDetails");
|
||||||
pipe(
|
pipe(
|
||||||
// Récupère les Sections sous forme d'Ensembles.
|
// Récupère les Sections sous forme d'Ensembles.
|
||||||
[...HashMap.values(Details)],
|
[...HashMap.values(Details)],
|
||||||
FxArray.forEach((detail: DetailEnsemble) => {
|
FxArray.forEach((detail: DetailEnsemble) => {
|
||||||
detail.button.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, false);
|
detail.button.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, shouldOpen);
|
||||||
detail.content.toggleAttribute(ATTRIBUT_HIDDEN, true);
|
detail.content.toggleAttribute(ATTRIBUT_HIDDEN, !shouldOpen);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDetailButtonClickHandler = Effect.fn("onDetailButtonClickHandler")(function*(
|
const onDetailButtonClickHandler = Effect.fn("onDetailButtonClickHandler")(
|
||||||
evt: Event,
|
function*(evt: Event) {
|
||||||
): Effect.fn.Return<void, NoSuchElementError> {
|
// Empêche la pollution de l'historique de navigation
|
||||||
// Empêche la pollution de l'historique de navigation
|
evt.preventDefault();
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
// La cible est connue.
|
// La cible est connue.
|
||||||
const target = evt.target as HTMLButtonElement;
|
const target = evt.target as HTMLButtonElement;
|
||||||
|
|
||||||
// Récupère le contenu correspondant au Bouton.
|
// Récupère le contenu correspondant au Bouton.
|
||||||
const linkedSection = yield* pipe(
|
const linkedSection = yield* pipe(
|
||||||
Option.fromNullishOr(target.getAttribute(ATTRIBUT_ARIA_CONTROLS)),
|
Option.fromNullishOr(target.getAttribute(ATTRIBUT_ARIA_CONTROLS)),
|
||||||
Option.flatMap((contentId: string) => HashMap.get(Details, contentId)),
|
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.
|
// Sauvegarde l'état d'ouverture de la Section avant de toutes les fermer.
|
||||||
const wasCurrentSection: boolean = target.getAttribute(ATTRIBUT_ARIA_EXPANDED) === "true";
|
const wasCurrentSection: boolean = target.getAttribute(ATTRIBUT_ARIA_EXPANDED) === "true";
|
||||||
|
|
||||||
// Replie toutes les Sections.
|
// Replie toutes les Sections.
|
||||||
yield* toggleAllDetails();
|
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;
|
return yield* Effect.void;
|
||||||
}
|
},
|
||||||
|
Effect.tapError(Console.error),
|
||||||
// Ouvre le nouvel onglet sélectionné
|
// Ouvre toutes les Sections en cas d'erreur.
|
||||||
target.toggleAttribute(ATTRIBUT_ARIA_EXPANDED, true);
|
Effect.catch(() => toggleAllDetails(true)),
|
||||||
linkedSection.content.toggleAttribute(ATTRIBUT_HIDDEN, false);
|
|
||||||
|
|
||||||
return yield* Effect.void;
|
|
||||||
});
|
|
||||||
|
|
||||||
const ProductVariations: ReadonlyArray<ProductVariation> = 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getChosenProductAttributesFromDOM = Effect.fn("getChosenProductAttributesFromDOM")(function*() {
|
const getChosenProductAttributesFromDOM = Effect.fn("getChosenProductAttributesFromDOM")(function*() {
|
||||||
|
|
@ -133,60 +165,90 @@ class ProductPageDOM extends Context.Service<
|
||||||
attribute: select.id,
|
attribute: select.id,
|
||||||
value: select.value,
|
value: select.value,
|
||||||
})),
|
})),
|
||||||
variations => Schema.decodeEffect(Schema.Array(ProductAttribute))(variations),
|
variations =>
|
||||||
Effect.mapError(error => SchemaIssue.makeFormatterDefault()(error.issue)),
|
Schema.decodeEffect(Schema.Array(ProductVariationAttribute))(variations, {
|
||||||
Effect.tapCause(Console.error),
|
errors: "all",
|
||||||
|
onExcessProperty: "error",
|
||||||
|
}),
|
||||||
|
Effect.mapError(IncoherentDOMError.fromSchemaError),
|
||||||
|
Effect.tapError(Console.error),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const CurrentVariation = yield* Ref.make(Option.none<ProductVariation>());
|
const onVariationChangeHandler = Effect.fn("onVariationChangeHandler")(function*() {
|
||||||
|
yield* Console.log("onVariationChangeHandler");
|
||||||
const onVariationChangeHandler = Effect.fn("onVariationChangeHandler")(function*(): Effect.fn.Return<
|
|
||||||
void,
|
|
||||||
NoSuchElementError | string
|
|
||||||
> {
|
|
||||||
yield* Console.debug("onVariationChangeHandler");
|
|
||||||
// Ne fais rien si le Formulaire n'est pas valide.
|
// Ne fais rien si le Formulaire n'est pas valide.
|
||||||
if (VariationChoiceForm.checkValidity() === false) {
|
if (VariationChoiceForm.checkValidity() === false) {
|
||||||
yield* Console.debug("onVariationChangeHandler", "Le formulaire est invalide.");
|
yield* Console.debug("onVariationChangeHandler", "Le formulaire est invalide.");
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const equivalence = Schema.toEquivalence(Schema.Array(ProductAttribute));
|
const equivalence = Schema.toEquivalence(Schema.Array(ProductVariationAttribute));
|
||||||
const chosenProductAttributes = yield* getChosenProductAttributesFromDOM();
|
const chosenProductAttributes = yield* getChosenProductAttributesFromDOM();
|
||||||
const chosenVariation: ProductVariation = yield* FxArray.findFirst(
|
yield* Console.debug("onVariationChangeHandler", "chosenProductAttributes", chosenProductAttributes);
|
||||||
ProductVariations,
|
const chosenVariation = yield* pipe(
|
||||||
(variation: ProductVariation) => equivalence(variation.attributes, chosenProductAttributes),
|
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.
|
// 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}€`;
|
ProductPrice.textContent = `${newPrice}€`;
|
||||||
|
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
}, Effect.tapCause(Console.error));
|
}, Effect.tapError(Console.error));
|
||||||
|
|
||||||
const onAddToCartButtonHandler = Effect.fn("onAddToCartButtonHandler")(function*() {
|
/**
|
||||||
const chosenVariation = yield* Ref.getUnsafe(CurrentVariation);
|
* Déclenche une ajout du Produit demandé au Panier auprès du Backend.
|
||||||
const productDetails = yield* Schema.decodeEffect(AddProductToCart)(
|
*/
|
||||||
{
|
const addToCartButtonClickHandler = Effect.fn("addToCartButtonClickHandler")(function*() {
|
||||||
id: chosenVariation.id,
|
yield* Console.log("addToCartButtonClickHandler");
|
||||||
quantity: 1,
|
|
||||||
variation: chosenVariation.attributes,
|
// Créé le corps de la requête
|
||||||
},
|
const requestBody: CartProduct = yield* pipe(
|
||||||
{ errors: "all" },
|
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*() {
|
const initAddToCartButtonInitialState = Effect.fn("initAddToCartButtonInitialState")(function*() {
|
||||||
/** Est-ce que le Produit affiché est en stock ? */
|
/** Est-ce que le Produit affiché est en stock ? */
|
||||||
const isProductInStock = AddToCartButton.hasAttribute("data-in-stock") === true;
|
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) {
|
if (isProductInStock === false) {
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
}
|
}
|
||||||
|
|
@ -199,32 +261,32 @@ class ProductPageDOM extends Context.Service<
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
});
|
});
|
||||||
|
|
||||||
const initAddToCartButtonUpdates = Effect.fn("initAddToCartInteractionUpdates")(function*() {
|
const initAddToCartButtonUpdates = Effect.fn("initAddToCartInteractionUpdates")(
|
||||||
return yield* pipe(
|
function*(): Effect.fn.Return<void> {
|
||||||
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<void, NoSuchElementError | string> {
|
|
||||||
return yield* pipe(
|
return yield* pipe(
|
||||||
Stream.fromEventListener(VariationChoiceForm, "change"),
|
Stream.fromEventListener(VariationChoiceForm, "change"),
|
||||||
Stream.tap(onVariationChangeHandler),
|
Stream.tap(onFormChangeHandler),
|
||||||
Stream.runDrain,
|
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*() {
|
const initDetailInteractions = Effect.fn("initDetailInteractions")(function*() {
|
||||||
return yield* pipe(
|
return yield* pipe(
|
||||||
// Créé un Stream par Bouton de Section.
|
// Créé un Stream par Bouton de Section.
|
||||||
|
|
@ -239,13 +301,13 @@ class ProductPageDOM extends Context.Service<
|
||||||
});
|
});
|
||||||
|
|
||||||
return ProductPageDOM.of({
|
return ProductPageDOM.of({
|
||||||
CurrentVariation,
|
CurrentVariation: CurrentProduct,
|
||||||
ProductVariations,
|
|
||||||
initAddToCartButtonClicks,
|
initAddToCartButtonClicks,
|
||||||
initAddToCartButtonInitialState,
|
initAddToCartButtonInitialState,
|
||||||
initAddToCartButtonUpdates,
|
initAddToCartButtonUpdates,
|
||||||
initDetailInteractions,
|
initDetailInteractions,
|
||||||
initPriceUpdatesOnVariationChange,
|
initPriceUpdatesOnVariationChange,
|
||||||
|
PageStates,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
DOM_CONTENUS_ACCORDEON,
|
DOM_CONTENUS_ACCORDEON,
|
||||||
DOM_PRIX_PRODUIT,
|
DOM_PRIX_PRODUIT,
|
||||||
} from "../constantes/dom.ts";
|
} from "../constantes/dom.ts";
|
||||||
|
import { IncoherentDOMError } from "./errors.ts";
|
||||||
import type { DetailEnsemble } from "./types.d.ts";
|
import type { DetailEnsemble } from "./types.d.ts";
|
||||||
|
|
||||||
class ProductPageElements extends Context.Service<
|
class ProductPageElements extends Context.Service<
|
||||||
|
|
@ -18,8 +19,8 @@ class ProductPageElements extends Context.Service<
|
||||||
Details: HashMap.HashMap<string, DetailEnsemble>;
|
Details: HashMap.HashMap<string, DetailEnsemble>;
|
||||||
DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>;
|
DetailsButtons: NonEmptyReadonlyArray<HTMLButtonElement>;
|
||||||
DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>;
|
DetailsContents: NonEmptyReadonlyArray<HTMLDivElement>;
|
||||||
|
PageStatesRawJson: HTMLScriptElement;
|
||||||
ProductPrice: HTMLParagraphElement;
|
ProductPrice: HTMLParagraphElement;
|
||||||
ProductRawJson: HTMLScriptElement;
|
|
||||||
VariationChoiceForm: HTMLFormElement;
|
VariationChoiceForm: HTMLFormElement;
|
||||||
VariationSelectors: ReadonlyArray<HTMLSelectElement>;
|
VariationSelectors: ReadonlyArray<HTMLSelectElement>;
|
||||||
}
|
}
|
||||||
|
|
@ -30,8 +31,8 @@ class ProductPageElements extends Context.Service<
|
||||||
const AddToCartButton = 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 PageStatesRawJson = yield* getFirstSelectorFromDocument<HTMLScriptElement>("#page-states");
|
||||||
const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT);
|
const ProductPrice = yield* getFirstSelectorFromDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT);
|
||||||
const ProductRawJson = yield* getFirstSelectorFromDocument<HTMLScriptElement>("#product-json");
|
|
||||||
const VariationChoiceForm = yield* getFirstSelectorFromDocument<HTMLFormElement>("#variation-choice");
|
const VariationChoiceForm = yield* getFirstSelectorFromDocument<HTMLFormElement>("#variation-choice");
|
||||||
const VariationSelectors = yield* pipe(
|
const VariationSelectors = yield* pipe(
|
||||||
getAllSelectorFromDocument<HTMLSelectElement>(".selecteur-produit select"),
|
getAllSelectorFromDocument<HTMLSelectElement>(".selecteur-produit select"),
|
||||||
|
|
@ -58,12 +59,12 @@ class ProductPageElements extends Context.Service<
|
||||||
Details,
|
Details,
|
||||||
DetailsButtons,
|
DetailsButtons,
|
||||||
DetailsContents,
|
DetailsContents,
|
||||||
|
PageStatesRawJson,
|
||||||
ProductPrice,
|
ProductPrice,
|
||||||
ProductRawJson,
|
|
||||||
VariationChoiceForm,
|
VariationChoiceForm,
|
||||||
VariationSelectors,
|
VariationSelectors,
|
||||||
});
|
});
|
||||||
}),
|
}).pipe(Effect.mapError(IncoherentDOMError.fromNoSuchElementError)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ type DetailEnsemble = {
|
||||||
button: HTMLButtonElement;
|
button: HTMLButtonElement;
|
||||||
content: HTMLDivElement;
|
content: HTMLDivElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { DetailEnsemble };
|
export { DetailEnsemble };
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,17 @@ import { Console, Effect } from "effect";
|
||||||
import ProductPageRuntime from "./page-produit/runtime.ts";
|
import ProductPageRuntime from "./page-produit/runtime.ts";
|
||||||
import ProductPageDOM from "./page-produit/service-dom.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 => {
|
document.addEventListener("DOMContentLoaded", (): void => {
|
||||||
console.debug("oups");
|
Effect.gen(function*() {
|
||||||
Effect.gen(function* () {
|
|
||||||
const DOM = yield* ProductPageDOM;
|
const DOM = yield* ProductPageDOM;
|
||||||
console.debug("oups");
|
|
||||||
|
|
||||||
const effects = Effect.all(
|
const effects = Effect.all(
|
||||||
[
|
[
|
||||||
DOM.initAddToCartButtonInitialState(),
|
DOM.initAddToCartButtonInitialState(),
|
||||||
DOM.initAddToCartButtonUpdates(),
|
DOM.initAddToCartButtonUpdates(),
|
||||||
|
DOM.initAddToCartButtonClicks(),
|
||||||
DOM.initDetailInteractions(),
|
DOM.initDetailInteractions(),
|
||||||
DOM.initPriceUpdatesOnVariationChange(),
|
DOM.initPriceUpdatesOnVariationChange(),
|
||||||
DOM.initAddToCartButtonClicks(),
|
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
concurrency: "unbounded",
|
concurrency: "unbounded",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue