sauvegarde le travail accompli
This commit is contained in:
parent
caf87cf1da
commit
26165682d9
521 changed files with 4919 additions and 17279 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export const CATALOG_VISIBILITIES = {
|
||||
INVISIBLE: "invisible",
|
||||
VISIBLE: "visible",
|
||||
} as const;
|
||||
118
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/cart/schemas.ts
Executable file
118
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/cart/schemas.ts
Executable 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,
|
||||
// });
|
||||
125
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/dom/dom.test.ts
Executable file
125
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/dom/dom.test.ts
Executable 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);
|
||||
});
|
||||
});
|
||||
73
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/dom/dom.ts
Executable file
73
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/dom/dom.ts
Executable 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);
|
||||
16
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/dom/errors.ts
Executable file
16
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/dom/errors.ts
Executable 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 }> {}
|
||||
15
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/errors.ts
Executable file
15
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/errors.ts
Executable 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;
|
||||
};
|
||||
54
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/local-storage.ts
Executable file
54
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/local-storage.ts
Executable 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 été 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),
|
||||
})
|
||||
),
|
||||
);
|
||||
28
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/network/errors.ts
Executable file
28
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/network/errors.ts
Executable 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 }> {}
|
||||
80
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/network/network.ts
Executable file
80
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/network/network.ts
Executable 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) });
|
||||
8
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/validation.ts
Executable file
8
web/app/themes/haiku-atelier-2024/src/scripts-effect/lib/validation.ts
Executable 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());
|
||||
|
|
@ -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 = "";
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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;
|
||||
107
web/app/themes/haiku-atelier-2024/src/scripts-effect/services/api.ts
Executable file
107
web/app/themes/haiku-atelier-2024/src/scripts-effect/services/api.ts
Executable 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,
|
||||
}),
|
||||
},
|
||||
) {}
|
||||
42
web/app/themes/haiku-atelier-2024/src/scripts-effect/services/context.ts
Executable file
42
web/app/themes/haiku-atelier-2024/src/scripts-effect/services/context.ts
Executable 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),
|
||||
});
|
||||
82
web/app/themes/haiku-atelier-2024/src/scripts-effect/services/elements.ts
Executable file
82
web/app/themes/haiku-atelier-2024/src/scripts-effect/services/elements.ts
Executable 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),
|
||||
}),
|
||||
},
|
||||
) {}
|
||||
0
web/app/themes/haiku-atelier-2024/src/scripts/lib/arrays.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/lib/arrays.ts
Normal file → Executable 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({
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
0
web/app/themes/haiku-atelier-2024/src/scripts/lib/safe-arrays.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/lib/safe-arrays.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/logging.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/logging.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/page-panier/scripts-page-panier-elements.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/page-panier/scripts-page-panier-elements.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/page-panier/scripts-page-panier-local-storage.ts
Normal file → Executable file
0
web/app/themes/haiku-atelier-2024/src/scripts/page-panier/scripts-page-panier-local-storage.ts
Normal file → Executable file
9
web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-cart.ts
Executable file
9
web/app/themes/haiku-atelier-2024/src/scripts/scripts-page-cart.ts
Executable 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue