sauvegarde le travail accompli

This commit is contained in:
gcch 2025-04-22 07:28:32 +02:00
commit 26165682d9
521 changed files with 4919 additions and 17279 deletions

View file

@ -123,7 +123,9 @@ $email = WC()->customer->get_billing_email();
$adresse_livraison = WC()->customer->get_shipping();
$adresse_facturation = WC()->customer->get_billing();
$adresse_renseignee = $adresse_livraison["city"] != "";
$pays_livraison = collect(WC()->countries->get_countries())->only($pays_acceptes)->toArray();
$pays_livraison = collect(WC()->countries->get_countries())
->only($pays_acceptes)
->toArray();
$total_livraison = Number::format(floatval(WC()->cart->get_totals()["shipping_total"]), precision: 0);
$methodes_livraison = collect(WC()->session->get("shipping_for_package_0")["rates"])
->values()
@ -165,9 +167,9 @@ function charge_scripts_styles_page_panier(): void {
);
wp_enqueue_script_module(
id: "haiku-atelier-2024-scripts-page-panier",
src: get_template_directory_uri() . "/assets/js/scripts-page-panier.js",
src: get_template_directory_uri() . "/assets/js/scripts-page-cart.js",
deps: [],
version: filemtime(get_template_directory() . "/assets/js/scripts-page-panier.js"),
version: filemtime(get_template_directory() . "/assets/js/scripts-page-cart.js"),
);
}
add_action("wp_enqueue_scripts", "charge_scripts_styles_page_panier");

View file

@ -0,0 +1,4 @@
export const CATALOG_VISIBILITIES = {
INVISIBLE: "invisible",
VISIBLE: "visible",
} as const;

View file

@ -0,0 +1,118 @@
import { Schema } from "effect";
import { CATALOG_VISIBILITIES } from "./constants";
export class CartItemTotals extends Schema.Class<CartItemTotals>("CartItemTotals")({
currency_code: Schema.String,
currency_decimal_separator: Schema.String,
currency_minor_unit: Schema.Number,
currency_prefix: Schema.String,
currency_suffix: Schema.String,
currency_symbol: Schema.String,
currency_thousand_separator: Schema.String,
line_subtotal: Schema.String,
line_subtotal_tax: Schema.String,
line_total: Schema.String,
line_total_tax: Schema.String,
}) {}
export class CartItem extends Schema.Class<CartItem>("CartItem")({
backorders_allowed: Schema.Boolean,
catalog_visibility: Schema.Enums(CATALOG_VISIBILITIES),
description: Schema.String,
extensions: Schema.Unknown,
id: Schema.Number,
images: Schema.Array(Schema.Unknown),
item_data: Schema.Array(Schema.Unknown),
key: Schema.String,
low_stock_remaining: Schema.Unknown,
name: Schema.String,
permalink: Schema.URL,
prices: Schema.Unknown,
quantity: Schema.Number,
quantity_limits: Schema.Unknown,
short_description: Schema.String,
show_backorder_badge: Schema.Boolean,
sku: Schema.String,
sold_individually: Schema.Boolean,
totals: CartItemTotals,
type: Schema.String,
variation: Schema.Array(Schema.Unknown),
}) {}
export class CartTotals extends Schema.Class<CartTotals>("CartTotals")({
currency_code: Schema.String,
currency_decimal_separator: Schema.String,
currency_minor_unit: Schema.Number,
currency_prefix: Schema.String,
currency_suffix: Schema.String,
currency_symbol: Schema.String,
currency_thousand_separator: Schema.String,
tax_lines: Schema.Array(Schema.Unknown),
total_discount: Schema.NumberFromString,
total_discount_tax: Schema.String,
total_fees: Schema.String,
total_fees_tax: Schema.String,
total_items: Schema.NumberFromString,
total_items_tax: Schema.String,
total_price: Schema.NumberFromString,
total_shipping: Schema.NumberFromString,
total_shipping_tax: Schema.Union(Schema.String, Schema.Null),
total_tax: Schema.String,
}) {}
export class Cart extends Schema.Class<Cart>("Cart")({
billing_address: Schema.Unknown,
/** List of applied basket coupons. */
coupons: Schema.Array(Schema.Unknown),
cross_sells: Schema.Unknown,
errors: Schema.Unknown,
extensions: Schema.Unknown,
fees: Schema.Unknown,
has_calculated_shipping: Schema.Boolean,
items: Schema.Array(CartItem),
items_count: Schema.Int,
items_weight: Schema.Int,
needs_payment: Schema.Boolean,
needs_shipping: Schema.Boolean,
payment_methods: Schema.Unknown,
payment_requirements: Schema.Unknown,
shipping_address: Schema.Unknown,
shipping_rates: Schema.Array(Schema.Unknown),
totals: CartTotals,
}) {}
// Requêtes.
export class CartUpdateItemArgs extends Schema.Class<CartUpdateItemArgs>("CartUpdateItemArgs")({
/** Unique identifier (key) for the basket item to update. */
key: Schema.String,
/** New quantity of the item in the basket. */
quantity: Schema.Number.pipe(Schema.greaterThan(1)),
}) {}
export class CartRemoveItemArgs extends Schema.Class<CartRemoveItemArgs>("CartRemoveItemArgs")({
/** Unique identifier (key) for the basket item. */
key: Schema.String,
}) {}
// export const WCStoreCartSchema = v.object({
// billing_address: WCStoreBillingAddressSchema,
// /** List of applied basket coupons. */
// coupons: v.array(WCStoreCartCouponsSchema),
// cross_sells: v.unknown(),
// errors: v.unknown(),
// extensions: v.unknown(),
// fees: v.unknown(),
// has_calculated_shipping: v.boolean(),
// items: v.array(WCStoreCartItemSchema),
// items_count: v.pipe(v.number(), v.integer()),
// items_weight: v.pipe(v.number(), v.integer()),
// needs_payment: v.boolean(),
// needs_shipping: v.boolean(),
// payment_methods: v.unknown(),
// payment_requirements: v.unknown(),
// shipping_address: WCStoreShippingAddressSchema,
// shipping_rates: v.array(WCStoreShippingRateSchema),
// totals: WCStoreCartTotalsSchema,
// });

View file

@ -0,0 +1,125 @@
import type { NonEmptyReadonlyArray } from "effect/Array";
import { Effect, identity, pipe } from "effect";
import { describe, expect, test } from "vitest";
import { mayGetDOMElement, mayGetDOMElements } from "./dom";
import { SelectorWithoutMatchError } from "./errors";
describe("safeGetDOMElement()", () => {
test("safeGetDOMElement retourne un Élément pour un sélecteur avec résultat", () => {
const parent = document.createElement("div");
const child1 = document.createElement("p");
const child2 = document.createElement("p");
child1.textContent = "Je suis un paragraphe de texte.";
child1.classList.add("classe");
child2.textContent = "Je suis un autre paragraphe de texte.";
parent.append(child1, child2);
const selector = "p.classe";
expect(pipe(
mayGetDOMElement(parent)<HTMLParagraphElement>(selector),
Effect.runSync,
)).toEqual(child1);
});
test("safeGetDOMElement retourne SelectorWithoutMatchError pour un sélecteur sans résultats", () => {
const parent = document.createElement("div");
const child1 = document.createElement("p");
const child2 = document.createElement("p");
child1.textContent = "Je suis un paragraphe de texte.";
child2.textContent = "Je suis un autre paragraphe de texte.";
parent.append(child1, child2);
const selector = "p.classe";
expect(pipe(
mayGetDOMElement(parent)<HTMLParagraphElement>(selector),
Effect.match({
onFailure: err => err instanceof SelectorWithoutMatchError,
onSuccess: identity,
}),
Effect.runSync,
)).toBe(true);
});
test("safeGetDOMElement retourne SelectorWithoutMatchError pour un sélecteur illégal", () => {
const parent = document.createElement("div");
const child1 = document.createElement("p");
const child2 = document.createElement("p");
child1.textContent = "Je suis un paragraphe de texte.";
child2.textContent = "Je suis un autre paragraphe de texte.";
parent.append(child1, child2);
const selector = "p:illegal-pseudo";
expect(pipe(
mayGetDOMElement(parent)<HTMLParagraphElement>(selector),
Effect.match({
onFailure: err => err instanceof SelectorWithoutMatchError,
onSuccess: identity,
}),
Effect.runSync,
)).toBe(true);
});
});
describe("safeGetDOMElements()", () => {
test("safeGetDOMElements() retourne plusieurs Éléments pour un sélecteur avec résultats", () => {
const parent = document.createElement("div");
const child1 = document.createElement("p");
const child2 = document.createElement("p");
const wanted: NonEmptyReadonlyArray<HTMLParagraphElement> = [child1, child2];
child1.textContent = "Je suis un paragraphe de texte.";
child2.textContent = "Je suis un autre paragraphe de texte.";
parent.append(child1, child2);
const selector = "p";
expect(pipe(
mayGetDOMElements(parent)<HTMLParagraphElement>(selector),
Effect.runSync,
)).toEqual(wanted);
});
test("safeGetDOMElement retourne SelectorWithoutMatchError pour un sélecteur sans résultats", () => {
const parent = document.createElement("div");
const child1 = document.createElement("p");
const child2 = document.createElement("p");
child1.textContent = "Je suis un paragraphe de texte.";
child2.textContent = "Je suis un autre paragraphe de texte.";
parent.append(child1, child2);
const selector = "li";
expect(pipe(
mayGetDOMElements(parent)<HTMLParagraphElement>(selector),
Effect.match({
onFailure: err => err instanceof SelectorWithoutMatchError,
onSuccess: identity,
}),
Effect.runSync,
)).toBe(true);
});
test("safeGetDOMElement retourne SelectorWithoutMatchError pour un sélecteur illégal", () => {
const parent = document.createElement("div");
const child1 = document.createElement("p");
const child2 = document.createElement("p");
child1.textContent = "Je suis un paragraphe de texte.";
child2.textContent = "Je suis un autre paragraphe de texte.";
parent.append(child1, child2);
const selector = "p:illegal-pseudo";
expect(pipe(
mayGetDOMElements(parent)<HTMLParagraphElement>(selector),
Effect.match({
onFailure: err => err instanceof SelectorWithoutMatchError,
onSuccess: identity,
}),
Effect.runSync,
)).toBe(true);
});
});

View file

@ -0,0 +1,73 @@
import type { NonEmptyReadonlyArray as NERA } from "effect/Array";
import type { NoSuchElementException } from "effect/Cause";
import { Effect, pipe } from "effect";
import type { ParentElement } from "../../../scripts/lib/types/dom";
import { isNonEmptyReadonlyArray } from "../validation";
import { SelectorWithoutMatchError } from "./errors";
/**
* Récupère un Élément manière sûre au sein d'un Nœud DOM.
*
* @param parent Le Nœud dans lequel récupérer l'Élément.
* @param selector Le sélecteur de l'Élément souhaité.
*
* @returns Un Effect avec soit l'Élément souhaité, soit une SyntaxError pour un sélecteur malformé ou ne débouchant sur aucun résultat.
*/
export const mayGetDOMElement =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Effect.Effect<E, SelectorWithoutMatchError> =>
pipe(
Effect.try(() => parent.querySelector<E>(selector)),
Effect.andThen(Effect.fromNullable),
Effect.mapError(_ => new SelectorWithoutMatchError({ selector })),
);
export const mayGetDOMElements =
(parent: ParentElement) =>
<E extends Element = Element>(selector: string): Effect.Effect<NERA<E>, SelectorWithoutMatchError> =>
pipe(
Effect.try(() => pipe(parent.querySelectorAll<E>(selector), Array.from<E>)),
Effect.andThen(xs => isNonEmptyReadonlyArray(xs)),
Effect.mapError(_ => new SelectorWithoutMatchError({ selector })),
);
export const mustGetDOMElement = (parent: ParentElement) => <E extends Element = Element>(selector: string): E =>
pipe(
mayGetDOMElement(parent)<E>(selector),
Effect.orDie,
Effect.runSync,
);
export const mustGetDOMElements = (parent: ParentElement) => <E extends Element = Element>(selector: string): NERA<E> =>
pipe(
mayGetDOMElements(parent)<E>(selector),
Effect.orDie,
Effect.runSync,
);
/**
* Convertis une chaîne JSON en un objet JavaScript sous forme d'`Effect`.
*
* @param chaine La chaîne à convertir.
*
* @returns Un `Effect` avec soit un objet JS, soit une SyntaxError en cas de chaîne invalide.
*/
export const mayParseJSON = (chaine: string): Effect.Effect<JSONValue, SyntaxError> =>
Effect.try(() => JSON.parse(chaine));
export const mayStringifyJSON = <T>(json: T): Effect.Effect<string, NoSuchElementException | TypeError> =>
pipe(
Effect.try(() => JSON.stringify(json)),
Effect.andThen(Effect.fromNullable),
);
/**
* Vérifie qu'un sélecteur s'applique à l'élément DOM d'une cible d'événement (un `EventTarget`) donnée.
*
* @returns Un booléen
*/
export const targetMatchesSelector = (selector: string) => (target: EventTarget | null): boolean =>
target !== null && (target as HTMLElement).matches(selector);

View file

@ -0,0 +1,16 @@
import { Data } from "effect";
/** SelectorWithoutMatchError correspond à l'usage d'un sélecteur ne retournant aucun résultat. */
export class SelectorWithoutMatchError
extends Data.TaggedError("SelectorWithoutMatchError")<{ readonly selector: string }>
{
customMessage: string;
constructor(props: { readonly selector: string }) {
super(props);
this.customMessage = `Le selecteur ${this.selector} n'a retourné aucun résultat.`;
}
}
/** UnknownKeyArror correspond à la récupération d'une entrée avec une clé inexistante. */
export class UnknownKeyError extends Data.TaggedError("UnknownKeyArror")<{ key: string }> {}

View file

@ -0,0 +1,15 @@
import { captureException } from "@sentry/core";
import { logger } from "../logging";
import { SelectorWithoutMatchError } from "./dom/dom";
export const reportAndReturnError = <E extends Error>(err: E): E => {
captureException(err);
logger.error(err);
if (err instanceof SelectorWithoutMatchError) {
logger.error(err.customMessage);
}
return err;
};

View file

@ -0,0 +1,54 @@
import type { NoSuchElementException } from "effect/Cause";
import type { ParseError } from "effect/ParseResult";
import { Effect, pipe, Schema } from "effect";
import { mayParseJSON, mayStringifyJSON } from "./dom/dom";
/**
* Récupère de manière sûre une entrée dans le `LocalStorage`.
*
* @param key La clé de l'Entrée dans le `LocalStorage` souhaitée.
* @param itemSchema Le Schéma auquel l'entrée retournée doit correspondre.
* @throws `NoSuchElementException` L'entrée du `LocalStorage` contient `null`.
* @throws `ParseError` Erreur lors de la vérification du Schéma.
* @throws `SyntaError` La chaîne à convertir en _JSON_ n'est pas valide.
* @returns Un `Effect` contenant la valeur contenue dans l'entrée du `LocalStorage`.
*/
export const mayGetLocalStorageByKey = <S>(
key: string,
itemSchema: Schema.Schema<S, S>,
): Effect.Effect<S, NoSuchElementException | ParseError | SyntaxError> =>
pipe(
// Filtre les valeurs null.
Effect.fromNullable(localStorage.getItem(key)),
// Convertis depuis le JSON et vérifie le Schéma.
Effect.andThen((item: string) => mayParseJSON(item)),
Effect.andThen((json: JSONValue) => Schema.decodeUnknown(itemSchema)(json)),
);
/**
* Enregistre de manière sûre une entrée dans le `LocalStorage`.
*
* @param key La clé de l'Entrée dans le `LocalStorage` souhaitée.
* @param itemSchema Le Schéma auquel la valeur de l'entrée à enregistrer doit correspondre.
* @throws `DOMException` `QuotaExceededError` a é rencontrée lors de l'enregistrement dans le `LocalStorage`.
* @throws `NoSuchElementException` La conversion de _JSON_ à chaîne a retournée `null`.
* @throws `ParseError` Erreur lors de la vérification du Schéma.
* @throws `TypeError` Une référence circulaire ou un `BigInt` sont présents dans le JSON.
* @returns Un `Effect` sans valeur de retour.
*/
export const maySetLocalStorageItem =
<S>(key: string, itemSchema: Schema.Schema<S, S>) =>
(value: unknown): Effect.Effect<void, DOMException | NoSuchElementException | ParseError | TypeError> =>
pipe(
// Vérifie le Schéma.
Schema.decodeUnknown(itemSchema)(value),
Effect.andThen((value: S) => mayStringifyJSON(value)),
Effect.andThen((value: string) =>
Effect.try({
catch: (error: unknown) => new DOMException(`Erreur à l'enregistrement dans le LocalStorage : ${error}`),
try: (): void => localStorage.setItem(key, value),
})
),
);

View file

@ -0,0 +1,28 @@
import { Data } from "effect";
/**
* Erreur survenant en cas de code HTTP 400 Bad Request.
*/
export class HttpBadRequestError extends Data.TaggedError("HttpBadRequestError")<{
message: string;
}> {}
/**
* Erreur survenant en cas de code HTTP 403 Forbidden.
*/
export class HttpForbiddenError extends Data.TaggedError("HttpForbiddenError")<{ message: string }> {}
/**
* Erreur survenant en cas de code HTTP 404 Not Found.
*/
export class HttpNotFoundError extends Data.TaggedError("HttpNotFoundError")<{ message: string }> {}
/**
* Erreur survenant en cas de code HTTP 500 Server Error.
*/
export class HttpServerError extends Data.TaggedError("HttpServerError")<{ message: string }> {}
/**
* Erreur survenant en cas de code HTTP 401 Unauthorized Error.
*/
export class HttpUnauthorizedError extends Data.TaggedError("HttpUnauthorizedError")<{ message: string }> {}

View file

@ -0,0 +1,80 @@
import type { HttpBody } from "@effect/platform/HttpBody";
import { FetchHttpClient, HttpClientRequest, type HttpClientResponse } from "@effect/platform";
import { Effect, Layer, Match } from "effect";
import { UnknownException } from "effect/Cause";
import { ENTETE_WC_NONCE } from "../../../scripts/constantes/api";
import {
HttpBadRequestError,
HttpForbiddenError,
HttpNotFoundError,
HttpServerError,
HttpUnauthorizedError,
} from "./errors";
const HttpGETClient = FetchHttpClient.layer.pipe(
Layer.provide(
Layer.succeed(FetchHttpClient.RequestInit, {
credentials: "same-origin",
headers: {
Accept: "application/json",
},
method: "GET",
mode: "same-origin",
}),
),
);
export const HttpPOSTClient = FetchHttpClient.layer.pipe(
Layer.provide(
Layer.succeed(FetchHttpClient.RequestInit, {
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
mode: "same-origin",
}),
),
);
/** Un objet des en-têtes d'authentification pris en charge par le backend. */
export interface HandledAuthorizationHeaders {
authString?: string;
nonce?: string;
}
export const createAuthorizationHeaders = (headersValues: HandledAuthorizationHeaders): Record<string, string> => {
return {
...(headersValues.nonce && { [ENTETE_WC_NONCE]: headersValues.nonce }),
...(headersValues.authString && { Authorization: `Basic ${headersValues.authString}` }),
};
};
/** Un type union de tous les codes HTTP d'erreurs pris en charge. */
export type HttpStatusErrors =
| HttpBadRequestError
| HttpForbiddenError
| HttpNotFoundError
| HttpServerError
| HttpUnauthorizedError;
export const matchHttpStatus = (
res: HttpClientResponse.HttpClientResponse,
): Effect.Effect<HttpClientResponse.HttpClientResponse, HttpStatusErrors | UnknownException> =>
Match.value(res).pipe(
Match.when({ status: 200 }, r => Effect.succeed(r)),
Match.when({ status: 400 }, () => Effect.fail(new HttpBadRequestError({ message: "400 Bad Request" }))),
Match.when({ status: 401 }, () => Effect.fail(new HttpUnauthorizedError({ message: "401 Unauthorized" }))),
Match.when({ status: 403 }, () => Effect.fail(new HttpForbiddenError({ message: "403 Forbidden" }))),
Match.when({ status: 404 }, () => Effect.fail(new HttpNotFoundError({ message: "404 Not Found" }))),
Match.when({ status: 500 }, () => Effect.fail(new HttpServerError({ message: "400 Server Error" }))),
Match.orElse(r => Effect.fail(new UnknownException(r.status.toString()))),
);
// La validation des arguments a été faite en amont.
export const createPOSTFetch =
(route: string, headers: HandledAuthorizationHeaders) => (body: HttpBody): HttpClientRequest.HttpClientRequest =>
HttpClientRequest.post(route, { body, headers: createAuthorizationHeaders(headers) });

View file

@ -0,0 +1,8 @@
import { Effect } from "effect";
import { isNonEmptyReadonlyArray as isNERArray, type NonEmptyReadonlyArray } from "effect/Array";
import { NoSuchElementException } from "effect/Cause";
export const isNonEmptyReadonlyArray = <T>(
xs: ReadonlyArray<T>,
): Effect.Effect<NonEmptyReadonlyArray<T>, NoSuchElementException> =>
isNERArray(xs) ? Effect.succeed(xs) : Effect.fail(new NoSuchElementException());

View file

@ -0,0 +1,24 @@
import { ATTRIBUT_DESACTIVE, ATTRIBUT_HIDDEN } from "../../../scripts/constantes/dom";
import { E } from "../../../scripts/page-panier/scripts-page-panier-elements";
import { mustGetDOMElements } from "../../lib-effect/dom";
export const initAddressesSplitToggle = (): void => {
const billingAddressFields = mustGetDOMElements(E.FORMULAIRE_FACTURATION)<HTMLInputElement | HTMLSelectElement>(
"input, select",
);
E.BOUTON_SEPARATION_ADRESSES.addEventListener("click", (): void => {
if (E.BOUTON_SEPARATION_ADRESSES.checked) {
// Les Adresses sont séparées.
E.FORMULAIRE_FACTURATION.removeAttribute(ATTRIBUT_HIDDEN);
billingAddressFields.forEach(f => f.setAttribute(ATTRIBUT_DESACTIVE, ""));
} else {
// Les Adresses sont les même.
E.FORMULAIRE_FACTURATION.setAttribute(ATTRIBUT_HIDDEN, "");
billingAddressFields.forEach(f => {
f.setAttribute(ATTRIBUT_DESACTIVE, "");
f.value = "";
});
}
});
};

View file

@ -0,0 +1,155 @@
import type { NonEmptyReadonlyArray } from "effect/Array";
import { Array, Effect, Option, pipe, Schema } from "effect";
import { match } from "ts-pattern";
import type { SelectorWithoutMatchError } from "../../lib/dom/errors";
import {
ATTRIBUT_CLE_PANIER,
ATTRIBUT_DESACTIVE,
SELECTEUR_BOUTON_ADDITION_QUANTITE,
SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE,
SELECTEUR_BOUTON_SUPPRESSION_PANIER,
} from "../../../scripts/constantes/dom";
import { CartRemoveItemArgs, CartUpdateItemArgs } from "../../lib/cart/schemas";
import { targetMatchesSelector } from "../../lib/dom/dom";
import { API } from "../../services/api";
import {
type CartEntryContext,
type CartEntryInteractiveElements,
getCartEntryInteractiveElements,
} from "../../services/context";
import { CartPageElements } from "../../services/elements";
import { PAGE_STATES } from "./scripts-page-cart-state";
// Interfaces
type UpdateOperation = "DECREMENT" | "INCREMENT";
// Fonctions
/**
* Met à jour l'état (activé/désactivé) des Éléments DOM interactifs (boutons, champ de quantité) d'une Entrée de Panier.
*
* @param shouldActivate Est-ce que les Éléments interactifs sont à activer.
* @param eles Les Éléments DOM à mettre à jour.
* @returns L'objet des Éléments DOM interactifs passé en argument.
*/
const updateCartEntryInteractiveElements =
(shouldActivate: boolean) => (elements: CartEntryInteractiveElements): CartEntryInteractiveElements => {
if (shouldActivate) {
Number(elements.quantityInput.value) === 1
? elements.substractionButton.setAttribute(ATTRIBUT_DESACTIVE, "")
: elements.substractionButton.removeAttribute(ATTRIBUT_DESACTIVE);
elements.additionButton.removeAttribute(ATTRIBUT_DESACTIVE);
elements.deletionButton.removeAttribute(ATTRIBUT_DESACTIVE);
elements.deletionButton.textContent = "Remove";
} else {
elements.substractionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
elements.additionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
elements.deletionButton.setAttribute(ATTRIBUT_DESACTIVE, "");
elements.deletionButton.textContent = "Loading";
}
return elements;
};
/**
* Enclenche la mise à jour de l'état (activé/désactivé) des Éléments DOM interactifs (boutons, champ de quantité) des Entrées du Panier.
*
* @param shouldActivate Est-ce que les Éléments interactifs sont à activer.
* @param entries Les entrées du Panier sous forme de tableau d'Éléments.
* @returns Rien.
*/
export const refreshCartEntriesInteractiveElements =
(shouldActivate: boolean) => (entries: NonEmptyReadonlyArray<HTMLElement>): void =>
pipe(
Array.map(entries, (entry: HTMLElement): CartEntryInteractiveElements => getCartEntryInteractiveElements(entry)),
Array.forEach(elements => updateCartEntryInteractiveElements(shouldActivate)(elements)),
);
/**
* Génère la Requête API pour l'incrémentation de la quantité d'une Entrée de Panier.
*
* @param context Les informations et Éléments DOM d'intérêt de l'Entrée du Panier.
* @param operation Le type d'opération souhaitée sur la quantité.
* @returns Un `Effect` de la requête API pour l'incrémentation de la quantité.
*/
export const updateCartEntryQuantity = (
context: CartEntryContext,
operation: UpdateOperation,
) =>
// Injecte le Service API.
Effect.andThen(API, (api: API) =>
pipe(
// Créé de manière sûre les arguments de la Requête de mise à jour de l'Entrée du Panier.
Schema.decodeUnknown(CartUpdateItemArgs)({
key: context.cartKey,
quantity: operation === "INCREMENT"
? context.interactiveElements.quantityInput.valueAsNumber + 1
: context.interactiveElements.quantityInput.valueAsNumber - 1,
}),
// Désactive les Éléments interactifs des Entrées du Panier.
Effect.tap(() => refreshCartEntriesInteractiveElements(false)(context.cartEntries)),
// Génère la Requête.
Effect.andThen(args => api.CartItemUpdate({ nonce: PAGE_STATES.nonce })(args)),
));
export const removeCartEntry = (context: CartEntryContext) =>
// Injecte le Service API.
Effect.andThen(API, (api: API) =>
pipe(
// Créé de manière sûre l'argument pour la requête de suppression du Panier.
Schema.decodeUnknown(CartRemoveItemArgs)({ key: context.cartKey }),
// Désactive les Éléments interactifs des Entrées du Panier.
Effect.tap(() => refreshCartEntriesInteractiveElements(false)(context.cartEntries)),
// Génère la Requête.
Effect.andThen(args => api.CartItemRemove({ nonce: PAGE_STATES.nonce })(args)),
));
export const initCartEntriesInteractiveElements = (): Effect.Effect<void, SelectorWithoutMatchError> =>
Effect.gen(function*() {
const elements: CartPageElements = yield* CartPageElements;
const cartEntries: NonEmptyReadonlyArray<HTMLElement> = yield* elements.ENTREES_PANIER;
Array.forEach(cartEntries, (entry: HTMLElement) => {
const context: CartEntryContext = {
cartEntries: cartEntries,
cartKey: pipe(
Option.fromNullable(entry.getAttribute(ATTRIBUT_CLE_PANIER)),
Option.getOrElse(() => "-1"),
),
interactiveElements: getCartEntryInteractiveElements(entry),
};
entry.addEventListener("click", (event: Event): void => {
match(event.target)
.when(
targetMatchesSelector(SELECTEUR_BOUTON_ADDITION_QUANTITE),
() =>
updateCartEntryQuantity(context, "INCREMENT").pipe(
Effect.provide(API.Default),
Effect.runPromise,
),
)
.when(
targetMatchesSelector(SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE),
() =>
updateCartEntryQuantity(context, "DECREMENT").pipe(
Effect.provide(API.Default),
Effect.runPromise,
),
)
.when(
targetMatchesSelector(SELECTEUR_BOUTON_SUPPRESSION_PANIER),
() =>
removeCartEntry(context).pipe(
Effect.provide(API.Default),
Effect.runPromise,
),
)
.otherwise(_ => {});
});
});
}).pipe(Effect.provide(CartPageElements.Default));

View file

@ -0,0 +1,11 @@
/** États utiles pour les scripts de la page. */
interface PageStates {
/** Le jeton d'authentification des requêtes pour les versions plus récents de l'API WooCommerce. */
authString: string;
/** 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
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- États injectés par le modèle PHP
export const PAGE_STATES: PageStates = _etats;

View file

@ -0,0 +1,107 @@
import type { HttpBodyError } from "@effect/platform/HttpBody";
import type { HttpClientError } from "@effect/platform/HttpClientError";
import type { HttpClientRequest } from "@effect/platform/HttpClientRequest";
import type { TimeoutException, UnknownException } from "effect/Cause";
import type { ParseError } from "effect/ParseResult";
import { HttpBody, HttpClient, HttpClientResponse } from "@effect/platform";
import { Duration, Effect, pipe, Schedule } from "effect";
import type { WCStoreCartUpdateItemArgs } from "../../scripts/lib/types/api/cart-update-item";
import { ROUTE_API_MAJ_ARTICLE_PANIER, ROUTE_API_RETIRE_ARTICLE_PANIER } from "../../scripts/constantes/api";
import { Cart, type CartRemoveItemArgs } from "../lib/cart/schemas";
import {
createPOSTFetch,
type HandledAuthorizationHeaders,
HttpPOSTClient,
type HttpStatusErrors,
matchHttpStatus,
} from "../lib/network/network";
/**
* L'union des erreurs possibles au sein d'une requête API :
* 1. La conversion des arguments en JSON échoue (`HttpBodyError`).
* 2. Une erreur de requête ou de réponse lors de l'exécution du `fetch` (`HttpClientError`).
* 3. La requête a expirée (`TimeoutError`).
* 4. Le code de statut HTTP de la réponse n'est pas 200 (`HttpStatusError`).
* 5. Le code de statut HTTP de la réponse n'est pas pris en charge (`UnknownException`).
* 6. La forme du corps de la réponse ne correspond pas au schéma attendu (`ParseError`).
*/
export type APIRequestErrors =
| HttpBodyError
| HttpClientError
| HttpStatusErrors
| ParseError
| TimeoutException
| UnknownException;
const cartItemRemove =
(authHeaders: HandledAuthorizationHeaders) => (args: CartRemoveItemArgs): Effect.Effect<Cart, APIRequestErrors> =>
pipe(
// Convertis de manière sûre en JSON.
HttpBody.json(args),
// Créé la requête de mise à jour de l'Entrée auprès du backend.
Effect.andThen((body: HttpBody.Uint8Array): HttpClientRequest =>
createPOSTFetch(ROUTE_API_RETIRE_ARTICLE_PANIER, authHeaders)(body)
),
// Exécute la requête, la faisant expirer au bout de 15 secondes et la réessayant 3 fois avec un délai exponentiel.
Effect.andThen((req: HttpClientRequest) => HttpClient.execute(req)),
Effect.timeout(Duration.seconds(15)),
Effect.retry({
schedule: Schedule.exponential(Duration.seconds(10), 2),
times: 3,
while: err => err._tag === "RequestError",
}),
// Discrimine la Réponse en fonction du code de status HTTP.
Effect.andThen((res: HttpClientResponse.HttpClientResponse) => matchHttpStatus(res)),
// Le corps de la Réponse doit être un Panier.
Effect.andThen((res: HttpClientResponse.HttpClientResponse) =>
HttpClientResponse.schemaBodyJson(Cart, { errors: "all" })(res)
),
Effect.scoped,
Effect.provide(HttpPOSTClient),
);
/**
* @param authHeaders
* @param args
* @returns
*/
const cartItemUpdate =
(authHeaders: HandledAuthorizationHeaders) =>
(args: WCStoreCartUpdateItemArgs): Effect.Effect<Cart, APIRequestErrors> =>
pipe(
// Convertis de manière sûre en JSON.
HttpBody.json(args),
// Créé la requête de mise à jour de l'Entrée auprès du backend.
Effect.andThen((body: HttpBody.Uint8Array): HttpClientRequest =>
createPOSTFetch(ROUTE_API_MAJ_ARTICLE_PANIER, authHeaders)(body)
),
// Exécute la requête, la faisant expirer au bout de 15 secondes et la réessayant 3 fois avec un délai exponentiel.
Effect.andThen((req: HttpClientRequest) => HttpClient.execute(req)),
Effect.timeout(Duration.seconds(15)),
Effect.retry({
schedule: Schedule.exponential(Duration.seconds(10), 2),
times: 3,
while: err => err._tag === "RequestError",
}),
// Discrimine la Réponse en fonction du code de status HTTP.
Effect.andThen((res: HttpClientResponse.HttpClientResponse) => matchHttpStatus(res)),
// Le corps de la Réponse doit être un Panier.
Effect.andThen((res: HttpClientResponse.HttpClientResponse) =>
HttpClientResponse.schemaBodyJson(Cart, { errors: "all" })(res)
),
Effect.scoped,
Effect.provide(HttpPOSTClient),
);
export class API extends Effect.Service<API>()(
"API",
{
effect: Effect.succeed({
CartItemRemove: cartItemRemove,
CartItemUpdate: cartItemUpdate,
}),
},
) {}

View file

@ -0,0 +1,42 @@
import type { NonEmptyReadonlyArray } from "effect/Array";
import {
SELECTEUR_BOUTON_ADDITION_QUANTITE,
SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE,
SELECTEUR_BOUTON_SUPPRESSION_PANIER,
SELECTEUR_CHAMP_QUANTITE_LIGNE_PANIER,
} from "../../scripts/constantes/dom";
import { mustGetDOMElement } from "../lib/dom/dom";
export interface CartEntryInteractiveElements {
/** Le Bouton d'ajout de quantité d'un Produit. */
additionButton: HTMLButtonElement;
/** Le Bouton de suppression de l'Entrée du Panier. */
deletionButton: HTMLButtonElement;
/** Le champ de quantité de Produits d'une Entrée. */
quantityInput: HTMLInputElement;
/** Le Bouton de soustraction de quantité d'un Produit. */
substractionButton: HTMLButtonElement;
}
// TODO: Transformer ce Contexte en Service
export interface CartEntryContext {
cartEntries: NonEmptyReadonlyArray<HTMLElement>;
cartKey: string;
interactiveElements: CartEntryInteractiveElements;
}
/**
* Récupère les Éléments DOM interactifs (boutons, champ de quantité) d'une Entrée de Panier sous forme d'objet.
*
* Si les sélecteurs de ces Éléments ne retournent rien, une Erreur sera levée.
*
* @param entry L'Entrée de Panier sous forme d'Élément DOM.
* @returns Un objet des Éléments interactifs.
*/
export const getCartEntryInteractiveElements = (entry: HTMLElement): CartEntryInteractiveElements => ({
additionButton: mustGetDOMElement(entry)<HTMLButtonElement>(SELECTEUR_BOUTON_ADDITION_QUANTITE),
deletionButton: mustGetDOMElement(entry)<HTMLButtonElement>(SELECTEUR_BOUTON_SUPPRESSION_PANIER),
quantityInput: mustGetDOMElement(entry)<HTMLInputElement>(SELECTEUR_CHAMP_QUANTITE_LIGNE_PANIER),
substractionButton: mustGetDOMElement(entry)<HTMLButtonElement>(SELECTEUR_BOUTON_SOUSTRACTION_QUANTITE),
});

View file

@ -0,0 +1,82 @@
import { Effect } from "effect";
import {
SELECTEUR_BOUTON_ACTIONS_FORMULAIRE,
SELECTEUR_BOUTON_CODE_PROMO,
SELECTEUR_BOUTON_SEPARATION_ADRESSES,
SELECTEUR_CHAMP_CODE_PROMO,
SELECTEUR_CONTENEUR_METHODES_LIVRAISON,
SELECTEUR_CONTENEUR_PANIER,
SELECTEUR_ENSEMBLE_CODE_PROMO,
SELECTEUR_ENTREES_PANIER,
SELECTEUR_FORMULAIRE_FACTURATION,
SELECTEUR_FORMULAIRE_PANIER,
SELECTEUR_INSTRUCTIONS_CLIENT,
SELECTEUR_MESSAGE_CODE_PROMO,
SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES,
SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT,
SELECTEUR_SOUS_TOTAL_PRODUITS,
SELECTEUR_TOTAL_PANIER,
SELECTEUR_TOTAL_REDUCTION,
SELECTEUR_TOTAL_REDUCTION_VALEUR,
} from "../../scripts/constantes/dom";
import { mayGetDOMElements, mustGetDOMElement } from "../lib/dom/dom";
export const E = {
BOUTON_ACTIONS_FORMULAIRE: mustGetDOMElement(document)<HTMLButtonElement>(SELECTEUR_BOUTON_ACTIONS_FORMULAIRE),
BOUTON_CODE_PROMO: mustGetDOMElement(document)<HTMLButtonElement>(SELECTEUR_BOUTON_CODE_PROMO),
BOUTON_SEPARATION_ADRESSES: mustGetDOMElement(document)<HTMLInputElement>(SELECTEUR_BOUTON_SEPARATION_ADRESSES),
CHAMP_CODE_PROMO: mustGetDOMElement(document)<HTMLInputElement>(SELECTEUR_CHAMP_CODE_PROMO),
CONTENEUR_METHODES_LIVRAISON: mustGetDOMElement(document)<HTMLFieldSetElement>(
SELECTEUR_CONTENEUR_METHODES_LIVRAISON,
),
CONTENEUR_PANIER: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_CONTENEUR_PANIER),
ENSEMBLE_CODE_PROMO: mustGetDOMElement(document)<HTMLFormElement>(SELECTEUR_ENSEMBLE_CODE_PROMO),
ENTREES_PANIER: mayGetDOMElements(document)<HTMLElement>(SELECTEUR_ENTREES_PANIER),
FORMULAIRE_FACTURATION: mustGetDOMElement(document)<HTMLDivElement>(SELECTEUR_FORMULAIRE_FACTURATION),
FORMULAIRE_PANIER: mustGetDOMElement(document)<HTMLFormElement>(SELECTEUR_FORMULAIRE_PANIER),
INSTRUCTIONS_CLIENT: mustGetDOMElement(document)<HTMLTextAreaElement>(SELECTEUR_INSTRUCTIONS_CLIENT),
MESSAGE_ADRESSES: mustGetDOMElement(document)<HTMLParagraphElement>(SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES),
MESSAGE_CODE_PROMO: mustGetDOMElement(document)<HTMLParagraphElement>(SELECTEUR_MESSAGE_CODE_PROMO),
SOUS_TOTAL_LIVRAISON_VALEUR: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT),
SOUS_TOTAL_PRODUITS: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_PRODUITS_VALEUR: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_REDUCTION: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
SOUS_TOTAL_REDUCTION_VALEUR: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
TOTAL_PANIER: mustGetDOMElement(document)<HTMLParagraphElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_PANIER_VALEUR: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_REDUCTION_LIGNE: mustGetDOMElement(document)<HTMLDivElement>(SELECTEUR_TOTAL_REDUCTION),
TOTAL_REDUCTION_VALEUR: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
};
export class CartPageElements extends Effect.Service<CartPageElements>()(
"CartPageElements",
{
effect: Effect.succeed({
BOUTON_ACTIONS_FORMULAIRE: mustGetDOMElement(document)<HTMLButtonElement>(SELECTEUR_BOUTON_ACTIONS_FORMULAIRE),
BOUTON_CODE_PROMO: mustGetDOMElement(document)<HTMLButtonElement>(SELECTEUR_BOUTON_CODE_PROMO),
BOUTON_SEPARATION_ADRESSES: mustGetDOMElement(document)<HTMLInputElement>(SELECTEUR_BOUTON_SEPARATION_ADRESSES),
CHAMP_CODE_PROMO: mustGetDOMElement(document)<HTMLInputElement>(SELECTEUR_CHAMP_CODE_PROMO),
CONTENEUR_METHODES_LIVRAISON: mustGetDOMElement(document)<HTMLFieldSetElement>(
SELECTEUR_CONTENEUR_METHODES_LIVRAISON,
),
CONTENEUR_PANIER: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_CONTENEUR_PANIER),
ENSEMBLE_CODE_PROMO: mustGetDOMElement(document)<HTMLFormElement>(SELECTEUR_ENSEMBLE_CODE_PROMO),
ENTREES_PANIER: mayGetDOMElements(document)<HTMLElement>(SELECTEUR_ENTREES_PANIER),
FORMULAIRE_FACTURATION: mustGetDOMElement(document)<HTMLDivElement>(SELECTEUR_FORMULAIRE_FACTURATION),
FORMULAIRE_PANIER: mustGetDOMElement(document)<HTMLFormElement>(SELECTEUR_FORMULAIRE_PANIER),
INSTRUCTIONS_CLIENT: mustGetDOMElement(document)<HTMLTextAreaElement>(SELECTEUR_INSTRUCTIONS_CLIENT),
MESSAGE_ADRESSES: mustGetDOMElement(document)<HTMLParagraphElement>(SELECTEUR_MESSAGE_FORMULAIRE_ADRESSES),
MESSAGE_CODE_PROMO: mustGetDOMElement(document)<HTMLParagraphElement>(SELECTEUR_MESSAGE_CODE_PROMO),
SOUS_TOTAL_LIVRAISON_VALEUR: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_SOUS_TOTAL_LIVRAISON_COUT),
SOUS_TOTAL_PRODUITS: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_PRODUITS_VALEUR: mustGetDOMElement(document)<HTMLElement>(SELECTEUR_SOUS_TOTAL_PRODUITS),
SOUS_TOTAL_REDUCTION: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
SOUS_TOTAL_REDUCTION_VALEUR: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
TOTAL_PANIER: mustGetDOMElement(document)<HTMLParagraphElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_PANIER_VALEUR: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_PANIER),
TOTAL_REDUCTION_LIGNE: mustGetDOMElement(document)<HTMLDivElement>(SELECTEUR_TOTAL_REDUCTION),
TOTAL_REDUCTION_VALEUR: mustGetDOMElement(document)<HTMLSpanElement>(SELECTEUR_TOTAL_REDUCTION_VALEUR),
}),
},
) {}

View file

View file

@ -6,17 +6,7 @@ import type { ParentElement } from "./types/dom.d.ts";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_DESACTIVE } from "../constantes/dom.ts";
import { logger } from "../logging.ts";
import { lanceAnimationCycleLoading } from "./animations.ts";
import {
BadRequestError,
creeSyntaxError,
ERREUR_SELECTEUR_INEXISTANT,
ERREUR_SYNTAXE_INVALIDE,
ForbiddenError,
NotFoundError,
reporteEtLeveErreur,
ServerError,
UnauthorizedError,
} from "./erreurs";
import { createSyntaxError, ErrorInvalidSelector, ErrorNonExistingSelector, reporteEtLeveErreur } from "./erreurs.ts";
export const recupereElementAvecSelecteur =
(parent: ParentElement) => <E extends Element = Element>(selecteur: string): Either<SyntaxError, E> =>
@ -24,10 +14,10 @@ export const recupereElementAvecSelecteur =
// Retourne une SyntaxError dans un Left si le sélecteur est invalide
.encase(() => parent.querySelector<E>(selecteur))
// Transforme le Left en une erreur plus sympathique
.mapLeft(_ => creeSyntaxError(ERREUR_SYNTAXE_INVALIDE(selecteur)))
.mapLeft(_ => createSyntaxError(ErrorInvalidSelector(selecteur)))
// Retourne une SyntaxError si l'Élément est null
.chain((e: E | null) =>
G.isNotNullable(e) ? Right(e) : Left(creeSyntaxError(ERREUR_SELECTEUR_INEXISTANT(selecteur)))
G.isNotNullable(e) ? Right(e) : Left(createSyntaxError(ErrorNonExistingSelector(selecteur)))
);
export const getDOMElementsWithSelector =
@ -36,9 +26,9 @@ export const getDOMElementsWithSelector =
// Retourne une SyntaxError dans un Left si le sélecteur est invalide
.encase(() => pipe(parent.querySelectorAll<E>(selecteur), Array.from<E>))
// Transforme le Left en une erreur plus sympathique
.mapLeft(_ => creeSyntaxError(ERREUR_SYNTAXE_INVALIDE(selecteur)))
.mapLeft(_ => createSyntaxError(ErrorInvalidSelector(selecteur)))
// Retourne une SyntaxError si le tableau est vide
.chain((e: Array<E>) => A.isEmpty(e) ? Left(creeSyntaxError(ERREUR_SELECTEUR_INEXISTANT(selecteur))) : Right(e));
.chain((e: Array<E>) => A.isEmpty(e) ? Left(createSyntaxError(ErrorNonExistingSelector(selecteur))) : Right(e));
export const recupereElementOuLeve = <E extends Element = Element>(elementOuErreur: Either<SyntaxError, E>): E =>
elementOuErreur.caseOf({

View file

@ -10,20 +10,20 @@ import type { WCErrorBody } from "./types/api/erreurs";
import { ErreurAdresseInvalide } from "./erreurs/adresses";
/* Messages d'erreur */
export const ERREUR_SYNTAXE_INVALIDE = (selecteur: string): string => `Le selecteur "${selecteur}" est invalide`;
export const ERREUR_SELECTEUR_INEXISTANT = (selecteur: string): string =>
/** @deprecated */
export const ErrorInvalidSelector = (selecteur: string): string => `Le selecteur "${selecteur}" est invalide`;
/** @deprecated */
export const ErrorNonExistingSelector = (selecteur: string): string =>
`La requête "${selecteur}" n'a retourné aucun Élément.`;
export const InvalidSelectorError = (s: string): SyntaxError => new SyntaxError(`Le selecteur ${s} est invalide.`);
export const NoResultsSelectorError = (s: string): SyntaxError =>
new SyntaxError(`Le sélecteur ${s} n'a retourné aucun Élément.`);
/* Création d'erreurs */
export const creeSyntaxError = (message: string): SyntaxError => new SyntaxError(message);
export const createSyntaxError = (message: string): SyntaxError => new SyntaxError(message);
/* Types d'erreurs */
export class BadRequestError extends Error {
constructor(message = "400 BadRequestError") {
super(message);
this.name = "BadRequestError";
}
}
export class CleNonTrouveError extends Error {
constructor(message: unknown) {
super(JSON.stringify(message));
@ -36,36 +36,12 @@ export class DOMElementAbsentError extends Error {
this.name = "DOMElementAbsentError";
}
}
export class ForbiddenError extends Error {
constructor(message = "403 ForbiddenError") {
super(message);
this.name = "ForbiddenError";
}
}
export class NonExistingKeyError extends Error {
constructor(message: unknown) {
super(JSON.stringify(message));
this.name = "NonExistingKeyError";
}
}
export class NotFoundError extends Error {
constructor(message = "404 NotFoundError") {
super(message);
this.name = "NotFoundError";
}
}
export class ServerError extends Error {
constructor(message = "500 ServerError") {
super(message);
this.name = "ServerError";
}
}
export class UnauthorizedError extends Error {
constructor(message = "401 UnauthorizedError") {
super(message);
this.name = "UnauthorizedError";
}
}
export class UnknownError extends Error {
constructor(message: unknown) {
super(JSON.stringify(message));

View file

View file

View file

@ -0,0 +1,9 @@
import { Effect } from "effect";
import { initAddressesSplitToggle } from "./page-panier/effect/scripts-page-cart-forms";
import { initCartEntriesInteractiveElements } from "./page-panier/effect/scripts-page-cart-products";
document.addEventListener("DOMContentLoaded", (): void => {
initAddressesSplitToggle();
initCartEntriesInteractiveElements().pipe(Effect.runSync);
});