wip
This commit is contained in:
parent
ff90b05977
commit
e9f5df223e
13 changed files with 259 additions and 152 deletions
|
|
@ -24,13 +24,15 @@ return new Config()
|
||||||
'blank_line_after_namespace' => true,
|
'blank_line_after_namespace' => true,
|
||||||
'blank_lines_before_namespace' => ['min_line_breaks' => 1, 'max_line_breaks' => 2],
|
'blank_lines_before_namespace' => ['min_line_breaks' => 1, 'max_line_breaks' => 2],
|
||||||
'cast_spaces' => true,
|
'cast_spaces' => true,
|
||||||
'class_attributes_separation' => ['elements' => [
|
'class_attributes_separation' => [
|
||||||
|
'elements' => [
|
||||||
'case' => 'none',
|
'case' => 'none',
|
||||||
'const' => 'none',
|
'const' => 'none',
|
||||||
'method' => 'one',
|
'method' => 'one',
|
||||||
'property' => 'one',
|
'property' => 'one',
|
||||||
'trait_import' => 'none',
|
'trait_import' => 'none',
|
||||||
]],
|
],
|
||||||
|
],
|
||||||
'class_reference_name_casing' => true,
|
'class_reference_name_casing' => true,
|
||||||
'clean_namespace' => true,
|
'clean_namespace' => true,
|
||||||
'combine_consecutive_issets' => true,
|
'combine_consecutive_issets' => true,
|
||||||
|
|
@ -93,7 +95,8 @@ return new Config()
|
||||||
'no_trailing_comma_in_singleline' => true,
|
'no_trailing_comma_in_singleline' => true,
|
||||||
'no_trailing_whitespace_in_comment' => true,
|
'no_trailing_whitespace_in_comment' => true,
|
||||||
'no_unneeded_braces' => ['namespaces' => true],
|
'no_unneeded_braces' => ['namespaces' => true],
|
||||||
'no_unneeded_control_parentheses' => ['statements' => [
|
'no_unneeded_control_parentheses' => [
|
||||||
|
'statements' => [
|
||||||
'break',
|
'break',
|
||||||
'clone',
|
'clone',
|
||||||
'continue',
|
'continue',
|
||||||
|
|
@ -104,7 +107,8 @@ return new Config()
|
||||||
'switch_case',
|
'switch_case',
|
||||||
'yield',
|
'yield',
|
||||||
'yield_from',
|
'yield_from',
|
||||||
]],
|
],
|
||||||
|
],
|
||||||
'no_unneeded_final_method' => true,
|
'no_unneeded_final_method' => true,
|
||||||
'no_unneeded_import_alias' => true,
|
'no_unneeded_import_alias' => true,
|
||||||
'no_unreachable_default_argument_value' => true,
|
'no_unreachable_default_argument_value' => true,
|
||||||
|
|
@ -135,11 +139,9 @@ return new Config()
|
||||||
'pow_to_exponentiation' => true,
|
'pow_to_exponentiation' => true,
|
||||||
'protected_to_private' => true,
|
'protected_to_private' => true,
|
||||||
'psr_autoloading' => true,
|
'psr_autoloading' => true,
|
||||||
'random_api_migration' => ['replacements' => [
|
'random_api_migration' => [
|
||||||
'getrandmax' => 'mt_getrandmax',
|
'replacements' => ['getrandmax' => 'mt_getrandmax', 'rand' => 'mt_rand', 'srand' => 'mt_srand'],
|
||||||
'rand' => 'mt_rand',
|
],
|
||||||
'srand' => 'mt_srand',
|
|
||||||
]],
|
|
||||||
'return_assignment' => true,
|
'return_assignment' => true,
|
||||||
'self_accessor' => true,
|
'self_accessor' => true,
|
||||||
'self_static_accessor' => true,
|
'self_static_accessor' => true,
|
||||||
|
|
@ -214,7 +216,8 @@ return new Config()
|
||||||
// The type of @return annotations of methods returning a reference to itself must the configured one.
|
// The type of @return annotations of methods returning a reference to itself must the configured one.
|
||||||
'phpdoc_return_self_reference' => true,
|
'phpdoc_return_self_reference' => true,
|
||||||
// Scalar types should always be written in the same form. int not integer, bool not boolean, float not real or double.
|
// Scalar types should always be written in the same form. int not integer, bool not boolean, float not real or double.
|
||||||
'phpdoc_scalar' => ['types' => [
|
'phpdoc_scalar' => [
|
||||||
|
'types' => [
|
||||||
'boolean',
|
'boolean',
|
||||||
'callback',
|
'callback',
|
||||||
'double',
|
'double',
|
||||||
|
|
@ -224,7 +227,8 @@ return new Config()
|
||||||
'no-return',
|
'no-return',
|
||||||
'real',
|
'real',
|
||||||
'str',
|
'str',
|
||||||
]],
|
],
|
||||||
|
],
|
||||||
// Annotations in PHPDoc should be grouped together so that annotations of the same type immediately follow each other. Annotations of a different type are separated by a single blank line.
|
// Annotations in PHPDoc should be grouped together so that annotations of the same type immediately follow each other. Annotations of a different type are separated by a single blank line.
|
||||||
'phpdoc_separation' => [
|
'phpdoc_separation' => [
|
||||||
'groups' => [
|
'groups' => [
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,11 @@ em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mixins Sass */
|
/* Mixins Sass */
|
||||||
/*
|
/*
|
||||||
* Réinitialisation des styles des <button>.
|
* Réinitialisation des styles des <button>.
|
||||||
|
|
@ -339,6 +344,50 @@ button.bouton-retour-haut[data-actif] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
z-index: 999;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
place-self: center center;
|
||||||
|
padding: var(--espace-l);
|
||||||
|
opacity: 0;
|
||||||
|
background: white;
|
||||||
|
transition: display 0.3s, opacity 0.3s, overlay 0.3s;
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
}
|
||||||
|
dialog::backdrop {
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.3s, display 0.3s, overlay 0.3s;
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
}
|
||||||
|
dialog:open {
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
dialog:open::backdrop {
|
||||||
|
background-color: var(--couleur-fond);
|
||||||
|
}
|
||||||
|
dialog * + * {
|
||||||
|
margin-block-start: var(--espace-m);
|
||||||
|
}
|
||||||
|
dialog p {
|
||||||
|
max-inline-size: 50ch;
|
||||||
|
}
|
||||||
|
dialog button {
|
||||||
|
align-self: end;
|
||||||
|
inline-size: fit-content;
|
||||||
|
padding: var(--espace-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
dialog:open {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@starting-style {
|
||||||
|
dialog:open::backdrop {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
fieldset {
|
fieldset {
|
||||||
all: initial;
|
all: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -587,6 +636,15 @@ ul.avec-puce-cercle a {
|
||||||
) url("/app/themes/haiku-atelier-2024/assets/img/icons/dot.svg"); /* 2 */
|
) url("/app/themes/haiku-atelier-2024/assets/img/icons/dot.svg"); /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
display: block;
|
||||||
|
inline-size: 100%;
|
||||||
|
min-inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
min-block-size: inherit;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
/* * Styles pour un bandeau défilant. */
|
/* * Styles pour un bandeau défilant. */
|
||||||
.bandeau {
|
.bandeau {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -1537,7 +1595,6 @@ body:has(#menu-mobile:not([aria-hidden="true"])) {
|
||||||
padding: var(--espace-s) var(--espace-m);
|
padding: var(--espace-s) var(--espace-m);
|
||||||
border-top: 1px solid var(--couleur-noir);
|
border-top: 1px solid var(--couleur-noir);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background: var(--couleur-jaune);
|
|
||||||
}
|
}
|
||||||
#pied-de-page .zone-menu-navigation-secondaire {
|
#pied-de-page .zone-menu-navigation-secondaire {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -154,12 +154,14 @@ $session_checkout_stripe = Session::create([
|
||||||
'mode' => 'payment',
|
'mode' => 'payment',
|
||||||
'success_url' => $urls['succes_commande'] . '?session_id={CHECKOUT_SESSION_ID}',
|
'success_url' => $urls['succes_commande'] . '?session_id={CHECKOUT_SESSION_ID}',
|
||||||
'metadata' => ['order_id' => $order_id, 'order_key' => $order_key],
|
'metadata' => ['order_id' => $order_id, 'order_key' => $order_key],
|
||||||
'shipping_options' => [['shipping_rate_data' => [
|
'shipping_options' => [[
|
||||||
|
'shipping_rate_data' => [
|
||||||
'display_name' => $methode_livraison['nom'],
|
'display_name' => $methode_livraison['nom'],
|
||||||
'fixed_amount' => ['amount' => $methode_livraison['cout'], 'currency' => 'EUR'],
|
'fixed_amount' => ['amount' => $methode_livraison['cout'], 'currency' => 'EUR'],
|
||||||
'tax_behavior' => 'inclusive',
|
'tax_behavior' => 'inclusive',
|
||||||
'type' => 'fixed_amount',
|
'type' => 'fixed_amount',
|
||||||
]]],
|
],
|
||||||
|
]],
|
||||||
], ['idempotency_key' => Uuid::v4()]);
|
], ['idempotency_key' => Uuid::v4()]);
|
||||||
// echo json_encode($session_checkout_stripe);
|
// echo json_encode($session_checkout_stripe);
|
||||||
header('HTTP/1.1 303 See Other');
|
header('HTTP/1.1 303 See Other');
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,10 @@ em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mixins Sass */
|
/* Mixins Sass */
|
||||||
// TODO: Créer une mixin pour le style "gris avec lettres espacées"
|
// TODO: Créer une mixin pour le style "gris avec lettres espacées"
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,4 @@ class WooCommerceAPI extends Context.Service<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { APIError, WooCommerceAPI };
|
export { WooCommerceAPI };
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,35 @@
|
||||||
import { Cause, Console, Context, Effect, Layer, pipe, Schedule, Schema, SchemaIssue } from "effect";
|
import { Console, Context, Effect, Layer, Match, pipe, Schedule, Schema, SchemaIssue } from "effect";
|
||||||
|
import { SchemaError } from "effect/Schema";
|
||||||
import {
|
import {
|
||||||
FetchHttpClient,
|
FetchHttpClient,
|
||||||
HttpClient,
|
HttpClient,
|
||||||
|
HttpClientError,
|
||||||
HttpClientRequest,
|
HttpClientRequest,
|
||||||
HttpClientResponse,
|
HttpClientResponse,
|
||||||
} from "effect/unstable/http";
|
} from "effect/unstable/http";
|
||||||
|
import { HttpClientErrorSchema } from "effect/unstable/http/HttpClientError";
|
||||||
import type { CartProduct } from "../schemas/api.ts";
|
import type { CartProduct } from "../schemas/api.ts";
|
||||||
import { WooCommerceCart } from "../schemas/cart.ts";
|
import { WooCommerceCart } from "../schemas/cart.ts";
|
||||||
|
|
||||||
class BodyParsingError extends Schema.TaggedErrorClass<BodyParsingError>()("BodyParsingError", {
|
/** Le nombre maximal d'essais pour une Requête. */
|
||||||
cause: Schema.String,
|
const MAX_RETRIES = 3;
|
||||||
}) {}
|
/** Le temps d'attente avant de réessayer une Requête. */
|
||||||
class FetchError extends Schema.TaggedErrorClass<FetchError>()("FetchError", {
|
const RETRY_WAIT_TIME = "1 seconds";
|
||||||
cause: Schema.Defect,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
|
/** Décrit une Erreur survenue au traitement d'une `Request`. */
|
||||||
class APIRequestError extends Schema.TaggedErrorClass<APIRequestError>()("APIRequestError", {
|
class APIRequestError extends Schema.TaggedErrorClass<APIRequestError>()("APIRequestError", {
|
||||||
reason: Schema.Union([BodyParsingError, FetchError]),
|
message: Schema.String,
|
||||||
}) {}
|
cause: Schema.Union([Schema.Defect, HttpClientErrorSchema]),
|
||||||
|
|
||||||
class BadRequestError extends Schema.TaggedErrorClass<BadRequestError>()("BadRequestError", {
|
|
||||||
cause: Schema.Defect,
|
|
||||||
}){}
|
|
||||||
class UnauthorizedError extends Schema.TaggedErrorClass<UnauthorizedError>()("UnauthorizedError", {
|
|
||||||
cause: Schema.Defect,
|
|
||||||
}){}
|
|
||||||
class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()("ForbiddenError", {
|
|
||||||
cause: Schema.Defect,
|
|
||||||
}){}
|
|
||||||
class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", {
|
|
||||||
cause: Schema.Defect,
|
|
||||||
}){}
|
|
||||||
class ServerError extends Schema.TaggedErrorClass<ServerError>()("ServerError", {
|
|
||||||
cause: Schema.Defect,
|
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
/** Décrit une Erreur survenue au traitement d'une `Response`. */
|
||||||
class APIResponseError extends Schema.TaggedErrorClass<APIResponseError>()("APIResponseError", {
|
class APIResponseError extends Schema.TaggedErrorClass<APIResponseError>()("APIResponseError", {
|
||||||
reason: Schema.Union([BadRequestError,UnauthorizedError,ForbiddenError,NotFoundError,ServerError]),
|
message: Schema.String,
|
||||||
|
cause: Schema.Union([Schema.Defect, HttpClientErrorSchema]),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
type APIError = APIRequestError | APIResponseError;
|
||||||
|
|
||||||
/** Client `fetch` contenant les options et en-têtes de Requêtes pré-renseignées. */
|
/** Client `fetch` contenant les options et en-têtes de Requêtes pré-renseignées. */
|
||||||
const APIFetchClient = FetchHttpClient.layer.pipe(
|
const APIFetchClient = FetchHttpClient.layer.pipe(
|
||||||
Layer.provide(
|
Layer.provide(
|
||||||
|
|
@ -61,24 +52,51 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
|
||||||
// Créé un client HTTP où chaque Requête est imprimée dans la console.
|
// Créé un client HTTP où chaque Requête est imprimée dans la console.
|
||||||
const haikuHTTPClient = pipe(
|
const haikuHTTPClient = pipe(
|
||||||
yield* HttpClient.HttpClient,
|
yield* HttpClient.HttpClient,
|
||||||
// Journalise toutes les Requêtes et Réponses.
|
// Journalise toutes les Requêtes.
|
||||||
HttpClient.tapRequest(request => Console.debug("APIClient", "Request", request)),
|
HttpClient.tapRequest(request => Console.debug("APIClient", "Request", request)),
|
||||||
HttpClient.tap(response =>
|
// En cas de code HTTP indiquant un échec, générer une erreur.
|
||||||
Effect.gen(function*() {
|
HttpClient.filterStatusOk,
|
||||||
const json = yield* response.json;
|
|
||||||
yield* Console.debug("APIClient", "Response", response.status, json);
|
|
||||||
})
|
|
||||||
),
|
|
||||||
// Définis une politique d'essai.
|
// Définis une politique d'essai.
|
||||||
HttpClient.retryTransient({
|
HttpClient.retryTransient({
|
||||||
retryOn: "errors-only",
|
retryOn: "errors-only",
|
||||||
schedule: Schedule.exponential("1 seconds"),
|
schedule: Schedule.exponential(RETRY_WAIT_TIME),
|
||||||
times: 3,
|
times: MAX_RETRIES,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const matchAPIError = (error: HttpClientError.HttpClientError | SchemaError): APIError => {
|
||||||
|
if (error._tag === "SchemaError") {
|
||||||
|
return new APIRequestError({
|
||||||
|
message: `Erreur lors du parsage du corps de la Requête :${SchemaIssue.makeFormatterDefault()(error.issue)}`,
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Match.typeTags<HttpClientError.HttpClientErrorReason, APIError>()({
|
||||||
|
TransportError: (cause): APIError =>
|
||||||
|
new APIRequestError({
|
||||||
|
cause,
|
||||||
|
message: "Un problème réseau empêche l'exécution de la Requête",
|
||||||
|
}),
|
||||||
|
EncodeError: cause => new APIRequestError({ cause, message: "Le corps de la Requête ne peut être lu" }),
|
||||||
|
InvalidUrlError: cause => new APIRequestError({ cause, message: "L'URL de la Requête n'est pas valide" }),
|
||||||
|
StatusCodeError: cause =>
|
||||||
|
new APIResponseError({ cause, message: "Le code HTTP de la Réponse correspond à un échec" }),
|
||||||
|
DecodeError: cause => new APIResponseError({ cause, message: "Le corps de la Réponse ne peut être lu" }),
|
||||||
|
EmptyBodyError: cause => new APIResponseError({ cause, message: "Un corps vide ne peut être lu" }),
|
||||||
|
})(error.reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const printErrorAsSuccinctMessage = (error: APIError): Effect.Effect<APIError> =>
|
||||||
|
Effect.gen(function*() {
|
||||||
|
yield* Console.error(
|
||||||
|
`${error.name} - ${error.message} : ${error.cause?._tag ?? ""} - ${error.cause?.description ?? ""}.`,
|
||||||
|
);
|
||||||
|
return yield* Effect.succeed(error);
|
||||||
|
});
|
||||||
|
|
||||||
const AddProductToCart = Effect.fn("AppClient.AddProductToCart")(
|
const AddProductToCart = Effect.fn("AppClient.AddProductToCart")(
|
||||||
function*(nonce: string, productToAdd: CartProduct): Effect.fn.Return<WooCommerceCart, APIRequestError> {
|
function*(nonce: string, productToAdd: CartProduct): Effect.fn.Return<WooCommerceCart, APIError> {
|
||||||
const request = pipe(
|
const request = pipe(
|
||||||
HttpClientRequest.post(`/wp-json/wc/store/cart/add-item`),
|
HttpClientRequest.post(`/wp-json/wc/store/cart/add-item`),
|
||||||
HttpClientRequest.setHeader("Nonce", nonce),
|
HttpClientRequest.setHeader("Nonce", nonce),
|
||||||
|
|
@ -88,23 +106,9 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
|
||||||
|
|
||||||
const response = yield* pipe(
|
const response = yield* pipe(
|
||||||
haikuHTTPClient.execute(request),
|
haikuHTTPClient.execute(request),
|
||||||
// TODO: Remplacer Schema.Unknown par un Schéma de l'objet retourné par le backend.
|
|
||||||
Effect.flatMap(HttpClientResponse.schemaBodyJson(WooCommerceCart)),
|
Effect.flatMap(HttpClientResponse.schemaBodyJson(WooCommerceCart)),
|
||||||
Effect.mapError(error => {
|
Effect.mapError(error => matchAPIError(error)),
|
||||||
if (error._tag === "SchemaError") {
|
Effect.tapError(error => printErrorAsSuccinctMessage(error)),
|
||||||
return new APIRequestError({
|
|
||||||
reason: new BodyParsingError({ cause: SchemaIssue.makeFormatterDefault()(error.issue) }),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new APIRequestError({ reason: new FetchError({ cause: error.reason }) });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Effect.tapError(error => {
|
|
||||||
error.stack = "";
|
|
||||||
error.reason.stack = "";
|
|
||||||
console.error(error._tag, error.name, error.message, error.reason, error.cause);
|
|
||||||
return Effect.succeed(error);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -117,4 +121,5 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
|
||||||
static readonly Live = Layer.effect(this, this.make).pipe(Layer.provide(APIFetchClient));
|
static readonly Live = Layer.effect(this, this.make).pipe(Layer.provide(APIFetchClient));
|
||||||
}
|
}
|
||||||
|
|
||||||
export { APIClient, APIFetchClient, APIRequestError };
|
export { APIClient, APIFetchClient, APIRequestError, APIResponseError };
|
||||||
|
export type { APIError };
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import { Console, Layer, ManagedRuntime, pipe } from "effect";
|
||||||
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||||
import ProductPageDOM from "./service-dom.ts";
|
import ProductPageDOM from "./service-dom.ts";
|
||||||
import ProductPageElements from "./service-elements.ts";
|
import ProductPageElements from "./service-elements.ts";
|
||||||
|
import ProductPageMessages from "./service-messages.ts";
|
||||||
|
|
||||||
const ProductPageRuntime = ManagedRuntime.make(
|
const ProductPageRuntime = ManagedRuntime.make(
|
||||||
pipe(
|
pipe(
|
||||||
ProductPageDOM.layer,
|
ProductPageDOM.Live,
|
||||||
|
Layer.provideMerge(ProductPageMessages.Live),
|
||||||
Layer.provide(ProductPageElements.Live),
|
Layer.provide(ProductPageElements.Live),
|
||||||
Layer.provide(APIClient.Live),
|
Layer.provide(APIClient.Live),
|
||||||
Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)),
|
Layer.tapError(error => Console.error("ProductPageRuntime", "Impossible de créer le Layer :", error)),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// oxlint-disable typescript/dot-notation
|
// oxlint-disable typescript/dot-notation
|
||||||
import {
|
import {
|
||||||
Array as FxArray,
|
Array as FxArray,
|
||||||
Cause,
|
|
||||||
Console,
|
Console,
|
||||||
Context,
|
Context,
|
||||||
Effect,
|
Effect,
|
||||||
|
|
@ -13,11 +12,11 @@ import {
|
||||||
Schema,
|
Schema,
|
||||||
SchemaIssue,
|
SchemaIssue,
|
||||||
Stream,
|
Stream,
|
||||||
|
SubscriptionRef,
|
||||||
} from "effect";
|
} from "effect";
|
||||||
import type { NoSuchElementError } from "effect/Cause";
|
|
||||||
import type { SchemaError } from "effect/Schema";
|
import type { SchemaError } from "effect/Schema";
|
||||||
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
import { APIClient } from "../../scripts-effect/lib/api.ts";
|
||||||
import type { APIRequestError } from "../../scripts-effect/lib/api.ts";
|
import type { APIError } from "../../scripts-effect/lib/api.ts";
|
||||||
import { CartProduct } from "../../scripts-effect/schemas/api.ts";
|
import { CartProduct } from "../../scripts-effect/schemas/api.ts";
|
||||||
import { WooCommerceCart } from "../../scripts-effect/schemas/cart.ts";
|
import { WooCommerceCart } from "../../scripts-effect/schemas/cart.ts";
|
||||||
import { Product, ProductVariation, ProductVariationAttribute } from "../../scripts-effect/schemas/product.ts";
|
import { Product, ProductVariation, ProductVariationAttribute } from "../../scripts-effect/schemas/product.ts";
|
||||||
|
|
@ -28,10 +27,10 @@ import {
|
||||||
ATTRIBUT_DESACTIVE,
|
ATTRIBUT_DESACTIVE,
|
||||||
ATTRIBUT_HIDDEN,
|
ATTRIBUT_HIDDEN,
|
||||||
} from "../constantes/dom.ts";
|
} from "../constantes/dom.ts";
|
||||||
import { lanceAnimationCycleLoading } from "../lib/animations.ts";
|
|
||||||
import { emetMessageMajBoutonPanier } from "../lib/messages.ts";
|
import { emetMessageMajBoutonPanier } from "../lib/messages.ts";
|
||||||
import { IncoherentDOMError } from "./errors.ts";
|
import { IncoherentDOMError } from "./errors.ts";
|
||||||
import ProductPageElements from "./service-elements.ts";
|
import ProductPageElements from "./service-elements.ts";
|
||||||
|
import ProductPageMessages from "./service-messages.ts";
|
||||||
import type { DetailEnsemble } from "./types.d.ts";
|
import type { DetailEnsemble } from "./types.d.ts";
|
||||||
|
|
||||||
const PageStatesSchema = Schema.Struct({
|
const PageStatesSchema = Schema.Struct({
|
||||||
|
|
@ -48,36 +47,10 @@ class InvalidPageStateError extends Schema.TaggedErrorClass<InvalidPageStateErro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProductPageDOM extends Context.Service<
|
class ProductPageDOM extends Context.Service<ProductPageDOM>()(
|
||||||
ProductPageDOM,
|
"haikuatelier.fr/Produit/ProductPageDOM",
|
||||||
{
|
{
|
||||||
/**
|
make: Effect.gen(function*() {
|
||||||
* Initialise l'état initial du Bouton d'ajout au Panier.
|
|
||||||
*/
|
|
||||||
initAddToCartButtonInitialState: () => Effect.Effect<void>;
|
|
||||||
/**
|
|
||||||
* Initialise les mises à jour du Bouton d'ajout au Panier en fonction des interactions de l'Utilisateur.
|
|
||||||
*/
|
|
||||||
initAddToCartButtonUpdates: () => Effect.Effect<void>;
|
|
||||||
/**
|
|
||||||
* Initialise les interactions des Sections de la Description du Produit.
|
|
||||||
*/
|
|
||||||
initDetailInteractions: () => Effect.Effect<void, NoSuchElementError>;
|
|
||||||
/*
|
|
||||||
* Initialise la mise à jour du Prix affiché en fonction du choix de la Varation de Produit.
|
|
||||||
*/
|
|
||||||
initPriceUpdatesOnVariationChange: () => Effect.Effect<void, Error>;
|
|
||||||
/**
|
|
||||||
* Replie toutes les sections de la description du Produit.
|
|
||||||
*/
|
|
||||||
initAddToCartButtonClicks: () => unknown;
|
|
||||||
PageStates: typeof PageStatesSchema.Type;
|
|
||||||
CurrentVariation: Ref.Ref<Option.Option<ProductVariation>>;
|
|
||||||
}
|
|
||||||
>()("haikuatelier.fr/Produit/ProductPageDOM") {
|
|
||||||
static readonly layer = Layer.effect(
|
|
||||||
ProductPageDOM,
|
|
||||||
Effect.gen(function*() {
|
|
||||||
const {
|
const {
|
||||||
AddToCartButton,
|
AddToCartButton,
|
||||||
Details,
|
Details,
|
||||||
|
|
@ -87,6 +60,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
VariationSelectors,
|
VariationSelectors,
|
||||||
PageStatesRawJson,
|
PageStatesRawJson,
|
||||||
} = yield* ProductPageElements;
|
} = yield* ProductPageElements;
|
||||||
|
const { AddToCartButtonText } = yield* ProductPageMessages;
|
||||||
const API = yield* APIClient;
|
const API = yield* APIClient;
|
||||||
|
|
||||||
const PageStates = yield* pipe(
|
const PageStates = yield* pipe(
|
||||||
|
|
@ -98,7 +72,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
|
|
||||||
const CurrentProduct = yield* Ref.make(Option.none<ProductVariation>());
|
const CurrentProduct = yield* Ref.make(Option.none<ProductVariation>());
|
||||||
|
|
||||||
const onFormChangeHandler = Effect.fn("onFormChangeHandler")(function*(evt: Event) {
|
const formChangeHandler = Effect.fn("formChangeHandler")(function*(evt: Event) {
|
||||||
// La cible ne peut qu'être un Formulaire.
|
// La cible ne peut qu'être un Formulaire.
|
||||||
const target: HTMLFormElement = evt.target as HTMLFormElement;
|
const target: HTMLFormElement = evt.target as HTMLFormElement;
|
||||||
const isClickAllowed = target.checkValidity() === false;
|
const isClickAllowed = target.checkValidity() === false;
|
||||||
|
|
@ -121,7 +95,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDetailButtonClickHandler = Effect.fn("onDetailButtonClickHandler")(
|
const detailButtonClickHandler = Effect.fn("detailButtonClickHandler")(
|
||||||
function*(evt: Event) {
|
function*(evt: Event) {
|
||||||
// Empêche la pollution de l'historique de navigation
|
// Empêche la pollution de l'historique de navigation
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
@ -176,17 +150,17 @@ class ProductPageDOM extends Context.Service<
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onVariationChangeHandler = Effect.fn("onVariationChangeHandler")(function*() {
|
const variationChangeHandler = Effect.fn("variationChangeHandler")(function*() {
|
||||||
yield* Console.log("onVariationChangeHandler");
|
yield* Console.log("variationChangeHandler");
|
||||||
// Ne fais rien si le Formulaire n'est pas valide.
|
// Ne fais rien si le Formulaire n'est pas valide.
|
||||||
if (VariationChoiceForm.checkValidity() === false) {
|
if (VariationChoiceForm.checkValidity() === false) {
|
||||||
yield* Console.debug("onVariationChangeHandler", "Le formulaire est invalide.");
|
yield* Console.debug("variationChangeHandler", "Le formulaire est invalide.");
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const equivalence = Schema.toEquivalence(Schema.Array(ProductVariationAttribute));
|
const equivalence = Schema.toEquivalence(Schema.Array(ProductVariationAttribute));
|
||||||
const chosenProductAttributes = yield* getChosenProductAttributesFromDOM();
|
const chosenProductAttributes = yield* getChosenProductAttributesFromDOM();
|
||||||
yield* Console.debug("onVariationChangeHandler", "chosenProductAttributes", chosenProductAttributes);
|
yield* Console.debug("variationChangeHandler", "chosenProductAttributes", chosenProductAttributes);
|
||||||
const chosenVariation = yield* pipe(
|
const chosenVariation = yield* pipe(
|
||||||
FxArray.findFirst(
|
FxArray.findFirst(
|
||||||
PageStates.product.variations,
|
PageStates.product.variations,
|
||||||
|
|
@ -195,7 +169,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
Effect.fromOption,
|
Effect.fromOption,
|
||||||
Effect.mapError(error => new Error("Impossible de trouver la variation demandée.", { cause: error })),
|
Effect.mapError(error => new Error("Impossible de trouver la variation demandée.", { cause: error })),
|
||||||
);
|
);
|
||||||
yield* Console.debug("onVariationChangeHandler", "chosenVariation", chosenVariation);
|
yield* Console.debug("variationChangeHandler", "chosenVariation", chosenVariation);
|
||||||
|
|
||||||
// Met à jour la valeur de la Variation choisie dans le Service.
|
// Met à jour la valeur de la Variation choisie dans le Service.
|
||||||
yield* Ref.set(Option.some(chosenVariation))(CurrentProduct);
|
yield* Ref.set(Option.some(chosenVariation))(CurrentProduct);
|
||||||
|
|
@ -207,8 +181,18 @@ class ProductPageDOM extends Context.Service<
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Faire une véritable gestion des erreurs.
|
// TODO: Faire une véritable gestion des erreurs.
|
||||||
const recoverFromBackendFailure = Effect.fn("recoverFromBackendFailure")(function*(_error: APIRequestError) {
|
const recoverFromBackendFailure = Effect.fn("recoverFromBackendFailure")(function*(error: APIError) {
|
||||||
AddToCartButton.textContent = "Error while adding the Product to the Cart...";
|
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, false);
|
||||||
|
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, false);
|
||||||
|
|
||||||
|
// Affiche un message d'erreur sommaire à l'utilisateur puis réinitialise le texte.
|
||||||
|
yield* SubscriptionRef.set(AddToCartButtonText, "Error!");
|
||||||
|
|
||||||
|
document.querySelector("#cart-error").showModal();
|
||||||
|
document.querySelector("#cart-error code").textContent = `${error.name} ${error.message}`;
|
||||||
|
|
||||||
|
yield* SubscriptionRef.set(AddToCartButtonText, "Add to cart").pipe(Effect.delay("5 seconds"));
|
||||||
|
|
||||||
return yield* Effect.void;
|
return yield* Effect.void;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -241,7 +225,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
// Désactive les interactions le temps de la requête.
|
// Désactive les interactions le temps de la requête.
|
||||||
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, true);
|
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, true);
|
||||||
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, true);
|
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, true);
|
||||||
lanceAnimationCycleLoading(AddToCartButton, 500);
|
// lanceAnimationCycleLoading(AddToCartButton, 500);
|
||||||
|
|
||||||
// Exécute la Requête auprès du backend.
|
// Exécute la Requête auprès du backend.
|
||||||
const newCart = yield* API.AddProductToCart(PageStates.nonce, requestBody);
|
const newCart = yield* API.AddProductToCart(PageStates.nonce, requestBody);
|
||||||
|
|
@ -252,9 +236,12 @@ class ProductPageDOM extends Context.Service<
|
||||||
|
|
||||||
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, false);
|
AddToCartButton.toggleAttribute(ATTRIBUT_DESACTIVE, false);
|
||||||
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, false);
|
AddToCartButton.toggleAttribute(ATTRIBUT_CHARGEMENT, false);
|
||||||
AddToCartButton.textContent = "Add to cart";
|
yield* SubscriptionRef.set(AddToCartButtonText, "Add to cart");
|
||||||
},
|
},
|
||||||
Effect.catchTag("APIRequestError", recoverFromBackendFailure),
|
Effect.catchTags({
|
||||||
|
APIResponseError: error => recoverFromBackendFailure(error),
|
||||||
|
APIRequestError: error => recoverFromBackendFailure(error),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const initAddToCartButtonInitialState = Effect.fn("initAddToCartButtonInitialState")(function*() {
|
const initAddToCartButtonInitialState = Effect.fn("initAddToCartButtonInitialState")(function*() {
|
||||||
|
|
@ -278,7 +265,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
function*(): Effect.fn.Return<void> {
|
function*(): Effect.fn.Return<void> {
|
||||||
return yield* pipe(
|
return yield* pipe(
|
||||||
Stream.fromEventListener(VariationChoiceForm, "change"),
|
Stream.fromEventListener(VariationChoiceForm, "change"),
|
||||||
Stream.tap(onFormChangeHandler),
|
Stream.tap(formChangeHandler),
|
||||||
Stream.runDrain,
|
Stream.runDrain,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -295,7 +282,7 @@ class ProductPageDOM extends Context.Service<
|
||||||
const initPriceUpdatesOnVariationChange = Effect.fn("initPriceUpdatesOnVariationChange")(function*() {
|
const initPriceUpdatesOnVariationChange = Effect.fn("initPriceUpdatesOnVariationChange")(function*() {
|
||||||
return yield* pipe(
|
return yield* pipe(
|
||||||
Stream.fromEventListener(VariationChoiceForm, "change"),
|
Stream.fromEventListener(VariationChoiceForm, "change"),
|
||||||
Stream.tap(onVariationChangeHandler),
|
Stream.tap(variationChangeHandler),
|
||||||
Stream.runDrain,
|
Stream.runDrain,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -306,14 +293,14 @@ class ProductPageDOM extends Context.Service<
|
||||||
FxArray.map(
|
FxArray.map(
|
||||||
DetailsButtons,
|
DetailsButtons,
|
||||||
(button: HTMLButtonElement) =>
|
(button: HTMLButtonElement) =>
|
||||||
pipe(Stream.fromEventListener(button, "click"), Stream.tap(onDetailButtonClickHandler)),
|
pipe(Stream.fromEventListener(button, "click"), Stream.tap(detailButtonClickHandler)),
|
||||||
),
|
),
|
||||||
Stream.mergeAll({ concurrency: "unbounded" }),
|
Stream.mergeAll({ concurrency: "unbounded" }),
|
||||||
Stream.runDrain,
|
Stream.runDrain,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return ProductPageDOM.of({
|
return {
|
||||||
CurrentVariation: CurrentProduct,
|
CurrentVariation: CurrentProduct,
|
||||||
initAddToCartButtonClicks,
|
initAddToCartButtonClicks,
|
||||||
initAddToCartButtonInitialState,
|
initAddToCartButtonInitialState,
|
||||||
|
|
@ -321,9 +308,11 @@ class ProductPageDOM extends Context.Service<
|
||||||
initDetailInteractions,
|
initDetailInteractions,
|
||||||
initPriceUpdatesOnVariationChange,
|
initPriceUpdatesOnVariationChange,
|
||||||
PageStates,
|
PageStates,
|
||||||
});
|
};
|
||||||
}),
|
}),
|
||||||
);
|
},
|
||||||
|
) {
|
||||||
|
static readonly Live = Layer.effect(this, this.make);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductPageDOM;
|
export default ProductPageDOM;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Context, Effect, Layer, pipe, Stream, SubscriptionRef } from "effect";
|
||||||
|
import ProductPageElements from "./service-elements.ts";
|
||||||
|
|
||||||
|
class ProductPageMessages extends Context.Service<ProductPageMessages>()("haikuatelier.fr/Product/Messages", {
|
||||||
|
make: Effect.gen(function*() {
|
||||||
|
const { AddToCartButton } = yield* ProductPageElements;
|
||||||
|
|
||||||
|
const AddToCartButtonText = yield* SubscriptionRef.make("Add to cart");
|
||||||
|
// const AddToCartErrorText = yield* SubscriptionRef.make<Option.Option<string>>(Option.none());
|
||||||
|
|
||||||
|
const initAddToCartButtonUpdates = Effect.fn("initAddToCartButtonUpdates")(function*() {
|
||||||
|
return yield* pipe(
|
||||||
|
SubscriptionRef.changes(AddToCartButtonText),
|
||||||
|
Stream.tap(newText => {
|
||||||
|
AddToCartButton.textContent = newText;
|
||||||
|
return Effect.succeed(newText);
|
||||||
|
}),
|
||||||
|
Stream.runDrain,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { AddToCartButtonText, initAddToCartButtonUpdates };
|
||||||
|
}),
|
||||||
|
}) {
|
||||||
|
static readonly Live = Layer.effect(this, this.make);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductPageMessages;
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
// Scripts pour la Page Produit
|
// Scripts pour la Page Produit
|
||||||
|
|
||||||
import { Console, Effect } from "effect";
|
import { Cause, Console, Effect } from "effect";
|
||||||
import ProductPageRuntime from "./page-produit/runtime.ts";
|
import ProductPageRuntime from "./page-produit/runtime.ts";
|
||||||
import ProductPageDOM from "./page-produit/service-dom.ts";
|
import ProductPageDOM from "./page-produit/service-dom.ts";
|
||||||
|
import ProductPageMessages from "./page-produit/service-messages.ts";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (): void => {
|
document.addEventListener("DOMContentLoaded", (): void => {
|
||||||
Effect.gen(function*() {
|
Effect.gen(function*() {
|
||||||
const DOM = yield* ProductPageDOM;
|
const DOM = yield* ProductPageDOM;
|
||||||
|
const Messages = yield* ProductPageMessages;
|
||||||
|
|
||||||
const effects = Effect.all(
|
const effects = Effect.all(
|
||||||
[
|
[
|
||||||
|
|
@ -15,14 +17,19 @@ document.addEventListener("DOMContentLoaded", (): void => {
|
||||||
DOM.initAddToCartButtonClicks(),
|
DOM.initAddToCartButtonClicks(),
|
||||||
DOM.initDetailInteractions(),
|
DOM.initDetailInteractions(),
|
||||||
DOM.initPriceUpdatesOnVariationChange(),
|
DOM.initPriceUpdatesOnVariationChange(),
|
||||||
|
Messages.initAddToCartButtonUpdates(),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
concurrency: "unbounded",
|
concurrency: "unbounded",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
yield* effects.pipe(Effect.tapCause(Console.error));
|
yield* effects.pipe(Effect.tapCause(cause => Console.error(Cause.pretty(cause))));
|
||||||
}).pipe(ProductPageRuntime.runFork);
|
}).pipe(ProductPageRuntime.runFork);
|
||||||
|
|
||||||
|
document.querySelector("#cart-error button").addEventListener("click", (): void => {
|
||||||
|
document.querySelector("#cart-error").close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// const ajouteProduitAuPanier = (event: MouseEvent): void => {
|
// const ajouteProduitAuPanier = (event: MouseEvent): void => {
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<dialog id="cart-error">
|
||||||
|
<p>An error happened! Please retry.</p>
|
||||||
|
<code></code>
|
||||||
|
<p>If that happens again, please contact us with the content or a capture of the error.</p>
|
||||||
|
|
||||||
|
<button class="bouton-blanc-sur-noir" autofocus>Close</button>
|
||||||
|
</dialog>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue