ref: refactorise les scripts de la page Produit
This commit is contained in:
parent
2d7ee36398
commit
4fe1056eab
11 changed files with 494 additions and 118 deletions
|
|
@ -0,0 +1,52 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
/** Délai par défaut pour la réalisation d'une Requête. */
|
||||||
|
const REQUEST_TIMEOUT = 5_000;
|
||||||
|
|
||||||
|
/** Représente un soucis lors de l'exécution d'une Requête auprès de l'API WooCommerce. */
|
||||||
|
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 { APIError, WooCommerceAPI };
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { Console, Context, Effect, Layer, pipe, Schedule, Schema, SchemaIssue } from "effect";
|
||||||
|
import {
|
||||||
|
FetchHttpClient,
|
||||||
|
HttpClient,
|
||||||
|
HttpClientError,
|
||||||
|
HttpClientRequest,
|
||||||
|
HttpClientResponse,
|
||||||
|
} from "effect/unstable/http";
|
||||||
|
import type { CartProduct } from "../schemas/api.ts";
|
||||||
|
|
||||||
|
class BodyParsingError extends Schema.TaggedErrorClass<BodyParsingError>()("BodyParsingError", {
|
||||||
|
cause: Schema.String,
|
||||||
|
}) {}
|
||||||
|
class FetchError extends Schema.TaggedErrorClass<FetchError>()("FetchError", {
|
||||||
|
cause: Schema.Defect,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class ApiRequestError extends Schema.TaggedErrorClass<ApiRequestError>()("ApiRequestError", {
|
||||||
|
reason: Schema.Union([BodyParsingError, FetchError]),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
/** Client `fetch` contenant les options et en-têtes de Requêtes pré-renseignées. */
|
||||||
|
const ApiFetchClient = FetchHttpClient.layer.pipe(
|
||||||
|
Layer.provide(
|
||||||
|
Layer.succeed(
|
||||||
|
FetchHttpClient.RequestInit,
|
||||||
|
{
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
mode: "same-origin",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ApiClient extends Context.Service<ApiClient, {
|
||||||
|
AddProductToCart: (nonce: string, product: CartProduct) => Effect.Effect<unknown, ApiRequestError>;
|
||||||
|
}>()("haikuatelier.fr/ApiClient") {
|
||||||
|
static readonly layer = Layer.effect(
|
||||||
|
ApiClient,
|
||||||
|
Effect.gen(function*() {
|
||||||
|
// Créé un client HTTP où chaque Requête est imprimée dans la console.
|
||||||
|
const httpClient: HttpClient.HttpClient.With<HttpClientError.HttpClientError> = pipe(
|
||||||
|
yield* HttpClient.HttpClient,
|
||||||
|
HttpClient.tapRequest(Console.debug),
|
||||||
|
// Définis une politique d'essai.
|
||||||
|
HttpClient.retryTransient({
|
||||||
|
retryOn: "errors-only",
|
||||||
|
schedule: Schedule.exponential("1 seconds"),
|
||||||
|
times: 3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const AddProductToCart = Effect.fn("AppClient.AddProductToCart")(
|
||||||
|
function*(nonce: string, productToAdd: CartProduct): Effect.fn.Return<unknown, ApiRequestError> {
|
||||||
|
const request = pipe(
|
||||||
|
HttpClientRequest.post(`/wp-json/wc/store/cart/add-item`),
|
||||||
|
HttpClientRequest.setHeader("Nonce", nonce),
|
||||||
|
// Le corps de la Requête a été validée en amont.
|
||||||
|
HttpClientRequest.bodyJsonUnsafe(productToAdd),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = yield* pipe(
|
||||||
|
httpClient.execute(request),
|
||||||
|
// TODO: Remplacer Schema.Unknown par un Schéma de l'objet retourné par le backend.
|
||||||
|
Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Unknown)),
|
||||||
|
Effect.mapError(error => {
|
||||||
|
if (error._tag === "SchemaError") {
|
||||||
|
return new ApiRequestError({
|
||||||
|
reason: new BodyParsingError({ cause: SchemaIssue.makeFormatterDefault()(error.issue) }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new ApiRequestError({ reason: new FetchError({ cause: error.reason }) });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Effect.tap(Console.debug),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiClient.of({ AddProductToCart });
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(ApiFetchClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ApiClient, ApiRequestError };
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { Context, Effect, flow, Layer, pipe, Schedule, Schema } from "effect";
|
||||||
|
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
||||||
|
import { HttpClientError } from "effect/unstable/http/HttpClientError";
|
||||||
|
|
||||||
|
class Todo extends Schema.Class<Todo>("Todo")({
|
||||||
|
userId: Schema.Number,
|
||||||
|
id: Schema.Number,
|
||||||
|
title: Schema.String,
|
||||||
|
completed: Schema.Boolean,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class FetchClientError extends Schema.TaggedErrorClass<FetchClientError>()("FetchClientError", {
|
||||||
|
cause: Schema.Defect,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class FetchClientExample extends Context.Service<FetchClientExample, {
|
||||||
|
readonly allTodos: Effect.Effect<ReadonlyArray<Todo>, FetchClientError>;
|
||||||
|
createTodo(todo: Omit<Todo, "id">): Effect.Effect<Todo, FetchClientError>;
|
||||||
|
getTodo(id: number): Effect.Effect<Todo, FetchClientError>;
|
||||||
|
}>()("FetchClientExample") {
|
||||||
|
static readonly layer = Layer.effect(
|
||||||
|
FetchClientExample,
|
||||||
|
Effect.gen(function*() {
|
||||||
|
// Access the HttpClient service, and apply some common middleware to all requests.
|
||||||
|
const client: HttpClient.HttpClient.With<HttpClientError> = pipe(
|
||||||
|
yield* HttpClient.HttpClient,
|
||||||
|
// Add a base URL to all requests with this client, and set the Accept header to expect JSON Response.
|
||||||
|
HttpClient.mapRequest(flow(
|
||||||
|
HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"),
|
||||||
|
HttpClientRequest.acceptJson,
|
||||||
|
)),
|
||||||
|
// Fail if the Response status is not 2xx.
|
||||||
|
HttpClient.filterStatusOk,
|
||||||
|
// Retry transient errors with an exponential backoff.
|
||||||
|
HttpClient.retryTransient({ schedule: Schedule.exponential(100), times: 3 }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allTodos = client.get("/todos").pipe(
|
||||||
|
Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Array(Todo))),
|
||||||
|
Effect.mapError(cause => new FetchClientError({ cause })),
|
||||||
|
Effect.withSpan("FetchClientExample.allTodos"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the HttpClient to fetch a Todo item by ID, and decode response using the Schema.
|
||||||
|
const getTodo = Effect.fn("FetchClientExample.getTodo")(function*(id: number) {
|
||||||
|
// Annotate the current span with the ID of the Todo being fetched so that it shows up in telemetry.
|
||||||
|
yield* Effect.annotateCurrentSpan({ id });
|
||||||
|
|
||||||
|
const todo = yield* pipe(
|
||||||
|
client.get(`/todos/${id}`, {
|
||||||
|
// You can pass additional options to individual Requests.
|
||||||
|
urlParams: { format: "json" },
|
||||||
|
}),
|
||||||
|
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
|
||||||
|
Effect.mapError(cause => new FetchClientError({ cause })),
|
||||||
|
);
|
||||||
|
|
||||||
|
return todo;
|
||||||
|
});
|
||||||
|
|
||||||
|
// You can use the HttpClientRequest module to build up complex Requests.
|
||||||
|
const createTodo = Effect.fn("createTodo")(function*(todo: Omit<Todo, "id">) {
|
||||||
|
yield* Effect.annotateCurrentSpan({ title: todo.title });
|
||||||
|
|
||||||
|
const createdTodo: Todo = yield* HttpClientRequest.post("/todos").pipe(
|
||||||
|
HttpClientRequest.setUrlParams({ format: "json" }),
|
||||||
|
HttpClientRequest.bodyJsonUnsafe(todo),
|
||||||
|
client.execute,
|
||||||
|
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)),
|
||||||
|
Effect.mapError(cause => new FetchClientError({ cause })),
|
||||||
|
);
|
||||||
|
|
||||||
|
return createdTodo;
|
||||||
|
});
|
||||||
|
|
||||||
|
return FetchClientExample.of({
|
||||||
|
allTodos,
|
||||||
|
getTodo,
|
||||||
|
createTodo,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
// Provides the fetch-based HttpClient implementation.
|
||||||
|
Layer.provide(FetchHttpClient.layer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FetchClientError, FetchClientExample };
|
||||||
|
|
@ -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,11 +1,15 @@
|
||||||
import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
||||||
|
import { WooCommerceAPI } from "../../scripts-effect/api-service.ts";
|
||||||
|
import { ApiClient } from "../../scripts-effect/lib/api.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";
|
||||||
|
|
||||||
const ProductPageRuntime = ManagedRuntime.make(
|
const ProductPageRuntime = ManagedRuntime.make(
|
||||||
pipe(
|
pipe(
|
||||||
ProductPageDOM.layer,
|
ProductPageDOM.layer,
|
||||||
|
Layer.provide(ApiClient.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 { ApiClient, ApiRequestError } from "../../scripts-effect/lib/api.ts";
|
||||||
|
import { 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* ApiClient;
|
||||||
|
|
||||||
|
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,99 @@ 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 recoverFromBackendFailure = Effect.fn("recoverFromBackendFailure")(function*(error: ApiRequestError) {
|
||||||
const chosenVariation = yield* Ref.getUnsafe(CurrentVariation);
|
yield* Console.error(error.reason);
|
||||||
const productDetails = yield* Schema.decodeEffect(AddProductToCart)(
|
AddToCartButton.textContent = "Error while adding the Product to the Cart...";
|
||||||
{
|
return yield* Effect.void;
|
||||||
id: chosenVariation.id,
|
|
||||||
quantity: 1,
|
|
||||||
variation: chosenVariation.attributes,
|
|
||||||
},
|
|
||||||
{ errors: "all" },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.debug(productDetails);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Désactive les interactions le temps de la requête.
|
||||||
|
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, true);
|
||||||
|
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, true);
|
||||||
|
lanceAnimationCycleLoading(AddToCartButton, 500);
|
||||||
|
|
||||||
|
// Exécute la Requête auprès du backend.
|
||||||
|
yield* API.AddProductToCart(PageStates.nonce, requestBody);
|
||||||
|
|
||||||
|
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, false);
|
||||||
|
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, false);
|
||||||
|
AddToCartButton.textContent = "Add to cart";
|
||||||
|
},
|
||||||
|
Effect.tapError(Console.error),
|
||||||
|
Effect.catchTag("ApiRequestError", recoverFromBackendFailure),
|
||||||
|
);
|
||||||
|
|
||||||
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 +270,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 +310,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