Compare commits

..

10 commits

28 changed files with 6844 additions and 1992 deletions

View file

@ -15,6 +15,21 @@
"..." "..."
], ],
"lsp": { "lsp": {
"css-variables": {
"settings": {
"cssVariables": {
"blacklistFolders": [
"**/*.min.css",
"**/dist/**",
"**/node_modules/**"
],
"lookupFiles": [
"**/*.scss"
],
"undefinedVarFallback": "info"
}
}
},
"oxlint": { "oxlint": {
"initialization_options": { "initialization_options": {
"settings": { "settings": {

6424
aube-lock.yaml Normal file

File diff suppressed because it is too large Load diff

6
aube-workspace.yaml Normal file
View file

@ -0,0 +1,6 @@
jailBuilds: false
nodeLinker: isolated
packageManagerStrict: true
packageManagerStrictVersion: true
trustPolicy: off
useBetaCli: true

1788
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,8 @@ import type { OxlintConfig } from "oxlint";
const config: OxlintConfig = { const config: OxlintConfig = {
...gcchConfig, ...gcchConfig,
// Désactive la configuration liée à Astro.
overrides: [],
globals: { globals: {
Bun: "readonly", Bun: "readonly",
}, },

56
composer.lock generated
View file

@ -585,16 +585,16 @@
}, },
{ {
"name": "illuminate/collections", "name": "illuminate/collections",
"version": "v13.6.0", "version": "v13.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/collections.git", "url": "https://github.com/illuminate/collections.git",
"reference": "8337eab46f512633d36df6b2f0ee54bf309c1da1" "reference": "36cfc25fc23ca18714fc8ee6abc947ee99c18462"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/illuminate/collections/zipball/8337eab46f512633d36df6b2f0ee54bf309c1da1", "url": "https://api.github.com/repos/illuminate/collections/zipball/36cfc25fc23ca18714fc8ee6abc947ee99c18462",
"reference": "8337eab46f512633d36df6b2f0ee54bf309c1da1", "reference": "36cfc25fc23ca18714fc8ee6abc947ee99c18462",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -640,11 +640,11 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2026-04-18T13:28:59+00:00" "time": "2026-04-27T18:16:57+00:00"
}, },
{ {
"name": "illuminate/conditionable", "name": "illuminate/conditionable",
"version": "v13.6.0", "version": "v13.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/conditionable.git", "url": "https://github.com/illuminate/conditionable.git",
@ -690,16 +690,16 @@
}, },
{ {
"name": "illuminate/contracts", "name": "illuminate/contracts",
"version": "v13.6.0", "version": "v13.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/contracts.git", "url": "https://github.com/illuminate/contracts.git",
"reference": "022d9816b4ff052a3db5946a86be3cd2e224db0f" "reference": "a5f08426a807faac42616f733113ba263779f2dd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/022d9816b4ff052a3db5946a86be3cd2e224db0f", "url": "https://api.github.com/repos/illuminate/contracts/zipball/a5f08426a807faac42616f733113ba263779f2dd",
"reference": "022d9816b4ff052a3db5946a86be3cd2e224db0f", "reference": "a5f08426a807faac42616f733113ba263779f2dd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -734,11 +734,11 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2026-04-12T17:46:48+00:00" "time": "2026-04-24T14:55:56+00:00"
}, },
{ {
"name": "illuminate/macroable", "name": "illuminate/macroable",
"version": "v13.6.0", "version": "v13.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/macroable.git", "url": "https://github.com/illuminate/macroable.git",
@ -784,7 +784,7 @@
}, },
{ {
"name": "illuminate/reflection", "name": "illuminate/reflection",
"version": "v13.6.0", "version": "v13.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/reflection.git", "url": "https://github.com/illuminate/reflection.git",
@ -835,16 +835,16 @@
}, },
{ {
"name": "illuminate/support", "name": "illuminate/support",
"version": "v13.6.0", "version": "v13.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/support.git", "url": "https://github.com/illuminate/support.git",
"reference": "5394e4b6008175f5a42c1227b5c4b959aa1be963" "reference": "cc0d9d484ddd83fa54a0495cf15e7606e6a6f4b1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/5394e4b6008175f5a42c1227b5c4b959aa1be963", "url": "https://api.github.com/repos/illuminate/support/zipball/cc0d9d484ddd83fa54a0495cf15e7606e6a6f4b1",
"reference": "5394e4b6008175f5a42c1227b5c4b959aa1be963", "reference": "cc0d9d484ddd83fa54a0495cf15e7606e6a6f4b1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -910,7 +910,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2026-04-20T13:42:58+00:00" "time": "2026-04-27T13:46:05+00:00"
}, },
{ {
"name": "laravel/helpers", "name": "laravel/helpers",
@ -4532,11 +4532,11 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "2.1.51", "version": "2.1.53",
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ef67586798c003274797b288a68b221e4270dca7",
"reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "reference": "ef67586798c003274797b288a68b221e4270dca7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4581,7 +4581,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2026-04-21T18:22:01+00:00" "time": "2026-04-28T16:09:00+00:00"
}, },
{ {
"name": "psr/event-dispatcher", "name": "psr/event-dispatcher",
@ -5215,12 +5215,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git", "url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "08cd07f04fb07fb4d316e956801d57b700cf7096" "reference": "87a281378fdad8f5926efe259f6ca72e7a395e68"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/08cd07f04fb07fb4d316e956801d57b700cf7096", "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/87a281378fdad8f5926efe259f6ca72e7a395e68",
"reference": "08cd07f04fb07fb4d316e956801d57b700cf7096", "reference": "87a281378fdad8f5926efe259f6ca72e7a395e68",
"shasum": "" "shasum": ""
}, },
"conflict": { "conflict": {
@ -5836,7 +5836,7 @@
"phpoffice/common": "<0.2.9", "phpoffice/common": "<0.2.9",
"phpoffice/math": "<=0.2", "phpoffice/math": "<=0.2",
"phpoffice/phpexcel": "<=1.8.2", "phpoffice/phpexcel": "<=1.8.2",
"phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", "phpoffice/phpspreadsheet": "<=1.30.3|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6",
"phppgadmin/phppgadmin": "<=7.13", "phppgadmin/phppgadmin": "<=7.13",
"phpseclib/phpseclib": "<2.0.53|>=3,<3.0.51", "phpseclib/phpseclib": "<2.0.53|>=3,<3.0.51",
"phpservermon/phpservermon": "<3.6", "phpservermon/phpservermon": "<3.6",
@ -6264,7 +6264,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-04-24T17:22:29+00:00" "time": "2026-04-28T23:21:55+00:00"
}, },
{ {
"name": "sebastian/diff", "name": "sebastian/diff",
@ -7576,5 +7576,5 @@
"php": ">=8.5" "php": ">=8.5"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.9.0"
} }

View file

@ -14,7 +14,12 @@ update:
composer update composer update
bun update bun update
# Formatte avec Prettier et dprint. # Formatte avec treefmt.
[group('qualité')]
treefmt:
treefmt --config-file ~/.config/treefmt/treefmt.toml .
# Formatte avec Prettier et treefmt.
[group('qualité')] [group('qualité')]
format: format:
bun prettier \ bun prettier \

14
package.json Executable file → Normal file
View file

@ -15,7 +15,7 @@
"@mobily/ts-belt": "v4.0.0-rc.5", "@mobily/ts-belt": "v4.0.0-rc.5",
"@sentry/browser": "^10.50.0", "@sentry/browser": "^10.50.0",
"a11y-dialog": "^8.1.5", "a11y-dialog": "^8.1.5",
"effect": "^4.0.0-beta.57", "effect": "^4.0.0-beta.59",
"lit-html": "^3.3.2", "lit-html": "^3.3.2",
"purify-ts": "2.1.2", "purify-ts": "2.1.2",
"ts-pattern": "^5.9.0", "ts-pattern": "^5.9.0",
@ -24,14 +24,14 @@
"devDependencies": { "devDependencies": {
"@effect/language-service": "^0.85.1", "@effect/language-service": "^0.85.1",
"@effect/tsgo": "0.5.1", "@effect/tsgo": "0.5.1",
"@gcch/configuration-eslint": "git+https://git.gcch.fr/gcch/configuration-eslint#888eb4aa54e5", "@gcch/configuration-eslint": "git+https://git.gcch.fr/gcch/configuration-eslint#888eb4aa54",
"@gcch/configuration-oxlint": "git+https://git.gcch.fr/gcch/configuration-oxlint#83547fc1ebfd", "@gcch/configuration-oxlint": "git+https://git.gcch.fr/gcch/configuration-oxlint#83547fc1ebfd",
"@gcch/configuration-prettier": "git+https://git.gcch.fr/gcch/configuration-prettier#d267d6dc5ee8", "@gcch/configuration-prettier": "git+https://git.gcch.fr/gcch/configuration-prettier#d267d6dc5e",
"@playwright/test": "^1.59.1", "@playwright/test": "^1.59.1",
"@sentry/core": "^10.50.0", "@sentry/core": "^10.50.0",
"@types/bun": "^1.3.13", "@types/bun": "^1.3.13",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@typescript/native-preview": "7.0.0-dev.20260427.1", "@typescript/native-preview": "7.0.0-dev.20260429.1",
"@vitejs/plugin-legacy": "^8.0.1", "@vitejs/plugin-legacy": "^8.0.1",
"better-typescript-lib": "^2.12.0", "better-typescript-lib": "^2.12.0",
"browserslist": "^4.28.2", "browserslist": "^4.28.2",
@ -44,15 +44,19 @@
"fdir": "^6.5.0", "fdir": "^6.5.0",
"globals": "^17.5.0", "globals": "^17.5.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"knip": "^6.7.0", "knip": "^6.8.0",
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"lightningcss-cli": "^1.32.0", "lightningcss-cli": "^1.32.0",
"oxlint": "^1.62.0", "oxlint": "^1.62.0",
"oxlint-tsgolint": "^0.22.1", "oxlint-tsgolint": "^0.22.1",
"playwright": "^1.59.1", "playwright": "^1.59.1",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"prettier-plugin-curly": "^0.4.1",
"prettier-plugin-ini": "^1.3.0",
"prettier-plugin-jsdoc": "^1.8.0",
"prettier-plugin-pkg": "^0.22.1", "prettier-plugin-pkg": "^0.22.1",
"prettier-plugin-sh": "^0.18.1", "prettier-plugin-sh": "^0.18.1",
"prettier-plugin-sort-json": "^4.2.0",
"sass-embedded": "^1.99.0", "sass-embedded": "^1.99.0",
"stylelint": "^17.9.1", "stylelint": "^17.9.1",
"stylelint-config-clean-order": "^8.0.1", "stylelint-config-clean-order": "^8.0.1",

View file

@ -10,12 +10,18 @@ namespace HaikuAtelier;
use HaikuAtelier\Data\Product; use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource; use HaikuAtelier\WP\Resource;
use Roots\WPConfig\Config;
use Timber\Timber; use Timber\Timber;
use WC_Product; use WC_Product;
use function add_action; use function add_action;
use function array_map; use function array_map;
use function assert;
use function base64_encode;
use function is_string;
use function wc_get_products; use function wc_get_products;
use function wp_create_nonce;
use function wp_json_encode;
$context = Timber::context(); $context = Timber::context();
$templates = ['boutique.twig']; $templates = ['boutique.twig'];
@ -26,6 +32,17 @@ $wc_products = wc_get_products(['limit' => 12, 'order' => 'DESC', 'orderby' => '
$products = array_map(callback: Product::from_wc_product(...), array: $wc_products); $products = array_map(callback: Product::from_wc_product(...), array: $wc_products);
$context['products'] = $products; $context['products'] = $products;
// Injecte les états initiaux des données du Produit sous forme de JSON dans le contexte.
$page_states = [
'nonce' => wp_create_nonce('wc_store_api'),
'authString' => base64_encode(
Config::get('WOOCOMMERCE_API_CONSUMER_KEY') . ':' . Config::get('WOOCOMMERCE_API_CONSUMER_SECRET'),
),
]
|> wp_json_encode(...);
assert(is_string($page_states));
$context['page_states'] = $page_states;
add_action('wp_enqueue_scripts', function (): void { add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file( Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-boutique', handle: 'haiku-atelier-2024-styles-page-boutique',

View file

@ -654,7 +654,7 @@ video {
object-fit: cover; object-fit: cover;
} }
/* * Styles pour un bandeau défilant. */ /* Styles pour un bandeau défilant. */
.bandeau { .bandeau {
overflow: hidden; overflow: hidden;
display: flex; display: flex;

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
/* * Styles pour un bandeau défilant. */ /* Styles pour un bandeau défilant. */
.bandeau { .bandeau {
overflow: hidden; overflow: hidden;

View file

@ -1,52 +0,0 @@
import { Context, Effect, Layer, Schema } from "effect";
import { ENTETE_WC_NONCE, ROUTE_API_AJOUTE_ARTICLE_PANIER } from "../scripts/constantes/api.ts";
import { CartProduct } from "./schemas/api.ts";
/** Délai par défaut pour la réalisation d'une Requête. */
const REQUEST_TIMEOUT = 5_000;
/** Représente un soucis lors de l'exécution d'une Requête auprès de l'API WooCommerce. */
class APIError extends Schema.TaggedErrorClass<APIError>()("APIError", {
cause: Schema.Defect,
}) {}
class WooCommerceAPI extends Context.Service<
WooCommerceAPI,
{
AddProductToCart: (nonce: string, requestBody: CartProduct) => Effect.Effect<Response, APIError>;
}
>()("haikuatelier.fr/WooCommerceAPI") {
static readonly layer = Layer.effect(
WooCommerceAPI,
// oxlint-disable-next-line require-yield
Effect.gen(function*() {
const AddProductToCart = Effect.fn("AddProductToCart")(function*(nonce: string, product: CartProduct) {
const response = yield* Effect.tryPromise({
catch: error => new APIError({ cause: error }),
try: async () =>
fetch(ROUTE_API_AJOUTE_ARTICLE_PANIER, {
// Convertis en chaîne de caractères (équivalent à JSON.stringify).
body: Schema.encodeSync(Schema.fromJsonString(CartProduct))(product),
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
[ENTETE_WC_NONCE]: nonce,
},
method: "POST",
mode: "same-origin",
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
}),
});
return response;
});
return WooCommerceAPI.of({
AddProductToCart,
});
}),
);
}
export { WooCommerceAPI };

View file

@ -1,14 +1,16 @@
import { Console, Context, Effect, Layer, Match, pipe, Schedule, Schema, SchemaIssue } from "effect"; import { Console, Context, Effect, Layer, Match, pipe, Schedule, Schema, SchemaIssue } from "effect";
import { SchemaError } from "effect/Schema"; import type { SchemaError } from "effect/Schema";
import type {
HttpClientError,
} from "effect/unstable/http";
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 { HttpClientErrorSchema } from "effect/unstable/http/HttpClientError";
import type { CartProduct } from "../schemas/api.ts"; import type { CartProduct, GetProducts } from "../schemas/api.ts";
import { WooCommerceCart } from "../schemas/cart.ts"; import { WooCommerceCart } from "../schemas/cart.ts";
/** Le nombre maximal d'essais pour une Requête. */ /** Le nombre maximal d'essais pour une Requête. */
@ -16,20 +18,21 @@ const MAX_RETRIES = 3;
/** Le temps d'attente avant de réessayer une Requête. */ /** Le temps d'attente avant de réessayer une Requête. */
const RETRY_WAIT_TIME = "1 seconds"; const RETRY_WAIT_TIME = "1 seconds";
type APIError = APIRequestError | APIResponseError;
type APIResponse<T> = T | WooCommerceError;
/** Décrit une Erreur survenue au traitement d'une `Request`. */ /** Décrit une Erreur survenue au traitement d'une `Request`. */
class APIRequestError extends Schema.TaggedErrorClass<APIRequestError>()("APIRequestError", { class APIRequestError extends Schema.TaggedErrorClass<APIRequestError>()("APIRequestError", {
message: Schema.String,
cause: Schema.Union([Schema.Defect, HttpClientErrorSchema]), cause: Schema.Union([Schema.Defect, HttpClientErrorSchema]),
message: Schema.String,
}) {} }) {}
/** Décrit une Erreur survenue au traitement d'une `Response`. */ /** Décrit une Erreur survenue au traitement d'une `Response`. */
class APIResponseError extends Schema.TaggedErrorClass<APIResponseError>()("APIResponseError", { class APIResponseError extends Schema.TaggedErrorClass<APIResponseError>()("APIResponseError", {
message: Schema.String,
cause: Schema.Union([Schema.Defect, HttpClientErrorSchema]), cause: Schema.Union([Schema.Defect, HttpClientErrorSchema]),
message: Schema.String,
}) {} }) {}
type APIError = APIRequestError | APIResponseError;
class WooCommerceErrorBody extends Schema.Class<WooCommerceErrorBody>("WooCommerceErrorBody")({ class WooCommerceErrorBody extends Schema.Class<WooCommerceErrorBody>("WooCommerceErrorBody")({
code: Schema.String, code: Schema.String,
data: Schema.Struct({ data: Schema.Struct({
@ -37,13 +40,12 @@ class WooCommerceErrorBody extends Schema.Class<WooCommerceErrorBody>("WooCommer
}), }),
message: Schema.String, message: Schema.String,
}) {} }) {}
class WooCommerceError extends Schema.Class<WooCommerceError>("WooCommerceError")({ class WooCommerceError extends Schema.Class<WooCommerceError>("WooCommerceError")({
body: WooCommerceErrorBody, body: WooCommerceErrorBody,
status: Schema.Number, status: Schema.Number,
}) {} }) {}
type APIResponse<T> = T | WooCommerceError;
/** 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(
@ -81,22 +83,22 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
const matchAPIError = (error: HttpClientError.HttpClientError | SchemaError): APIError => { const matchAPIError = (error: HttpClientError.HttpClientError | SchemaError): APIError => {
if (error._tag === "SchemaError") { if (error._tag === "SchemaError") {
return new APIRequestError({ return new APIRequestError({
message: `Erreur lors du parsage du corps de la Requête :${SchemaIssue.makeFormatterDefault()(error.issue)}`,
cause: error, cause: error,
message: `Erreur lors du parsage du corps de la Requête :${SchemaIssue.makeFormatterDefault()(error.issue)}`,
}); });
} else { } else {
return Match.typeTags<HttpClientError.HttpClientErrorReason, APIError>()({ return Match.typeTags<HttpClientError.HttpClientErrorReason, APIError>()({
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" }),
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" }),
TransportError: (cause): APIError => TransportError: (cause): APIError =>
new APIRequestError({ new APIRequestError({
cause, cause,
message: "Un problème réseau empêche l'exécution de la Requête", 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); })(error.reason);
} }
}; };
@ -109,7 +111,7 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
return yield* Effect.succeed(error); return yield* Effect.succeed(error);
}); });
const AddProductToCart = Effect.fn("AppClient.AddProductToCart")( const AddProductToCart = Effect.fn("APIClient.AddProductToCart")(
function*(nonce: string, productToAdd: CartProduct): Effect.fn.Return<APIResponse<WooCommerceCart>, APIError> { function*(nonce: string, productToAdd: CartProduct): Effect.fn.Return<APIResponse<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`),
@ -132,7 +134,28 @@ class APIClient extends Context.Service<APIClient>()("haikuatelier.fr/APIClient"
}, },
); );
return { AddProductToCart }; const GetProducts = Effect.fn("APIClient.GetProducts")(
function*(nonce: string, authString: string, queryParams: GetProducts) {
const request = pipe(
HttpClientRequest.get(`/wp-json/wc/store/products`),
HttpClientRequest.setHeader("Nonce", nonce),
HttpClientRequest.bearerToken(authString),
// 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.mapError(error => matchAPIError(error)),
Effect.tapError(error => printErrorAsSuccinctMessage(error)),
);
return response;
},
);
return { AddProductToCart, GetProducts };
}), }),
}) { }) {
static readonly Live = Layer.effect(this, this.make).pipe(Layer.provide(APIFetchClient)); static readonly Live = Layer.effect(this, this.make).pipe(Layer.provide(APIFetchClient));

View file

@ -31,7 +31,7 @@ const getAllSelectorFromParent =
pipe( pipe(
parent.querySelectorAll<E>(selector), parent.querySelectorAll<E>(selector),
// Convertis NodeListOf en Array. // Convertis NodeListOf en Array.
(xs: NodeListOf<E>) => Array.from<E>(xs), (xs: NodeListOf<E>) => [...xs],
(xs: Array<E>) => Option.liftPredicate(FxArray.isReadonlyArrayNonEmpty)(xs), (xs: Array<E>) => Option.liftPredicate(FxArray.isReadonlyArrayNonEmpty)(xs),
); );

View file

@ -0,0 +1,9 @@
import { Effect } from "effect";
import { ATTRIBUT_CHARGEMENT, ATTRIBUT_DESACTIVE } from "../../scripts/constantes/dom.ts";
const setLoadingState = Effect.fn("setLoadingState")(function*(element: HTMLElement, isLoading: boolean) {
element.toggleAttribute(ATTRIBUT_DESACTIVE, isLoading);
element.toggleAttribute(ATTRIBUT_CHARGEMENT, isLoading);
});
export { setLoadingState };

View file

@ -1,18 +1,18 @@
import { Context, Effect, flow, Layer, pipe, Schedule, Schema } from "effect"; import { Context, Effect, flow, Layer, pipe, Schedule, Schema } from "effect";
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
import { HttpClientError } from "effect/unstable/http/HttpClientError"; import type { HttpClientError } from "effect/unstable/http/HttpClientError";
class Todo extends Schema.Class<Todo>("Todo")({
userId: Schema.Number,
id: Schema.Number,
title: Schema.String,
completed: Schema.Boolean,
}) {}
class FetchClientError extends Schema.TaggedErrorClass<FetchClientError>()("FetchClientError", { class FetchClientError extends Schema.TaggedErrorClass<FetchClientError>()("FetchClientError", {
cause: Schema.Defect, cause: Schema.Defect,
}) {} }) {}
class Todo extends Schema.Class<Todo>("Todo")({
completed: Schema.Boolean,
id: Schema.Number,
title: Schema.String,
userId: Schema.Number,
}) {}
class FetchClientExample extends Context.Service<FetchClientExample, { class FetchClientExample extends Context.Service<FetchClientExample, {
readonly allTodos: Effect.Effect<ReadonlyArray<Todo>, FetchClientError>; readonly allTodos: Effect.Effect<ReadonlyArray<Todo>, FetchClientError>;
createTodo(todo: Omit<Todo, "id">): Effect.Effect<Todo, FetchClientError>; createTodo(todo: Omit<Todo, "id">): Effect.Effect<Todo, FetchClientError>;
@ -75,8 +75,8 @@ class FetchClientExample extends Context.Service<FetchClientExample, {
return FetchClientExample.of({ return FetchClientExample.of({
allTodos, allTodos,
getTodo,
createTodo, createTodo,
getTodo,
}); });
}), }),
).pipe( ).pipe(

View file

@ -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 };

View file

@ -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,
}; };

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,6 +1,6 @@
// oxlint-disable typescript/dot-notation
import type { SchemaError } from "effect/Schema"; import type { SchemaError } from "effect/Schema";
// oxlint-disable typescript/dot-notation
import { import {
Array as FxArray, Array as FxArray,
Console, Console,
@ -21,6 +21,7 @@ import type { APIError } from "../../scripts-effect/lib/api.ts";
import type { DetailEnsemble } from "./types.d.ts"; import type { DetailEnsemble } from "./types.d.ts";
import { APIClient } from "../../scripts-effect/lib/api.ts"; import { APIClient } from "../../scripts-effect/lib/api.ts";
import { setLoadingState } from "../../scripts-effect/lib/elements.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";
@ -98,11 +99,6 @@ class ProductPageDOM extends Context.Service<ProductPageDOM>()(
); );
}); });
const setLoadingState = Effect.fn("setLoadingState")(function*(element: HTMLElement, isLoading: boolean) {
element.toggleAttribute(ATTRIBUT_DESACTIVE, isLoading);
element.toggleAttribute(ATTRIBUT_CHARGEMENT, isLoading);
});
const detailButtonClickHandler = Effect.fn("detailButtonClickHandler")( 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
@ -232,7 +228,7 @@ class ProductPageDOM extends Context.Service<ProductPageDOM>()(
// Désactive les interactions le temps de la requête. // Désactive les interactions le temps de la requête.
yield* setLoadingState(AddToCartButton, true); yield* setLoadingState(AddToCartButton, true);
yield* SubscriptionRef.set(AddToCartButtonText, "Adding Product..."); yield* SubscriptionRef.set(AddToCartButtonText, "Adding Product...");
// 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);

View file

@ -7,7 +7,7 @@ class ProductPageMessages extends Context.Service<ProductPageMessages>()("haikua
const { AddToCartButton } = yield* ProductPageElements; const { AddToCartButton } = yield* ProductPageElements;
const AddToCartButtonText = yield* SubscriptionRef.make("Add to cart"); const AddToCartButtonText = yield* SubscriptionRef.make("Add to cart");
// const AddToCartErrorText = yield* SubscriptionRef.make<Option.Option<string>>(Option.none()); // Const AddToCartErrorText = yield* SubscriptionRef.make<Option.Option<string>>(Option.none());
const initAddToCartButtonUpdates = Effect.fn("initAddToCartButtonUpdates")(function*() { const initAddToCartButtonUpdates = Effect.fn("initAddToCartButtonUpdates")(function*() {
return yield* pipe( return yield* pipe(

View file

@ -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);
}));
}); });

View file

@ -11,15 +11,20 @@ namespace HaikuAtelier;
use HaikuAtelier\Data\Product; use HaikuAtelier\Data\Product;
use HaikuAtelier\WP\Resource; use HaikuAtelier\WP\Resource;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Roots\WPConfig\Config;
use Timber\Timber; use Timber\Timber;
use WC_Product; use WC_Product;
use WP_Term; use WP_Term;
use function add_action; use function add_action;
use function assert; use function assert;
use function base64_encode;
use function get_queried_object; use function get_queried_object;
use function is_array; use function is_array;
use function is_string;
use function wc_get_products; use function wc_get_products;
use function wp_create_nonce;
use function wp_json_encode;
$context = Timber::context(); $context = Timber::context();
$templates = ['boutique.twig']; $templates = ['boutique.twig'];
@ -48,6 +53,17 @@ $products = wc_get_products([
$context['products'] = $products; $context['products'] = $products;
$context['category_id'] = $current_term->term_id; $context['category_id'] = $current_term->term_id;
// Injecte les états initiaux des données du Produit sous forme de JSON dans le contexte.
$page_states = [
'nonce' => wp_create_nonce('wc_store_api'),
'authString' => base64_encode(
Config::get('WOOCOMMERCE_API_CONSUMER_KEY') . ':' . Config::get('WOOCOMMERCE_API_CONSUMER_SECRET'),
),
]
|> wp_json_encode(...);
assert(is_string($page_states));
$context['page_states'] = $page_states;
add_action('wp_enqueue_scripts', function (): void { add_action('wp_enqueue_scripts', function (): void {
Resource::enqueue_style_file( Resource::enqueue_style_file(
handle: 'haiku-atelier-2024-styles-page-boutique', handle: 'haiku-atelier-2024-styles-page-boutique',

View file

@ -3,14 +3,12 @@
{% block head %} {% block head %}
{{ include('parts/en-tetes-backend.twig') }} {{ include('parts/en-tetes-backend.twig') }}
<script id="injection"> <!-- markup-fmt-ignore -->
// dprint-ignore-file <script
// Injection d'états pour les Scripts de la page. id="page-states"
type="application/json"
const _etats = { >
authString: "{{ auth_string }}", {{ page_states }}
nonce: "{{ nonce_wc }}",
};
</script> </script>
{% endblock head %} {% endblock head %}