ref(boutique) wip scripts sous forme Effect
This commit is contained in:
parent
ab665599c9
commit
305fcce1ba
7 changed files with 248 additions and 71 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { Schema } from "effect";
|
import { Schema } from "effect";
|
||||||
import { ProductId, ProductQuantity, ProductVariationAttribute } from "./product.ts";
|
import { ProductId, ProductQuantity, ProductStatus, ProductVariationAttribute } from "./product.ts";
|
||||||
|
|
||||||
class CartProduct extends Schema.Class<CartProduct>("CartProduct")({
|
class CartProduct extends Schema.Class<CartProduct>("CartProduct")({
|
||||||
id: ProductId,
|
id: ProductId,
|
||||||
|
|
@ -7,4 +7,15 @@ class CartProduct extends Schema.Class<CartProduct>("CartProduct")({
|
||||||
variation: Schema.Array(ProductVariationAttribute),
|
variation: Schema.Array(ProductVariationAttribute),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export { CartProduct };
|
class GetProducts extends Schema.Class<GetProducts>("GetProducts")({
|
||||||
|
/** L'ID de la Catégorie de Produits demandé. */
|
||||||
|
category: Schema.Int.pipe(Schema.optional),
|
||||||
|
/** Le numéro de page demandé. */
|
||||||
|
page: Schema.Int,
|
||||||
|
/** Le nombre de Produits par page demandé. */
|
||||||
|
per_page: Schema.Int,
|
||||||
|
/** Le statut demandé des Produits. */
|
||||||
|
status: ProductStatus,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export { CartProduct, GetProducts };
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
import { Effect, Option, pipe, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
import { Effect, Option, pipe, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
||||||
import type { SchemaError } from "effect/Schema";
|
import type { SchemaError } from "effect/Schema";
|
||||||
|
|
||||||
|
const ProductStatus = Schema.Literals(["any", "draft", "future", "pending", "private", "publish", "trash"]);
|
||||||
|
|
||||||
/** Représente l'identifiant numérique unique d'un Produit. */
|
/** Représente l'identifiant numérique unique d'un Produit. */
|
||||||
const ProductId = Schema.Int.pipe(Schema.brand("ProductId")).check(Schema.isGreaterThan(0));
|
const ProductId = Schema.Int.pipe(Schema.brand("ProductId")).check(Schema.isGreaterThan(0));
|
||||||
|
|
||||||
|
|
@ -78,6 +80,7 @@ export {
|
||||||
ProductId,
|
ProductId,
|
||||||
ProductQuantity,
|
ProductQuantity,
|
||||||
ProductQuantityFromString,
|
ProductQuantityFromString,
|
||||||
|
ProductStatus,
|
||||||
ProductVariation,
|
ProductVariation,
|
||||||
ProductVariationAttribute,
|
ProductVariationAttribute,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
||||||
|
|
||||||
|
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||||
|
import ShopPageDOM from "./service-dom.ts";
|
||||||
|
import ShopPageElements from "./service-elements.ts";
|
||||||
|
import ShopPageMessages from "./service-messages.ts";
|
||||||
|
|
||||||
|
const ShopPageRuntime = ManagedRuntime.make(
|
||||||
|
pipe(
|
||||||
|
ShopPageDOM.Live,
|
||||||
|
Layer.provideMerge(ShopPageMessages.Live),
|
||||||
|
Layer.provideMerge(ShopPageElements.Live),
|
||||||
|
Layer.provide(APIClient.Live),
|
||||||
|
Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
export default ShopPageRuntime;
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import {
|
||||||
|
Console,
|
||||||
|
Context,
|
||||||
|
Effect,
|
||||||
|
Layer,
|
||||||
|
Option,
|
||||||
|
pipe,
|
||||||
|
Ref,
|
||||||
|
Schema,
|
||||||
|
SchemaIssue,
|
||||||
|
Stream,
|
||||||
|
SubscriptionRef,
|
||||||
|
} from "effect";
|
||||||
|
import { SchemaError } from "effect/Schema";
|
||||||
|
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||||
|
import { setLoadingState } from "../../scripts-effect/lib/elements.ts";
|
||||||
|
import { GetProducts } from "../../scripts-effect/schemas/api.ts";
|
||||||
|
import { ATTRIBUT_ID_CATEGORIE_PRODUITS } from "../constantes/dom.ts";
|
||||||
|
import ShopPageElements from "./service-elements.ts";
|
||||||
|
import ShopPageMessages from "./service-messages.ts";
|
||||||
|
|
||||||
|
const PRODUCTS_PER_PAGE = 18;
|
||||||
|
|
||||||
|
const PageStatesSchema = Schema.Struct({
|
||||||
|
authString: Schema.NonEmptyString,
|
||||||
|
nonce: Schema.NonEmptyString,
|
||||||
|
});
|
||||||
|
|
||||||
|
class InvalidPageStateError extends Schema.TaggedErrorClass<InvalidPageStateError>()("InvalidPageStateError", {
|
||||||
|
cause: Schema.String,
|
||||||
|
}) {
|
||||||
|
static readonly fromSchemaError = (schemaError: SchemaError): InvalidPageStateError =>
|
||||||
|
new InvalidPageStateError({
|
||||||
|
cause: SchemaIssue.makeFormatterDefault()(schemaError.issue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShopPageDOM extends Context.Service<ShopPageDOM>()("haikuatelier.fr/Shop/ShopPageDOM", {
|
||||||
|
make: Effect.gen(function*() {
|
||||||
|
const Elements = yield* ShopPageElements;
|
||||||
|
const Messages = yield* ShopPageMessages;
|
||||||
|
const API = yield* APIClient;
|
||||||
|
|
||||||
|
const PageStates = yield* pipe(
|
||||||
|
Elements.PageStatesRawJson.textContent,
|
||||||
|
(textContent: string) =>
|
||||||
|
Schema.decodeUnknownEffect(Schema.fromJsonString(PageStatesSchema))(textContent, { errors: "all" }),
|
||||||
|
Effect.mapError(InvalidPageStateError.fromSchemaError),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** ID de la Catégorie des Produits de la Page, si la Page courante est une Archive. */
|
||||||
|
const ProductsCategoryId: Ref.Ref<Option.Option<number>> = yield* Ref.make(
|
||||||
|
Option.fromNullishOr(Number(Elements.ProductsGrid.getAttribute(ATTRIBUT_ID_CATEGORIE_PRODUITS))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Créer une SubscriptionRef mettant à jour le DOM au changement de valeur.
|
||||||
|
const PageNumber = yield* Ref.make(1);
|
||||||
|
|
||||||
|
const onMoreProductedWantedHandler = Effect.fn("onMoreProductedWantedHandler")(function*() {
|
||||||
|
yield* Console.debug("onMoreProductedWantedHandler");
|
||||||
|
|
||||||
|
const newPageNumber = yield* Ref.getAndUpdate(PageNumber, pageNumber => pageNumber + 1);
|
||||||
|
const categoryId = pipe(yield* Ref.get(ProductsCategoryId), Option.getOrUndefined);
|
||||||
|
const requestBody = yield* GetProducts.makeEffect({
|
||||||
|
page: newPageNumber,
|
||||||
|
per_page: PRODUCTS_PER_PAGE,
|
||||||
|
status: "publish",
|
||||||
|
...(categoryId && { category: categoryId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Désactive les interactions et affiche un texte de chargement le temps de la requête.
|
||||||
|
yield* setLoadingState(Elements.ShowMoreButton, true);
|
||||||
|
yield* SubscriptionRef.set(Messages.ShowMoreButtonText, "Getting Products...");
|
||||||
|
|
||||||
|
const newProducts = yield* API.GetProducts(PageStates.nonce, PageStates.authString, requestBody);
|
||||||
|
yield* Console.debug("onMoreProductedWantedHandler", newProducts);
|
||||||
|
|
||||||
|
// Rétablis le texte du Bouton et réactive les interactions.
|
||||||
|
yield* SubscriptionRef.set(Messages.ShowMoreButtonText, "Show more");
|
||||||
|
yield* setLoadingState(Elements.ShowMoreButton, false);
|
||||||
|
|
||||||
|
// Cache le bouton s'il y a moins de PRODUCTS_PER_PAGE Produits disponibles (que l'on est à la dernière page)
|
||||||
|
if (donnees.length < PRODUCTS_PER_PAGE) {
|
||||||
|
E.BOUTON_PLUS_DE_PRODUITS.toggleAttribute(ATTRIBUT_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créé un DocumentFragment qui recevra tous les nouveaux Produits
|
||||||
|
const fragment: DocumentFragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
// Créé les Éléments <article> à insérer
|
||||||
|
for (const produit of donnees.slice(0, PRODUCTS_PER_PAGE)) {
|
||||||
|
pipe(
|
||||||
|
html`
|
||||||
|
<article class="produit">
|
||||||
|
<figure>
|
||||||
|
<a href="/product/${produit.slug}">
|
||||||
|
<picture class="produit__illustration produit__illustration__principale">
|
||||||
|
${produit.image_repos ?? ""}
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<picture class="produit__illustration produit__illustration__survol">
|
||||||
|
${produit.image_survol ?? ""}
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<figcaption class="produit__textuel">
|
||||||
|
<h3 class="produit__textuel__titre">
|
||||||
|
<a href="${produit.permalink}">${produit.name}</a>
|
||||||
|
</h3>
|
||||||
|
<p class="produit__textuel__prix">
|
||||||
|
${produit.prix_maximal}€
|
||||||
|
</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
tap(article => fragment.append(article)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajoute les nouveaux Produits dans le DOM
|
||||||
|
E.GRILLE_PRODUITS.append(fragment);
|
||||||
|
E.GRILLE_PRODUITS.setAttribute(ATTRIBUT_PAGE, String(nouveauNumeroPage));
|
||||||
|
});
|
||||||
|
|
||||||
|
const initLoadMoreProductsOnButtonClick = Effect.fn("initLoadMoreProductsOnButtonClick")(function*() {
|
||||||
|
return yield* pipe(
|
||||||
|
Stream.fromEventListener(Elements.ShowMoreButton, "click"),
|
||||||
|
Stream.tap(onMoreProductedWantedHandler),
|
||||||
|
Stream.runDrain,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ProductsCategoryId,
|
||||||
|
initLoadMoreProductsOnButtonClick,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}) {
|
||||||
|
static readonly Live = Layer.effect(this, this.make);
|
||||||
|
}
|
||||||
|
export default ShopPageDOM;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import { getFirstSelectorFromDocument } from "../../scripts-effect/lib/dom.ts";
|
||||||
|
import { IncoherentDOMError } from "../page-produit/errors.ts";
|
||||||
|
|
||||||
|
class ShopPageElements extends Context.Service<ShopPageElements>()("haikuatelier.fr/Shop/ShopPageElements", {
|
||||||
|
make: Effect.gen(function*() {
|
||||||
|
const PageStatesRawJson = yield* getFirstSelectorFromDocument<HTMLScriptElement>("#page-states");
|
||||||
|
|
||||||
|
/** Le Bouton « Show more » pour afficher plus de Produits à la suite de la Grille. */
|
||||||
|
const ShowMoreButton = yield* getFirstSelectorFromDocument<HTMLButtonElement>(
|
||||||
|
"#page-boutique #bouton-plus-de-produits",
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Le conteneur de la Grille des Produits. */
|
||||||
|
const ProductsGrid = yield* getFirstSelectorFromDocument<HTMLDivElement>("#page-boutique .grille-produits");
|
||||||
|
|
||||||
|
return {
|
||||||
|
PageStatesRawJson,
|
||||||
|
ProductsGrid,
|
||||||
|
ShowMoreButton,
|
||||||
|
};
|
||||||
|
}).pipe(Effect.mapErrorEager(IncoherentDOMError.fromNoSuchElementError)),
|
||||||
|
}) {
|
||||||
|
static readonly Live = Layer.effect(this, this.make);
|
||||||
|
}
|
||||||
|
export default ShopPageElements;
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Context, Effect, Layer, pipe, Stream, SubscriptionRef } from "effect";
|
||||||
|
|
||||||
|
import ShopPageElements from "./service-elements.ts";
|
||||||
|
|
||||||
|
class ShopPageMessages extends Context.Service<ShopPageMessages>()("haikuatelier.fr/Shop/Messages", {
|
||||||
|
make: Effect.gen(function*() {
|
||||||
|
const { ShowMoreButton } = yield* ShopPageElements;
|
||||||
|
|
||||||
|
const ShowMoreButtonText = yield* SubscriptionRef.make("Add to cart");
|
||||||
|
// Const ShowMoreErrorText = yield* SubscriptionRef.make<Option.Option<string>>(Option.none());
|
||||||
|
|
||||||
|
const initShowMoreButtonUpdates = Effect.fn("initShowMoreButtonUpdates")(function*() {
|
||||||
|
return yield* pipe(
|
||||||
|
SubscriptionRef.changes(ShowMoreButtonText),
|
||||||
|
Stream.tap(newText => {
|
||||||
|
ShowMoreButton.textContent = newText;
|
||||||
|
return Effect.succeed(newText);
|
||||||
|
}),
|
||||||
|
Stream.runDrain,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ShowMoreButtonText, initShowMoreButtonUpdates };
|
||||||
|
}),
|
||||||
|
}) {
|
||||||
|
static readonly Live = Layer.effect(this, this.make);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShopPageMessages;
|
||||||
|
|
@ -2,75 +2,24 @@
|
||||||
* Scripts pour les fonctionnalités de la page Boutique.
|
* Scripts pour les fonctionnalités de la page Boutique.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { pipe } from "@mobily/ts-belt";
|
import { Effect } from "effect";
|
||||||
import { tap } from "@mobily/ts-belt/Function";
|
import ShopPageRuntime from "./page-boutique/runtime.ts";
|
||||||
import { EitherAsync } from "purify-ts";
|
import ShopPageDOM from "./page-boutique/service-dom.ts";
|
||||||
import { match, P } from "ts-pattern";
|
import ShopPageElements from "./page-boutique/service-elements.ts";
|
||||||
import { ValiError } from "valibot";
|
import ShopPageMessages from "./page-boutique/service-messages.ts";
|
||||||
|
|
||||||
import type { APIFetchErrors } from "./lib/types/api/erreurs";
|
|
||||||
import type { WCV3Products, WCV3ProductsArgs } from "./lib/types/api/v3/products.ts";
|
|
||||||
import type { GenericPageState } from "./lib/types/pages";
|
|
||||||
|
|
||||||
import { ROUTE_API_NOUVELLE_PRODUCTS } from "./constantes/api.ts";
|
|
||||||
import { PRODUCT_STATUTES } from "./constantes/api/products.ts";
|
|
||||||
import {
|
|
||||||
ATTRIBUT_CHARGEMENT,
|
|
||||||
ATTRIBUT_DESACTIVE,
|
|
||||||
ATTRIBUT_HIDDEN,
|
|
||||||
ATTRIBUT_ID_CATEGORIE_PRODUITS,
|
|
||||||
ATTRIBUT_PAGE,
|
|
||||||
DOM_BOUTON_PLUS_PRODUITS,
|
|
||||||
DOM_GRILLE_PRODUITS,
|
|
||||||
} from "./constantes/dom.ts";
|
|
||||||
import { lanceAnimationCycleLoading } from "./lib/animations.ts";
|
|
||||||
import { html, mustGetEleInDocument } from "./lib/dom.ts";
|
|
||||||
import { BadRequestError, reporteErreur, ServerError } from "./lib/erreurs.ts";
|
|
||||||
import { getBackendAvecParametresUrl, newPartialResponse } from "./lib/reseau.ts";
|
|
||||||
import { WCV3ProductsArgsSchema, WCV3ProductsSchema } from "./lib/schemas/api/v3/products.ts";
|
|
||||||
import { safeSchemaParse } from "./lib/validation.ts";
|
|
||||||
|
|
||||||
type APIProductsErrors =
|
|
||||||
| APIFetchErrors
|
|
||||||
| ValiError<typeof WCV3ProductsArgsSchema>
|
|
||||||
| ValiError<typeof WCV3ProductsSchema>;
|
|
||||||
|
|
||||||
// @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
|
|
||||||
const ETATS_PAGE: GenericPageState = _etats;
|
|
||||||
|
|
||||||
// Numéros magiques
|
|
||||||
const PRODUCTS_PER_PAGE = 12;
|
|
||||||
|
|
||||||
// Éléments d'intérêt
|
|
||||||
const E = {
|
|
||||||
BOUTON_PLUS_DE_PRODUITS: mustGetEleInDocument<HTMLButtonElement>(DOM_BOUTON_PLUS_PRODUITS),
|
|
||||||
GRILLE_PRODUITS: mustGetEleInDocument<HTMLDivElement>(DOM_GRILLE_PRODUITS),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
const initialisePageBoutique = (): void => {
|
|
||||||
/** ID de la Catégorie de Produits si la Page courante est l'Archive d'une Catégorie. */
|
|
||||||
const idCategorieProduits: null | string = E.GRILLE_PRODUITS.getAttribute(ATTRIBUT_ID_CATEGORIE_PRODUITS);
|
|
||||||
|
|
||||||
E.BOUTON_PLUS_DE_PRODUITS.addEventListener("click", (): void => {
|
|
||||||
/** Le numéro de page demandée par l'Utilisateur. */
|
|
||||||
const nouveauNumeroPage = Number(E.GRILLE_PRODUITS.getAttribute(ATTRIBUT_PAGE)) + 1;
|
|
||||||
/** Les arguments passés à la requête auprès Backend pour la nouvelle page de Produits. */
|
|
||||||
const args: WCV3ProductsArgs = {
|
|
||||||
page: nouveauNumeroPage,
|
|
||||||
per_page: PRODUCTS_PER_PAGE,
|
|
||||||
status: PRODUCT_STATUTES.PUBLISH,
|
|
||||||
// Ajoute conditionnellement la Catégorie de Produits
|
|
||||||
...(idCategorieProduits && { category: idCategorieProduits }),
|
|
||||||
};
|
|
||||||
|
|
||||||
undefined;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (): void => {
|
document.addEventListener("DOMContentLoaded", (): void => {
|
||||||
initialisePageBoutique();
|
console.debug("scripts-page-boutique");
|
||||||
|
// initialisePageBoutique();
|
||||||
|
ShopPageRuntime.runFork(Effect.gen(function*() {
|
||||||
|
const Elements = yield* ShopPageElements;
|
||||||
|
const DOM = yield* ShopPageDOM;
|
||||||
|
const Messages = yield* ShopPageMessages;
|
||||||
|
|
||||||
|
yield* Effect.all([DOM.initLoadMoreProductsOnButtonClick(), Messages.initShowMoreButtonUpdates()], {
|
||||||
|
concurrency: "unbounded",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.debug(Elements.ProductsGrid);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue