wip + convertis images

This commit is contained in:
gcch 2026-03-30 17:05:59 +02:00
commit ffadb1644c
2742 changed files with 898 additions and 607 deletions

View file

@ -0,0 +1,42 @@
import { Option, pipe } from "effect";
import { Array as EffectArray } from "effect";
import { NonEmptyReadonlyArray } from "effect/Array";
import { getOptionOrThrowWithError } from "./utils";
/** Type union des parents possibles pour un `querySelector`. */
export type ParentElement = Document | Element;
export const getFirstSelectorFromParent =
(parent: ParentElement) => <E extends Element = Element>(selector: string): Option.Option<NonNullable<E>> =>
Option.fromNullishOr(parent.querySelector<E>(selector));
export const getFirstSelectorFromDocument = <E extends Element = Element>(
selector: string,
): Option.Option<NonNullable<E>> => getFirstSelectorFromParent(document)<E>(selector);
export const getFirstSelectorFromDocumentOrThrow = <E extends Element = Element>(selector: string): NonNullable<E> =>
pipe(
getFirstSelectorFromDocument<E>(selector),
getOptionOrThrowWithError(`Il n'y a pas d'Élément dans le Document avec le sélecteur suivant : ${selector}.`),
);
export const getAllSelectorFromParent =
(parent: ParentElement) => <E extends Element = Element>(selector: string): Option.Option<NonEmptyReadonlyArray<E>> =>
pipe(
parent.querySelectorAll<E>(selector),
// Convertis NodeListOf en Array.
Array.from<E>,
(xs: Array<E>) => Option.liftPredicate(EffectArray.isReadonlyArrayNonEmpty)(xs),
);
export const getAllSelectorFromDocument = <E extends Element = Element>(
selector: string,
): Option.Option<NonEmptyReadonlyArray<E>> => getAllSelectorFromParent(document)<E>(selector);
export const getAllSelectorFromDocumentOrThrow = <E extends Element = Element>(
selector: string,
): NonEmptyReadonlyArray<E> =>
pipe(
getAllSelectorFromDocument<E>(selector),
getOptionOrThrowWithError(`Il n'y a pas d'Éléments dans le Document avec le sélecteur suivant : ${selector}.`),
);

View file

@ -0,0 +1,7 @@
import { pipe, Option } from "effect";
export const getOptionOrThrowWithError = (message: string) => <T>(option: Option.Option<T>): T =>
pipe(
option,
Option.getOrThrowWith(() => new Error(message)),
);

View file

@ -26,7 +26,7 @@ export const ATTRIBUT_TABINDEX = "tabindex";
// En-tête
export const DOM_BOUTON_MENU_MOBILE = "#bouton-menu-mobile";
export const DOM_BOUTON_PANIER = ".compte-panier a[rel='cart']";
export const DOM_ENTREE_MENU_CATEGORIES_PRODUITS = "#menu-categories-produits ul li a";
export const DOM_ENTREES_MENU_CATEGORIES_PRODUITS = "#menu-categories-produits ul li a";
export const DOM_MENU_CATEGORIES_PRODUITS = "#menu-categories-produits";
export const DOM_MENU_MOBILE = "#menu-mobile";

View file

@ -1,25 +1,10 @@
import { pipe } from "@mobily/ts-belt";
import { EitherAsync } from "purify-ts";
import { match, P } from "ts-pattern";
import { type GenericSchema, parse } from "valibot";
import { match } from "ts-pattern";
import type { HttpCodeErrors, SimplifiedResponse } from "./types/reseau";
import { ENTETE_WC_NONCE } from "../constantes/api.ts";
import {
BadRequestError,
ErreurInconnue,
ForbiddenError,
leveBadRequestError,
leveErreur,
leveNotFoundError,
leveUnauthorizedError,
NotFoundError,
ServerError,
UnauthorizedError,
type UnknownError,
} from "./erreurs.ts";
import { estWCError } from "./schemas/api/erreurs.ts";
import { BadRequestError, ForbiddenError, NotFoundError, ServerError, UnauthorizedError } from "./erreurs.ts";
// Types
@ -149,22 +134,6 @@ export const prefilledPostBackend =
export const safeFetch = (f: Promise<Response>): EitherAsync<DOMException | TypeError, Response> =>
EitherAsync<DOMException | TypeError, Response>(async () => await f);
// TODO: Ne traite pas du tout les Erreurs
// TODO: Utiliser un Either
export const traiteReponseBackendWCSelonCodesHTTP = <R, S extends GenericSchema<R>>(
corpsReponse: unknown,
schemaReponse: S,
): R =>
match(corpsReponse)
// Réponses problématiques
.with({ body: P.select(), status: 400 }, estWCError, leveBadRequestError)
.with({ body: P.select(), status: 401 }, estWCError, leveUnauthorizedError)
.with({ body: P.select(), status: 404 }, estWCError, leveNotFoundError)
// Réponse OK (201)
.with(P._, corpsOkInconnu => parse<S>(schemaReponse, corpsOkInconnu))
// Réponses inconnues
.otherwise(e => pipe(e, ErreurInconnue, leveErreur<UnknownError>));
// Réponses Simplifiées
export const newPartialResponse = async (reponse: Response): Promise<SimplifiedResponse> => {
return {

View file

@ -1,35 +1,39 @@
/** Scripts pour le Menu des Catégories de Produits */
import { A } from "@mobily/ts-belt";
import { match } from "ts-pattern";
import { Array as EffectArray, Match, Predicate } from "effect";
import { DOM_ENTREE_MENU_CATEGORIES_PRODUITS, DOM_MENU_CATEGORIES_PRODUITS } from "./constantes/dom.ts";
import { mustGetEleInDocument, mustGetElesInDocument } from "./lib/dom.ts";
import { DOM_ENTREES_MENU_CATEGORIES_PRODUITS, DOM_MENU_CATEGORIES_PRODUITS } from "./constantes/dom.ts";
import { getAllSelectorFromDocumentOrThrow, getFirstSelectorFromDocumentOrThrow } from "../scripts-effect/lib/dom.ts";
// Initialise les attributs HTML pour l'affichage initiale des flèches de défilement du menu de catégories de Produits.
document.addEventListener("DOMContentLoaded", (): void => {
const MENU_CATEGORIES_PRODUITS: HTMLElement = mustGetEleInDocument(DOM_MENU_CATEGORIES_PRODUITS);
const ENTREES_MENU_CATEGORIES_PRODUITS: Array<HTMLAnchorElement> = mustGetElesInDocument(
DOM_ENTREE_MENU_CATEGORIES_PRODUITS,
const productsCategoriesMenu: HTMLElement = getFirstSelectorFromDocumentOrThrow<HTMLElement>(
DOM_MENU_CATEGORIES_PRODUITS,
);
const menuEntries: ReadonlyArray<HTMLAnchorElement> = getAllSelectorFromDocumentOrThrow(
DOM_ENTREES_MENU_CATEGORIES_PRODUITS,
);
A.forEachWithIndex(
[ENTREES_MENU_CATEGORIES_PRODUITS.at(0), ENTREES_MENU_CATEGORIES_PRODUITS.at(-1)],
(index, entreeMenu): void => {
if (!entreeMenu) return;
const firstAndLastEntries: Array<(HTMLAnchorElement | undefined)> = [menuEntries.at(0), menuEntries.at(-1)];
new IntersectionObserver(
A.forEach(entree => {
// Ne déclenche rien si le scroll n'est pas horizontal
if (entree.boundingClientRect.top <= 0) return;
match([entree.isIntersecting, index])
.with([true, 0], () => MENU_CATEGORIES_PRODUITS.removeAttribute("data-entrees-presentes-debut"))
.with([true, 1], () => MENU_CATEGORIES_PRODUITS.removeAttribute("data-entrees-presentes-fin"))
.with([false, 0], () => MENU_CATEGORIES_PRODUITS.setAttribute("data-entrees-presentes-debut", ""))
.with([false, 1], () => MENU_CATEGORIES_PRODUITS.setAttribute("data-entrees-presentes-fin", ""))
.run();
}),
{ root: null, threshold: 0.9 },
).observe(entreeMenu);
},
);
// Créé un nouvel Observer pour la première et dernière entrée.
EffectArray.forEach(firstAndLastEntries, (menuEntry, _index) => {
if (Predicate.isUndefined(menuEntry)) return;
new IntersectionObserver(
EffectArray.forEach(intersectionEntry => {
// Ne déclenche rien si le scroll n'est pas horizontal
if (intersectionEntry.boundingClientRect.top <= 0) return;
Match.value([intersectionEntry.isIntersecting]).pipe(
Match.when([true, 0], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-debut")),
Match.when([true, 1], () => productsCategoriesMenu.removeAttribute("data-entrees-presentes-fin")),
Match.when([false, 0], () => productsCategoriesMenu.setAttribute("data-entrees-presentes-debut", "")),
Match.when([false, 1], () => productsCategoriesMenu.setAttribute("data-entrees-presentes-fin", "")),
Match.orElse(() => {}),
);
}),
{ root: null, threshold: 0.9 },
).observe(menuEntry);
});
});