ref(boutique) wip scripts sous forme Effect
This commit is contained in:
parent
498ae877a1
commit
56af75707f
8 changed files with 334 additions and 77 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Console, Context, Effect, Layer, Match, pipe, Schedule, Schema, SchemaIssue } from "effect";
|
||||
import { Console, Context, Effect, Layer, Match, pipe, References, Schedule, Schema, SchemaIssue } from "effect";
|
||||
import type { SchemaError } from "effect/Schema";
|
||||
import type {
|
||||
HttpClientError,
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "effect/unstable/http";
|
||||
import { HttpClientErrorSchema } from "effect/unstable/http/HttpClientError";
|
||||
import type { CartProduct, GetProducts } from "../schemas/api.ts";
|
||||
import { Product } from "../schemas/api.ts";
|
||||
import { WooCommerceCart } from "../schemas/cart.ts";
|
||||
|
||||
/** Le nombre maximal d'essais pour une Requête. */
|
||||
|
|
@ -52,7 +53,7 @@ const APIFetchClient = FetchHttpClient.layer.pipe(
|
|||
Layer.succeed(
|
||||
FetchHttpClient.RequestInit,
|
||||
{
|
||||
credentials: "same-origin",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -135,18 +136,22 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
|
|||
);
|
||||
|
||||
const GetProducts = Effect.fn("APIClient.GetProducts")(
|
||||
function*(nonce: string, authString: string, queryParams: GetProducts) {
|
||||
function*(nonce: string, queryParams: GetProducts) {
|
||||
const request = pipe(
|
||||
HttpClientRequest.get(`/wp-json/wc/store/products`),
|
||||
HttpClientRequest.get(`/wp-json/wc/v3/products`),
|
||||
HttpClientRequest.setHeader("Nonce", nonce),
|
||||
HttpClientRequest.bearerToken(authString),
|
||||
// TODO: Utiliser l'environnement
|
||||
HttpClientRequest.basicAuth(
|
||||
"ck_ed966a2265099a6dfe9915db692cbd2450cceed6",
|
||||
"cs_a046c91647af95188a3e39a736ebe02f2024e430",
|
||||
),
|
||||
// Le corps de la Requête a été validée en amont, on peut utiliser Unsafe.
|
||||
HttpClientRequest.setUrlParams(queryParams),
|
||||
);
|
||||
|
||||
const response = yield* pipe(
|
||||
haikuHTTPClient.execute(request),
|
||||
Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Unknown)),
|
||||
Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Array(Product))),
|
||||
Effect.mapError(error => matchAPIError(error)),
|
||||
Effect.tapError(error => printErrorAsSuccinctMessage(error)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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")({
|
||||
id: ProductId,
|
||||
|
|
@ -7,4 +7,58 @@ class CartProduct extends Schema.Class<CartProduct>("CartProduct")({
|
|||
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,
|
||||
}) {}
|
||||
|
||||
class Product extends Schema.Class<Product>("Product")({
|
||||
attributes: Schema.Unknown,
|
||||
brands: Schema.Unknown,
|
||||
// TODO: Pourrait être une énumération.
|
||||
catalog_visibility: Schema.String,
|
||||
categories: Schema.Unknown,
|
||||
description: Schema.String,
|
||||
dimensions: Schema.Unknown,
|
||||
featured: Schema.Boolean,
|
||||
grouped_products: Schema.Unknown,
|
||||
has_options: Schema.Boolean,
|
||||
id: Schema.Int,
|
||||
// NOTE: Non-standard, injecté dans la Réponse.
|
||||
image_repos: Schema.String,
|
||||
// NOTE: Non-standard, injecté dans la Réponse.
|
||||
image_survol: Schema.String,
|
||||
images: Schema.Unknown,
|
||||
low_stock_amount: Schema.Union([Schema.Number, Schema.Null]),
|
||||
menu_order: Schema.Int,
|
||||
meta_data: Schema.Unknown,
|
||||
name: Schema.String,
|
||||
on_sale: Schema.Boolean,
|
||||
parent_id: Schema.Int,
|
||||
permalink: Schema.URLFromString,
|
||||
price: Schema.String,
|
||||
// NOTE: Non-standard, injecté dans la Réponse.
|
||||
prix_maximal: Schema.String,
|
||||
regular_price: Schema.String,
|
||||
sale_price: Schema.String,
|
||||
short_description: Schema.String,
|
||||
sku: Schema.String,
|
||||
slug: Schema.String,
|
||||
sold_individually: Schema.Boolean,
|
||||
stock_quantity: Schema.Union([Schema.Int, Schema.Null]),
|
||||
// TODO: Pourrait être une énumération.
|
||||
stock_status: Schema.String,
|
||||
tags: Schema.Unknown,
|
||||
type: Schema.Literals(["external", "grouped", "simple", "variable"]),
|
||||
variations: Schema.Array(Schema.Int),
|
||||
virtual: Schema.Boolean,
|
||||
weight: Schema.String,
|
||||
}) {}
|
||||
|
||||
export { CartProduct, GetProducts, Product };
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import { Effect, Option, pipe, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
||||
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. */
|
||||
const ProductId = Schema.Int.pipe(Schema.brand("ProductId")).check(Schema.isGreaterThan(0));
|
||||
|
||||
|
|
@ -78,6 +80,7 @@ export {
|
|||
ProductId,
|
||||
ProductQuantity,
|
||||
ProductQuantityFromString,
|
||||
ProductStatus,
|
||||
ProductVariation,
|
||||
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,174 @@
|
|||
import {
|
||||
Array as FxArray,
|
||||
Console,
|
||||
Context,
|
||||
Effect,
|
||||
Layer,
|
||||
Option,
|
||||
pipe,
|
||||
Ref,
|
||||
Schema,
|
||||
SchemaIssue,
|
||||
Stream,
|
||||
SubscriptionRef,
|
||||
} from "effect";
|
||||
import { SchemaError } from "effect/Schema";
|
||||
import html from "html-template-tag";
|
||||
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||
import { setLoadingState } from "../../scripts-effect/lib/elements.ts";
|
||||
import { GetProducts, Product } from "../../scripts-effect/schemas/api.ts";
|
||||
import { ATTRIBUT_HIDDEN, ATTRIBUT_ID_CATEGORIE_PRODUITS, ATTRIBUT_PAGE } from "../constantes/dom.ts";
|
||||
import ShopPageElements from "./service-elements.ts";
|
||||
import ShopPageMessages from "./service-messages.ts";
|
||||
|
||||
/** Le nombre de Produits à afficher par « page ». */
|
||||
const PRODUCTS_PER_PAGE = 18;
|
||||
|
||||
/** Forme attendue des données injectées dans la page sous forme de JSON. */
|
||||
class PageStates extends Schema.Opaque<PageStates>()(
|
||||
Schema.Struct({
|
||||
authString: Schema.NonEmptyString,
|
||||
nonce: Schema.NonEmptyString,
|
||||
}),
|
||||
) {}
|
||||
|
||||
/** Représente une Erreur liée à un état de page invalide ou incohérent empêchant la poursuite des interactions/de la navigation. */
|
||||
class InvalidShopPageStateError
|
||||
extends Schema.TaggedErrorClass<InvalidShopPageStateError>()("InvalidShopPageStateError", {
|
||||
cause: Schema.String,
|
||||
})
|
||||
{
|
||||
/** Créé une `InvalidShopPageStateError` depuis une `SchemaError` levée suite à une validation. */
|
||||
static readonly fromSchemaError = (schemaError: SchemaError): InvalidShopPageStateError =>
|
||||
new InvalidShopPageStateError({
|
||||
cause: SchemaIssue.makeFormatterDefault()(schemaError.issue),
|
||||
});
|
||||
}
|
||||
|
||||
class ShopPageDOM extends Context.Service<ShopPageDOM>()("haikuatelier.fr/Shop/ShopPageDOM", {
|
||||
make: Effect.gen(function*() {
|
||||
const { PageStatesRawJson, ProductsGrid, ShowMoreButton } = yield* ShopPageElements;
|
||||
const { ShowMoreButtonText } = yield* ShopPageMessages;
|
||||
const API = yield* APIClient;
|
||||
|
||||
const { authString, nonce } = yield* pipe(
|
||||
PageStatesRawJson.textContent,
|
||||
(textContent: string) =>
|
||||
Schema.decodeUnknownEffect(Schema.fromJsonString(PageStates))(textContent, { errors: "all" }),
|
||||
Effect.mapError(InvalidShopPageStateError.fromSchemaError),
|
||||
);
|
||||
|
||||
/** ID de la Catégorie des Produits de la Page, si la Page courante est une Archive. */
|
||||
const ProductsCategoryId = yield* pipe(
|
||||
ProductsGrid.getAttribute(ATTRIBUT_ID_CATEGORIE_PRODUITS),
|
||||
Number,
|
||||
Option.fromNullishOr,
|
||||
Ref.make,
|
||||
);
|
||||
|
||||
// TODO: Créer une SubscriptionRef mettant à jour le DOM au changement de valeur.
|
||||
const PageNumber = yield* Ref.make(1);
|
||||
|
||||
/**
|
||||
* Créé le `DOM` d'une Carte de Produit sous forme de `<article>`.
|
||||
*/
|
||||
const createProductDOM = (product: Product): HTMLElement => {
|
||||
const article = document.createElement("article");
|
||||
article.classList.add("produit");
|
||||
article.innerHTML = html`<figure>
|
||||
<a href="/product/${product.slug}">
|
||||
<picture class="produit__illustration produit__illustration__principale">
|
||||
$${product.image_repos}
|
||||
</picture>
|
||||
|
||||
<picture class="produit__illustration produit__illustration__survol">
|
||||
$${product.image_survol}
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<figcaption class="produit__textuel">
|
||||
<h3 class="produit__textuel__titre">
|
||||
<a href="$${product.permalink.toString()}">${product.name}</a>
|
||||
</h3>
|
||||
<p class="produit__textuel__prix">
|
||||
${product.prix_maximal}€
|
||||
</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`;
|
||||
return article;
|
||||
};
|
||||
|
||||
/**
|
||||
* Créé le `DOM` des Cartes d'une nouvelle page de Produits sous forme d'un `DocumentFragment`.
|
||||
*/
|
||||
const createNewPageDOM = (products: ReadonlyArray<Product>) => {
|
||||
const fragment: DocumentFragment = document.createDocumentFragment();
|
||||
|
||||
// Ajoute le HTML des Produits au fragment.
|
||||
pipe(
|
||||
FxArray.take(products, PRODUCTS_PER_PAGE),
|
||||
FxArray.forEach(product => {
|
||||
const productHTML = createProductDOM(product);
|
||||
fragment.append(productHTML);
|
||||
}),
|
||||
);
|
||||
|
||||
return fragment;
|
||||
};
|
||||
|
||||
const onMoreProductsWantedHandler = Effect.fn("onMoreProductsWantedHandler")(function*() {
|
||||
yield* Console.debug("onMoreProductsWantedHandler");
|
||||
|
||||
/** Le numéro de page souhaitée. */
|
||||
const newPageNumber = yield* Ref.updateAndGet(PageNumber, pageNumber => pageNumber + 1);
|
||||
/** L'ID de la Catégorie de Produits affichée dans la page si elle existe. */
|
||||
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(ShowMoreButton, true);
|
||||
yield* SubscriptionRef.set(ShowMoreButtonText, "Getting Products...");
|
||||
|
||||
const newProducts = yield* API.GetProducts(nonce, requestBody);
|
||||
yield* Console.debug("onMoreProductsWantedHandler", newProducts);
|
||||
|
||||
// Rétablis le texte du Bouton et réactive les interactions.
|
||||
yield* SubscriptionRef.set(ShowMoreButtonText, "Show more");
|
||||
yield* setLoadingState(ShowMoreButton, false);
|
||||
|
||||
// Cache le bouton s'il y a moins de Produits disponibles que PRODUCTS_PER_PAGE (que l'on est donc à la dernière page).
|
||||
ShowMoreButton.toggleAttribute(ATTRIBUT_HIDDEN, newProducts.length < PRODUCTS_PER_PAGE);
|
||||
|
||||
// Ajoute les nouveaux Produits dans le DOM.
|
||||
const newProductsFragment = createNewPageDOM(newProducts);
|
||||
ProductsGrid.append(newProductsFragment);
|
||||
ProductsGrid.setAttribute(ATTRIBUT_PAGE, String(newPageNumber));
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialise l'écouteur d'événements de clic sur le bouton de chargement d'une nouvelle page de Produits (« Show more »).
|
||||
*/
|
||||
const initMoreProductsOnButtonClick = Effect.fn("initMoreProductsOnButtonClick")(function*() {
|
||||
return yield* pipe(
|
||||
Stream.fromEventListener(ShowMoreButton, "click"),
|
||||
Stream.tap(onMoreProductsWantedHandler),
|
||||
Stream.runDrain,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
ProductsCategoryId,
|
||||
initMoreProductsOnButtonClick,
|
||||
};
|
||||
}),
|
||||
}) {
|
||||
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("Show more");
|
||||
// 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.
|
||||
*/
|
||||
|
||||
import { pipe } from "@mobily/ts-belt";
|
||||
import { tap } from "@mobily/ts-belt/Function";
|
||||
import { EitherAsync } from "purify-ts";
|
||||
import { match, P } from "ts-pattern";
|
||||
import { ValiError } from "valibot";
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
import { Console, Effect } from "effect";
|
||||
import ShopPageRuntime from "./page-boutique/runtime.ts";
|
||||
import ShopPageDOM from "./page-boutique/service-dom.ts";
|
||||
import ShopPageElements from "./page-boutique/service-elements.ts";
|
||||
import ShopPageMessages from "./page-boutique/service-messages.ts";
|
||||
|
||||
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",
|
||||
}).pipe(Effect.tapCause(Console.error));
|
||||
|
||||
console.debug(Elements.ProductsGrid);
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue