temp: transfert entre ordinateurs

This commit is contained in:
gcch 2025-12-23 16:18:28 +01:00
commit d81acac380
46 changed files with 18652 additions and 1328 deletions

1588
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -25,33 +25,6 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
volumes: volumes:
- "db-data:/var/lib/mysql:rw" - "db-data:/var/lib/mysql:rw"
jaeger:
container_name: "haikuatelier.fr-jaeger"
environment:
- "COLLECTOR_OTLP_ENABLED=true"
healthcheck:
interval: "5s"
retries: 3
start_period: "5s"
test:
- "CMD"
- "wget"
- "--spider"
- "http://localhost:16686"
timeout: "2s"
image: "cr.jaegertracing.io/jaegertracing/jaeger:latest"
networks:
- "haiku-network"
ports:
- "6831:6831/udp"
- "6832:6832/udp"
- "5778:5778"
- "16686:16686"
- "4317:4317"
- "4318:4318"
- "14250:14250"
- "14268:14268"
- "14269:14269"
proxy: proxy:
container_name: "haikuatelier.fr-proxy" container_name: "haikuatelier.fr-proxy"
depends_on: depends_on:
@ -105,30 +78,10 @@ services:
- "./containers/data/certs:/etc/certs/:ro" - "./containers/data/certs:/etc/certs/:ro"
- "./containers/data/traefik/logs:/var/log/traefik:rw" - "./containers/data/traefik/logs:/var/log/traefik:rw"
- "/var/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro" - "/var/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro"
valkey:
command: "valkey-server /usr/local/etc/valkey/valkey.conf"
container_name: "haikuatelier.fr-valkey"
env_file:
- path: "./.env"
required: true
healthcheck:
interval: "10s"
retries: 3
test:
- "CMD-SHELL"
- "valkey-cli ping | grep PONG"
timeout: "5s"
image: "docker.io/valkey/valkey:9-alpine"
restart: "unless-stopped"
sysctls:
- "net.core.somaxconn=512"
volumes:
- "./containers/conf/valkey.conf:/usr/local/etc/valkey/valkey.conf:ro"
wordpress: wordpress:
container_name: "haikuatelier.fr-wordpress" container_name: "haikuatelier.fr-wordpress"
depends_on: depends_on:
- "db" - "db"
- "valkey"
- "traefik" - "traefik"
env_file: env_file:
- path: "./.env" - path: "./.env"

476
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -36,7 +36,8 @@ VOLUME /var/www/wordpress
WORKDIR /var/www/wordpress WORKDIR /var/www/wordpress
# Récupère les fichiers du projet. # Récupère les fichiers du projet.
COPY --from=repo --chmod=777 "/tmp/repo/" . COPY --from=repo --chmod=775 "/tmp/repo/" .
RUN chown www-data: -R .
# Installe les dépendences Composer. # Installe les dépendences Composer.
RUN composer install RUN composer install

View file

@ -7,6 +7,7 @@ include /etc/angie/modules-enabled/*.conf;
pcre_jit on; pcre_jit on;
pid /run/angie.pid; pid /run/angie.pid;
error_log /dev/stdout info; error_log /dev/stdout info;
error_log /var/log/angie/angie.log warn;
events { events {
worker_connections 2048; worker_connections 2048;

View file

@ -17,4 +17,5 @@ fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_hide_header X-Powered-By;
fastcgi_index index.php; fastcgi_index index.php;

View file

@ -2,15 +2,12 @@ server {
listen 80; listen 80;
server_name _; server_name _;
root /var/www/wordpress/web; root /var/www/wordpress/web/;
index index.html index.php; index index.html index.php;
access_log /var/log/angie/haikuatelier-access.log; access_log /var/log/angie/haikuatelier-access.log;
error_log /var/log/angie/haikuatelier-error.log; error_log /var/log/angie/haikuatelier-error.log;
# Remove X-Powered-By, which is an information leak
fastcgi_hide_header X-Powered-By;
# Pour éviter des erreurs liés à des requêtes trop lourdes. # Pour éviter des erreurs liés à des requêtes trop lourdes.
fastcgi_buffers 16 32k; fastcgi_buffers 16 32k;
fastcgi_buffer_size 64k; fastcgi_buffer_size 64k;
@ -33,21 +30,23 @@ server {
access_log off; access_log off;
} }
location ~ \.php$ {
fastcgi_pass wordpress:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include /etc/angie/fastcgi.conf;
try_files $uri =404;
}
location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
expires 365d;
}
location / { location / {
try_files $uri $uri/ /index.php?$args; try_files $uri $uri/ /index.php?$args;
} }
location ~ \.php$ {
include /etc/angie/fastcgi.conf;
fastcgi_pass wordpress:9000;
fastcgi_intercept_errors on;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
}
location ~* \.(?:ico|svg|css|js|gif|jpe?g|png|avif|jxl|webp|avif|woff2?)$ {
access_log off;
expires max;
add_header "Cache-Control" "public, immutable";
}
location * { location * {
add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS"; add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS";
add_header "Access-Control-Allow-Origin" "*"; add_header "Access-Control-Allow-Origin" "*";

View file

@ -12,3 +12,4 @@ memory_limit = 1024M
post_max_size = 32M post_max_size = 32M
register_globals = Off register_globals = Off
upload_max_filesize = 32M upload_max_filesize = 32M
open_basedir = "/"

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,9 @@
# Journal de développement # Journal de développement
## 2026-01-09
- Faire un modèle _Twig_ pour l'injection de données _JSON_ dans le _HTML_ d'une page.
## 2025-06-13 ## 2025-06-13
### Informations produit sous forme de grille ### Informations produit sous forme de grille

View file

@ -1,4 +1,6 @@
- Produits - Produits
- Aller sur tous les Produits - Aller sur tous les Produits
- La page doit correctement se charger - La page doit correctement se charger
- Tous les attributs et leurs valeurs doivent être proposés - Il est possible d'ajouter chaque variation au Panier
- Il n'est pas possible d'ajouter un Produit sans stock au Panier
- Le backend renvoie une erreur quand une demande d'ajout au Panier pour un Produit sans stock est malgré tout effectuée

View file

@ -1,3 +1,9 @@
## 2026-02-19
- Créer un _timer_ et _service_ `systemd` adossés à un script réalisant un export de la BDD de production du site Haiku toutes les semaines dans le dossier `db` du répertoire.
---
- PAGE PANIER - PAGE PANIER
- [-] Bouton « Réinitialiser » pour les Articles - [-] Bouton « Réinitialiser » pour les Articles
- [-] Bouton « Réinitialiser » pour les Adresses - [-] Bouton « Réinitialiser » pour les Adresses

View file

@ -76,13 +76,13 @@
}, },
"newLineKind": "lf", "newLineKind": "lf",
"plugins": [ "plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm", "https://plugins.dprint.dev/typescript-0.95.15.wasm",
"https://plugins.dprint.dev/json-0.21.0.wasm", "https://plugins.dprint.dev/json-0.21.1.wasm",
"https://plugins.dprint.dev/markdown-0.20.0.wasm", "https://plugins.dprint.dev/markdown-0.21.1.wasm",
"https://plugins.dprint.dev/toml-0.7.0.wasm", "https://plugins.dprint.dev/toml-0.7.0.wasm",
"https://plugins.dprint.dev/g-plane/malva-v0.15.1.wasm", "https://plugins.dprint.dev/g-plane/malva-v0.15.2.wasm",
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm", "https://plugins.dprint.dev/g-plane/markup_fmt-v0.26.0.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm", "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm",
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364" "https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364"
], ],
"toml": { "toml": {

View file

@ -172,3 +172,6 @@ restart-services:
[group('container')] [group('container')]
pull-images: pull-images:
bun "scripts/pull-container-images.ts" bun "scripts/pull-container-images.ts"
export_production_db:
fish "scripts/déclenche-sauvegarde-bdd-production.fish"

View file

@ -1,2 +0,0 @@
[tools]
"cargo:mago" = "latest"

View file

@ -7,15 +7,17 @@
"license": "ISC", "license": "ISC",
"main": "index.js", "main": "index.js",
"keywords": [], "keywords": [],
"scripts": { "knip": "knip" }, "scripts": {
"knip": "knip"
},
"dependencies": { "dependencies": {
"@effect/language-service": "^0.64.1", "@effect/language-service": "^0.75.1",
"@logtape/logtape": "^1.3.6", "@logtape/logtape": "^1.3.7",
"@mobily/ts-belt": "v4.0.0-rc.5", "@mobily/ts-belt": "v4.0.0-rc.5",
"@sentry/browser": "^10.32.1", "@sentry/browser": "^10.40.0",
"a11y-dialog": "^8.1.4", "a11y-dialog": "^8.1.5",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"effect": "^3.19.14", "effect": "^3.19.19",
"lit-html": "^3.3.2", "lit-html": "^3.3.2",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"loglevel-plugin-prefix": "^0.8.4", "loglevel-plugin-prefix": "^0.8.4",
@ -25,45 +27,45 @@
"valibot": "1.1.0" "valibot": "1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11", "@biomejs/biome": "^2.4.4",
"@cspell/dict-fr-fr": "^2.3.2", "@cspell/dict-fr-fr": "^2.3.2",
"@eslint/js": "^9.39.2", "@eslint/js": "^10.0.1",
"@playwright/test": "^1.57.0", "@playwright/test": "^1.58.2",
"@prettier/plugin-xml": "^3.4.2", "@prettier/plugin-xml": "^3.4.2",
"@sentry/core": "^10.32.1", "@sentry/core": "^10.40.0",
"@swc/cli": "0.7.8", "@swc/cli": "0.7.8",
"@types/eslint__js": "^9.14.0", "@types/eslint__js": "^9.14.0",
"@types/node": "^25.0.3", "@types/node": "^25.3.1",
"@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-legacy": "^7.2.1",
"better-typescript-lib": "^2.12.0", "better-typescript-lib": "^2.12.0",
"browserslist": "^4.28.1", "browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001763", "caniuse-lite": "^1.0.30001774",
"eslint": "^9.39.2", "eslint": "^10.0.2",
"eslint-plugin-oxlint": "^1.38.0", "eslint-plugin-oxlint": "^1.50.0",
"eslint-plugin-perfectionist": "^5.3.1", "eslint-plugin-perfectionist": "^5.6.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
"globals": "^17.0.0", "globals": "^17.3.0",
"knip": "^5.80.0", "knip": "^5.85.0",
"lightningcss-cli": "^1.30.2", "lightningcss-cli": "^1.31.1",
"oxlint": "^1.38.0", "oxlint": "^1.50.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"playwright": "^1.57.0", "playwright": "^1.58.2",
"prettier": "^4.0.0-alpha.13", "prettier": "^4.0.0-alpha.13",
"prettier-plugin-pkg": "^0.21.2", "prettier-plugin-pkg": "^0.21.2",
"prettier-plugin-sh": "^0.18.0", "prettier-plugin-sh": "^0.18.0",
"sass-embedded": "^1.97.2", "sass-embedded": "^1.97.3",
"stylelint": "^16.26.1", "stylelint": "^17.4.0",
"stylelint-config-clean-order": "^8.0.0", "stylelint-config-clean-order": "^8.0.1",
"stylelint-config-sass-guidelines": "^12.1.0", "stylelint-config-sass-guidelines": "^13.0.0",
"stylelint-config-standard-scss": "^16.0.0", "stylelint-config-standard-scss": "^17.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.8.0", "stylelint-declaration-block-no-ignored-properties": "^3.0.0",
"stylelint-plugin-logical-css": "^1.2.3", "stylelint-plugin-logical-css": "^2.0.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "^8.52.0", "typescript-eslint": "^8.56.1",
"vite": "^8.0.0-beta.0", "vite": "^8.0.0-beta.0",
"vite-plugin-valibot-env": "^1.0.1", "vite-plugin-valibot-env": "^1.0.1",
"vite-tsconfig-paths": "^6.0.3", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.16", "vitest": "^4.0.18",
"wp-types": "^4.69.0" "wp-types": "^4.69.0"
}, },
"browserslist": [ "browserslist": [
@ -80,5 +82,7 @@
"project": ["web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}"] "project": ["web/app/themes/haiku-atelier-2024/src/scripts/**/*.{js,ts,d.ts}"]
}, },
"trustedDependencies": ["@biomejs/biome", "@parcel/watcher", "@swc/core", "core-js", "esbuild", "lightningcss-cli"], "trustedDependencies": ["@biomejs/biome", "@parcel/watcher", "@swc/core", "core-js", "esbuild", "lightningcss-cli"],
"overrides": { "vite": "8.0.0-beta.0" } "overrides": {
"vite": "8.0.0-beta.0"
}
} }

View file

@ -0,0 +1,2 @@
ssh ade -- fish /srv/haikuatelier.com/scripts/sauvegarde-bdd-production.fish
rclone copy --check-first --progress --multi-thread-streams 8 ade:/srv/haikuatelier.com/db /home/gcch/Répertoires/git.gcch.fr/gcch/haiku-atelier-2024/db

View file

@ -1,5 +1,10 @@
set -f fichiers_toml (fd --glob "*.toml") set -f fichiers_toml (fd --glob "*.toml")
set -f fichiers_angie (fd --glob "*.conf" containers/conf/angie)
for toml in $fichiers_toml for toml in $fichiers_toml
taplo format "$toml" tombi format "$toml"
end
for angie in $angie
nginxfmt "$angie"
end end

View file

@ -0,0 +1,4 @@
cd /srv/haikuatelier.com/web
sudo -S wp-cli --allow-root db export
sudo -S mv -v /srv/haikuatelier.com/web/*.sql ../db
sudo -S chown www-data: ../db

View file

@ -3,6 +3,9 @@ import {
WCV3Product, WCV3Product,
WCV3Products, WCV3Products,
} from "../../web/app/themes/haiku-atelier-2024/src/scripts/lib/types/api/v3/products"; } from "../../web/app/themes/haiku-atelier-2024/src/scripts/lib/types/api/v3/products";
import { BackendHeaders, getBackendHeadersFromHtml } from "./utils.ts";
import { pipe } from "effect";
import { not } from "effect/Boolean";
/* /*
* Faire un premier test simple l'on clic sur la première carte du shop * Faire un premier test simple l'on clic sur la première carte du shop
@ -15,40 +18,50 @@ import {
*/ */
type ProductsFixture = { type ProductsFixture = {
products: WCV3Products; products: ProductsKinds;
};
type ProductsKinds = {
allProducts: WCV3Products;
simpleProducts: WCV3Products;
simpleProductsWithStock: WCV3Products;
simpleProductsWithoutStock: WCV3Products;
}; };
export const test = base.extend<ProductsFixture>({ export const test = base.extend<ProductsFixture>({
products: async ({ page, request }, use) => { products: async ({ page, request }, use) => {
await page.goto("/shop"); await page.goto("/shop");
const nonce = await page.locator("data#nonce").textContent(); const backendHeaders: BackendHeaders = await getBackendHeadersFromHtml(page);
const authString = await page.locator("data#auth-string").textContent();
if (nonce === null || nonce === "") {
throw new Error("Le nonce ne peut être vide.");
}
if (authString === null || authString === "") {
throw new Error("L'en-tête auth-string ne peut être vide.");
}
const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", { const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", {
headers: { Nonce: nonce, Authorization: `Basic ${authString}` }, headers: { Nonce: backendHeaders.nonce, Authorization: `Basic ${backendHeaders.authString}` },
}); });
expect(response.ok(), "The API returned the list of every Product").toBeTruthy(); expect(response.ok(), "The API returned the list of every Product").toBeTruthy();
const products = await response.json() as WCV3Products; const isSimpleProduct = (product: WCV3Product) => product.type === "simple";
await use(products); const hasStock = (product: WCV3Product) => (product.stock_quantity ?? 0) > 0;
const hasNoStock = (product: WCV3Product) => pipe(hasStock(product), not);
const allProducts = await response.json() as WCV3Products;
const simpleProducts = allProducts.filter(isSimpleProduct);
const simpleProductsWithStock = simpleProducts.filter(hasStock);
const simpleProductsWithoutStock = simpleProducts.filter(hasNoStock);
const kinds = {
allProducts,
simpleProducts,
simpleProductsWithStock,
simpleProductsWithoutStock,
} satisfies ProductsKinds;
await use(kinds);
}, },
}); });
test("can add a Product without variation with stock to the Cart", async ({ products, page }) => { test("can add a Product without variation with stock to the Cart", async ({ products, page }) => {
const simpleProducts = products.filter(p => isSimpleProduct(p) && hasQuantity(p));
console.debug("Simple Products with stock", simpleProducts.length);
expect(simpleProducts.length, "At least one Simple product with stock must exist").toBeGreaterThan(0);
// Prend un produit au hasard. // Prend un produit au hasard.
const randomProductIndex = getRandomIntInclusive(0, simpleProducts.length - 1); const randomProductIndex = getRandomIntInclusive(0, products.simpleProductsWithStock.length - 1);
const randomProduct = simpleProducts.at(randomProductIndex); const randomProduct = products.simpleProductsWithStock.at(randomProductIndex);
expect(randomProduct, "The selected random Product must exist").toBeTruthy(); expect(randomProduct, "The selected random Product must exist").toBeTruthy();
if (randomProduct === undefined) { if (randomProduct === undefined) {
throw new Error("The random product can't be undefined"); throw new Error("The random product can't be undefined");
@ -81,13 +94,9 @@ test("can add a Product without variation with stock to the Cart", async ({ prod
}); });
test("can't add a Product without variation without stock to the Cart", async ({ products, page }) => { test("can't add a Product without variation without stock to the Cart", async ({ products, page }) => {
const simpleProducts = products.filter(p => isSimpleProduct(p) && !hasQuantity(p));
console.debug("Simple Products without stock", simpleProducts.length);
expect(simpleProducts.length, "At least one Simple product without stock must exist").toBeGreaterThan(0);
// Prend un produit au hasard. // Prend un produit au hasard.
const randomProductIndex = getRandomIntInclusive(0, simpleProducts.length - 1); const randomProductIndex = getRandomIntInclusive(0, products.simpleProductsWithoutStock.length - 1);
const randomProduct = simpleProducts.at(randomProductIndex); const randomProduct = products.simpleProductsWithoutStock.at(randomProductIndex);
expect(randomProduct, "The selected random Product must exist").toBeTruthy(); expect(randomProduct, "The selected random Product must exist").toBeTruthy();
if (randomProduct === undefined) { if (randomProduct === undefined) {
throw new Error("The random product can't be undefined"); throw new Error("The random product can't be undefined");
@ -109,6 +118,3 @@ const getRandomIntInclusive = (min: number, max: number): number => {
return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled);
}; };
const isSimpleProduct = (product: WCV3Product) => product.type === "simple";
const hasQuantity = (product: WCV3Product) => (product.stock_quantity ?? 0) > 0;

View file

@ -1,5 +1,6 @@
import { test, expect, Page, Locator, Response, APIRequestContext } from "@playwright/test"; import { test, expect, Page, Locator, Response, APIRequestContext } from "@playwright/test";
import { WCV3Products } from "../../web/app/themes/haiku-atelier-2024/src/scripts/lib/types/api/v3/products"; import { WCV3Products } from "../../web/app/themes/haiku-atelier-2024/src/scripts/lib/types/api/v3/products";
import { BackendHeaders, getBackendHeadersFromHtml } from "./utils.ts";
test.describe.configure({ mode: "parallel", timeout: 60000 }); test.describe.configure({ mode: "parallel", timeout: 60000 });
@ -19,18 +20,9 @@ test("can access all Products' pages", async ({ page, request }): Promise<void>
}); });
const getAllProductsLinks = async (page: Page, request: APIRequestContext): Promise<Array<string>> => { const getAllProductsLinks = async (page: Page, request: APIRequestContext): Promise<Array<string>> => {
const nonce = await page.locator("data#nonce").textContent(); const backendHeaders: BackendHeaders = await getBackendHeadersFromHtml(page);
const authString = await page.locator("data#auth-string").textContent();
if (nonce === null || nonce === "") {
throw new Error("Le nonce ne peut être vide.");
}
if (authString === null || authString === "") {
throw new Error("L'en-tête auth-string ne peut être vide.");
}
const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", { const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", {
headers: { Nonce: nonce, Authorization: `Basic ${authString}` }, headers: { Nonce: backendHeaders.nonce, Authorization: `Basic ${backendHeaders.authString}` },
}); });
const json = await response.json() as WCV3Products; const json = await response.json() as WCV3Products;
const links = json.map(p => p.permalink); const links = json.map(p => p.permalink);
@ -45,14 +37,15 @@ const scrollToGridsEnd = async (page: Page): Promise<void> => {
let currentPageNumber = "1"; let currentPageNumber = "1";
const productsGrid: Locator = page.locator(".grille-produits"); const productsGrid: Locator = page.locator(".grille-produits");
await expect(productsGrid).toBeVisible(); await expect(productsGrid, "The Product's grid is visible").toBeVisible();
expect(await (productsGrid.getAttribute("data-page")), "The initial page number attribute is correct").toBe(
currentPageNumber,
);
const showMoreButton: Locator = page.getByRole("button", { name: "Show more" }); const showMoreButton: Locator = page.getByRole("button", { name: "Show more" });
await expect(productsGrid).toBeVisible(); await expect(showMoreButton, "The 'Show more' button is visible").toBeVisible();
while (hasMoreProducts) { while (hasMoreProducts) {
expect(await (productsGrid.getAttribute("data-page"))).toBe(currentPageNumber);
const newProductsResponse: Promise<Response> = page.waitForResponse( const newProductsResponse: Promise<Response> = page.waitForResponse(
new RegExp(".*wp-json\/wc\/v3\/products.*"), new RegExp(".*wp-json\/wc\/v3\/products.*"),
); );
@ -65,7 +58,9 @@ const scrollToGridsEnd = async (page: Page): Promise<void> => {
await gridWithNewPageNumber.waitFor(); await gridWithNewPageNumber.waitFor();
// Redondance pour expliciter la raison de l'assertion. // Redondance pour expliciter la raison de l'assertion.
expect(await (productsGrid.getAttribute("data-page"))).toBe(newPageNumber); expect(await (productsGrid.getAttribute("data-page")), "The page number attribute is incremented").toBe(
newPageNumber,
);
currentPageNumber = newPageNumber; currentPageNumber = newPageNumber;
// La fin de la grille est atteint. // La fin de la grille est atteint.

24
tests/playwright/utils.ts Normal file
View file

@ -0,0 +1,24 @@
import { Option, pipe } from "effect";
import { Page } from "playwright/test";
export type BackendHeaders = {
authString: string;
nonce: string;
};
/**
* @throws Lève une exception si la balise du JSON est introuvable.
*/
export const getBackendHeadersFromHtml = async (page: Page): Promise<BackendHeaders> => {
const backendHeaders: BackendHeaders | undefined = pipe(
Option.fromNullable(await page.locator("#injection-v2").textContent()),
Option.andThen(j => JSON.parse(j) as BackendHeaders),
Option.getOrUndefined,
);
if (backendHeaders === undefined) {
throw new Error("The JSON of the backend headers in the page's HTML can't be null.");
}
return backendHeaders;
};

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
#page-a-propos{--images-longueur-maximale:1300px;--page-marges-bloc-debut:var(--en-tete-hauteur);--images-marges-ligne:var(--espace-xl)*2;margin-top:var(--page-marges-bloc-debut);flex-flow:column;display:flex}#page-a-propos .storytelling{padding:var(--espace-xl)0;color:var(--couleur-gris-fonce)}#page-a-propos .storytelling__conteneur{width:min(var(--images-longueur-maximale),100% - var(--images-marges-ligne));margin:auto;position:relative}#page-a-propos .storytelling picture{position:relative}#page-a-propos .storytelling picture:before{content:"";filter:opacity(0%);background-image:url(/app/themes/haiku-atelier-2024/assets/img/icons/cloud-gris.svg);background-position:50%;background-repeat:space;background-size:contain;width:100%;height:100%;animation:1s linear infinite alternate both clignotement;position:absolute;top:0;left:0}#page-a-propos .storytelling img{object-fit:contain;background:0 0;width:max-content;height:auto;position:relative}#page-a-propos .storytelling .epingle{position:absolute;right:46.5%}#page-a-propos .storytelling .epingle img{pointer-events:none;width:1.75rem;display:block}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="1"]{top:1%;right:60%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="2"]{top:25%;right:70%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="3"]{top:37%;right:20%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="4"]{top:58%;right:70%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="5"]{top:76%;right:14%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="6"]{top:95.5%;right:75%}#page-a-propos .storytelling .boite-texte{top:0;right:calc(46.5% - (15rem + var(--espace-l))/2);padding:var(--espace-l);border:1px solid var(--couleur-noir);color:var(--couleur-noir);visibility:hidden;opacity:0;background:var(--couleur-fond);flex-flow:column;font-size:.8rem;font-style:italic;transition:opacity .2s,visibility .2s;display:flex;position:absolute}#page-a-propos .storytelling .boite-texte button{top:0;right:calc(-1.5rem - var(--espace-m) - var(--espace-xs));padding:var(--espace-xs);align-self:end;position:absolute}#page-a-propos .storytelling .boite-texte button img{pointer-events:none;aspect-ratio:1;width:1.5rem}#page-a-propos .storytelling .boite-texte button:active{background:var(--couleur-jaune)}#page-a-propos .storytelling .boite-texte p{max-width:15rem}#page-a-propos .storytelling .boite-texte p+p{margin-top:var(--espace-m)}#page-a-propos .storytelling .boite-texte[data-ensemble-epingle-boite-actif]{visibility:visible;opacity:1}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="2"]{--hauteur-boite:calc(7lh + 1rem + var(--espace-l)*2);top:calc(23% - var(--hauteur-boite)/2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="3"]{--hauteur-boite:calc(7lh + 1rem + var(--espace-l)*2);top:calc(35% - var(--hauteur-boite)/2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="3"] button{left:calc(-1.5rem - var(--espace-m) - var(--espace-xs))}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="4"]{--hauteur-boite:calc(8lh + 2rem + var(--espace-l)*2);top:calc(58% - var(--hauteur-boite)/2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="5"]{--hauteur-boite:calc(12lh + 2rem + var(--espace-l)*2);top:calc(76% - var(--hauteur-boite)/2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="6"]{--hauteur-boite:calc(7lh + 1rem + var(--espace-l)*2);top:calc(95.5% - var(--hauteur-boite)/2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="6"] button{left:calc(-1.5rem - var(--espace-m) - var(--espace-xs))}@media (width<=700px){#page-a-propos{--images-marges-ligne:var(--espace-m)}}@keyframes clignotement{to{filter:opacity(30%)}} #page-a-propos{--images-longueur-maximale:1300px;--page-marges-bloc-debut:var(--en-tete-hauteur);--images-marges-ligne:var(--espace-xl) * 2;margin-top:var(--page-marges-bloc-debut);flex-flow:column;display:flex}#page-a-propos .storytelling{padding:var(--espace-xl) 0;color:var(--couleur-gris-fonce)}#page-a-propos .storytelling__conteneur{width:min(var(--images-longueur-maximale), 100% - var(--images-marges-ligne));margin:auto;position:relative}#page-a-propos .storytelling picture{position:relative}#page-a-propos .storytelling picture:before{content:"";filter:opacity(0%);background-image:url(/app/themes/haiku-atelier-2024/assets/img/icons/cloud-gris.svg);background-position:50%;background-repeat:space;background-size:contain;width:100%;height:100%;animation:1s linear infinite alternate both clignotement;position:absolute;top:0;left:0}#page-a-propos .storytelling img{object-fit:contain;background:0 0;width:max-content;height:auto;position:relative}#page-a-propos .storytelling .epingle{position:absolute;right:46.5%}#page-a-propos .storytelling .epingle img{pointer-events:none;width:1.75rem;display:block}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="1"]{top:1%;right:60%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="2"]{top:25%;right:70%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="3"]{top:37%;right:20%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="4"]{top:58%;right:70%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="5"]{top:76%;right:14%}#page-a-propos .storytelling .epingle[data-id-ensemble-epingle-boite="6"]{top:95.5%;right:75%}#page-a-propos .storytelling .boite-texte{top:0;right:calc(46.5% - (15rem + var(--espace-l)) / 2);padding:var(--espace-l);border:1px solid var(--couleur-noir);color:var(--couleur-noir);visibility:hidden;opacity:0;background:var(--couleur-fond);flex-flow:column;font-size:.8rem;font-style:italic;transition:opacity .2s,visibility .2s;display:flex;position:absolute}#page-a-propos .storytelling .boite-texte button{top:0;right:calc(-1.5rem - var(--espace-m) - var(--espace-xs));padding:var(--espace-xs);align-self:end;position:absolute}#page-a-propos .storytelling .boite-texte button img{pointer-events:none;aspect-ratio:1;width:1.5rem}#page-a-propos .storytelling .boite-texte button:active{background:var(--couleur-jaune)}#page-a-propos .storytelling .boite-texte p{max-width:15rem}#page-a-propos .storytelling .boite-texte p+p{margin-top:var(--espace-m)}#page-a-propos .storytelling .boite-texte[data-ensemble-epingle-boite-actif]{visibility:visible;opacity:1}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="2"]{--hauteur-boite:calc(7lh + 1rem + var(--espace-l) * 2);top:calc(23% - var(--hauteur-boite) / 2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="3"]{--hauteur-boite:calc(7lh + 1rem + var(--espace-l) * 2);top:calc(35% - var(--hauteur-boite) / 2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="3"] button{left:calc(-1.5rem - var(--espace-m) - var(--espace-xs))}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="4"]{--hauteur-boite:calc(8lh + 2rem + var(--espace-l) * 2);top:calc(58% - var(--hauteur-boite) / 2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="5"]{--hauteur-boite:calc(12lh + 2rem + var(--espace-l) * 2);top:calc(76% - var(--hauteur-boite) / 2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="6"]{--hauteur-boite:calc(7lh + 1rem + var(--espace-l) * 2);top:calc(95.5% - var(--hauteur-boite) / 2)}#page-a-propos .storytelling .boite-texte[data-id-ensemble-epingle-boite="6"] button{left:calc(-1.5rem - var(--espace-m) - var(--espace-xs))}@media (width<=700px){#page-a-propos{--images-marges-ligne:var(--espace-m)}}@keyframes clignotement{to{filter:opacity(30%)}}

View file

@ -1 +1 @@
#page-accueil{--hauteur-conteneur:var(--contenu-page-hauteur-minimale-sans-categories);--page-marges-bloc-debut:var(--en-tete-hauteur);--conteneur-marges-internes-ligne:var(--espace-xl);min-block-size:var(--hauteur-conteneur);max-block-size:var(--hauteur-conteneur);margin-top:var(--page-marges-bloc-debut);flex-flow:column;display:flex;overflow:hidden}#page-accueil .storytelling{overscroll-behavior:none;min-block-size:inherit;max-block-size:inherit;overflow-y:scroll}#page-accueil .storytelling__conteneur{overscroll-behavior:inherit;min-block-size:calc(var(--hauteur-conteneur)*13);padding:0 var(--conteneur-marges-internes-ligne);flex-flow:column;place-items:center;display:flex}#page-accueil .storytelling__animation{--hauteur-animation:90px;--taille-police:calc(var(--espace-xl)*2.5);pointer-events:none;z-index:3;visibility:visible;opacity:1;block-size:100%;mask-image:linear-gradient(var(--mask-direction,to right),#0000,#000 20%,#000 80%,#0000);place-content:center;place-items:center;margin:auto;transition:opacity 1s ease-in-out,visibility 1s ease-in-out;display:grid;position:absolute;top:0;left:0;right:0;overflow:hidden}#page-accueil .storytelling__animation[hidden]{visibility:hidden;opacity:0;transition:opacity 1s ease-in-out,visibility 1s ease-in-out;display:grid!important}#page-accueil .storytelling__animation.no-js{visibility:hidden;opacity:0;transition:opacity 1s ease-in-out,visibility 1s ease-in-out}#page-accueil .storytelling__animation .animation-conteneur{width:120vw;block-size:var(--hauteur-animation);overflow:visible}#page-accueil .storytelling__animation .animation-texte{font-size:var(--taille-police);text-shadow:4px 4px 0 var(--couleur-blanc);text-transform:uppercase;letter-spacing:var(--espacement-inter-lettres-rapproche-s);font-weight:600;overflow:visible}#page-accueil .storytelling__image{inline-size:max-content;max-inline-size:100%;min-block-size:var(--hauteur-conteneur);max-block-size:var(--hauteur-conteneur);align-content:center;position:sticky;top:0}#page-accueil .storytelling__image[data-caché]{display:none!important}#page-accueil .storytelling__image picture{max-block-size:inherit}#page-accueil .storytelling__image img{max-block-size:inherit;object-fit:contain;background:0 0;margin:auto;scale:.95}@media (scripting:none){#page-accueil .storytelling__animation{visibility:hidden}}@media (width<=700px){#page-accueil{--conteneur-marges-internes-ligne:var(--espace-l)}}@media (width<=500px){#page-accueil{--conteneur-marges-internes-ligne:var(--espace-m)}}@supports ((-moz-appearance:none)){#page-accueil .storytelling__animation{--taille-police:calc(var(--espace-xl)*2.2)}} #page-accueil{--hauteur-conteneur:var(--contenu-page-hauteur-minimale-sans-categories);--page-marges-bloc-debut:var(--en-tete-hauteur);--conteneur-marges-internes-ligne:var(--espace-xl);min-block-size:var(--hauteur-conteneur);max-block-size:var(--hauteur-conteneur);margin-top:var(--page-marges-bloc-debut);flex-flow:column;display:flex;overflow:hidden}#page-accueil .storytelling{overscroll-behavior:none;min-block-size:inherit;max-block-size:inherit;overflow-y:scroll}#page-accueil .storytelling__conteneur{overscroll-behavior:inherit;min-block-size:calc(var(--hauteur-conteneur) * 13);padding:0 var(--conteneur-marges-internes-ligne);flex-flow:column;place-items:center;display:flex}#page-accueil .storytelling__animation{--hauteur-animation:90px;--taille-police:calc(var(--espace-xl) * 2.5);pointer-events:none;z-index:3;visibility:visible;opacity:1;block-size:100%;mask-image:linear-gradient(var(--mask-direction,to right), #0000, #000 20%, #000 80%, #0000);place-content:center;place-items:center;margin:auto;transition:opacity 1s ease-in-out,visibility 1s ease-in-out;display:grid;position:absolute;top:0;left:0;right:0;overflow:hidden}#page-accueil .storytelling__animation[hidden]{visibility:hidden;opacity:0;transition:opacity 1s ease-in-out,visibility 1s ease-in-out;display:grid!important}#page-accueil .storytelling__animation.no-js{visibility:hidden;opacity:0;transition:opacity 1s ease-in-out,visibility 1s ease-in-out}#page-accueil .storytelling__animation .animation-conteneur{width:120vw;block-size:var(--hauteur-animation);overflow:visible}#page-accueil .storytelling__animation .animation-texte{font-size:var(--taille-police);text-shadow:4px 4px 0 var(--couleur-blanc);text-transform:uppercase;letter-spacing:var(--espacement-inter-lettres-rapproche-s);font-weight:600;overflow:visible}#page-accueil .storytelling__image{inline-size:max-content;max-inline-size:100%;min-block-size:var(--hauteur-conteneur);max-block-size:var(--hauteur-conteneur);align-content:center;position:sticky;top:0}#page-accueil .storytelling__image[data-caché]{display:none!important}#page-accueil .storytelling__image picture{max-block-size:inherit}#page-accueil .storytelling__image img{max-block-size:inherit;object-fit:contain;background:0 0;margin:auto;scale:.95}@media (scripting:none){#page-accueil .storytelling__animation{visibility:hidden}}@media (width<=700px){#page-accueil{--conteneur-marges-internes-ligne:var(--espace-l)}}@media (width<=500px){#page-accueil{--conteneur-marges-internes-ligne:var(--espace-m)}}@supports ((-moz-appearance:none)){#page-accueil .storytelling__animation{--taille-police:calc(var(--espace-xl) * 2.2)}}

View file

@ -1 +1 @@
#page-boutique .actions{text-align:center;align-content:center;width:100%}#page-boutique .actions button{height:initial;padding:var(--espace-xl)0;margin:auto} #page-boutique .actions{text-align:center;align-content:center;width:100%}#page-boutique .actions button{height:initial;padding:var(--espace-xl) 0;margin:auto}

View file

@ -1 +1 @@
.page-modele-simple{--page-hauteur-minimale:calc(100svh - var(--en-tete-hauteur) - var(--pied-de-page-hauteur) - var(--espace-xl) - 1px);--page-marges-bloc-debut:var(--en-tete-hauteur);margin-top:var(--page-marges-bloc-debut);margin-bottom:var(--espace-xl);border-bottom:1px solid var(--couleur-noir);flex-flow:column;display:flex}.page-modele-simple .contenu{width:min(50rem,100%);min-height:var(--page-hauteur-minimale);border:1px solid var(--couleur-noir);border-bottom:initial;flex-flow:column;place-items:center;margin:auto;font-style:italic;font-weight:400;display:flex}.page-modele-simple .contenu__en-tete{width:100%;padding:var(--espace-m)var(--espace-xl);color:var(--couleur-blanc);background:var(--couleur-noir)}.page-modele-simple .contenu__en-tete h2{text-transform:uppercase;width:fit-content;letter-spacing:var(--espacement-inter-lettres-etendu-l);margin:auto}.page-modele-simple .contenu__textuel{max-width:34rem;height:100%;padding:0 var(--espace-xl);text-wrap:pretty;flex-flow:column;flex:1;place-content:center;display:flex}.page-modele-simple .contenu__textuel p+p{margin-top:var(--espace-m)}.page-modele-simple#page-cgv .contenu{font-style:normal}.page-modele-simple#page-cgv .contenu header{font-style:italic}.page-modele-simple#page-cgv .contenu__textuel{max-width:initial;padding:0}.page-modele-simple#page-cgv .contenu__textuel__section{width:100%}.page-modele-simple#page-cgv .contenu__textuel__section:first-of-type header{border-top:initial}.page-modele-simple#page-cgv .contenu__textuel__section header{width:100%;margin-bottom:var(--espace-l);padding:var(--espace-m)var(--espace-xl);border-top:1px solid var(--couleur-noir);border-bottom:1px solid var(--couleur-noir)}.page-modele-simple#page-cgv .contenu__textuel__section header h3{text-transform:uppercase;width:fit-content;letter-spacing:var(--espacement-inter-lettres-etendu-l);margin:auto}.page-modele-simple#page-cgv .contenu__textuel__section ul{padding:0 var(--espace-xl);margin-bottom:1lh;list-style:inside square}.page-modele-simple#page-cgv .contenu__textuel__section p{padding:0 var(--espace-xl)}.page-modele-simple#page-cgv .contenu__textuel__section p:last-of-type{margin-bottom:var(--espace-xl)}@media (width<=50rem){.page-modele-simple .contenu{border-right:initial;border-left:initial}} .page-modele-simple{--page-hauteur-minimale:calc(100svh - var(--en-tete-hauteur) - var(--pied-de-page-hauteur) - var(--espace-xl) - 1px);--page-marges-bloc-debut:var(--en-tete-hauteur);margin-top:var(--page-marges-bloc-debut);margin-bottom:var(--espace-xl);border-bottom:1px solid var(--couleur-noir);flex-flow:column;display:flex}.page-modele-simple .contenu{width:min(50rem,100%);min-height:var(--page-hauteur-minimale);border:1px solid var(--couleur-noir);border-bottom:initial;flex-flow:column;place-items:center;margin:auto;font-style:italic;font-weight:400;display:flex}.page-modele-simple .contenu__en-tete{width:100%;padding:var(--espace-m) var(--espace-xl);color:var(--couleur-blanc);background:var(--couleur-noir)}.page-modele-simple .contenu__en-tete h2{text-transform:uppercase;width:fit-content;letter-spacing:var(--espacement-inter-lettres-etendu-l);margin:auto}.page-modele-simple .contenu__textuel{max-width:34rem;height:100%;padding:0 var(--espace-xl);text-wrap:pretty;flex-flow:column;flex:1;place-content:center;display:flex}.page-modele-simple .contenu__textuel p+p{margin-top:var(--espace-m)}.page-modele-simple#page-cgv .contenu{font-style:normal}.page-modele-simple#page-cgv .contenu header{font-style:italic}.page-modele-simple#page-cgv .contenu__textuel{max-width:initial;padding:0}.page-modele-simple#page-cgv .contenu__textuel__section{width:100%}.page-modele-simple#page-cgv .contenu__textuel__section:first-of-type header{border-top:initial}.page-modele-simple#page-cgv .contenu__textuel__section header{width:100%;margin-bottom:var(--espace-l);padding:var(--espace-m) var(--espace-xl);border-top:1px solid var(--couleur-noir);border-bottom:1px solid var(--couleur-noir)}.page-modele-simple#page-cgv .contenu__textuel__section header h3{text-transform:uppercase;width:fit-content;letter-spacing:var(--espacement-inter-lettres-etendu-l);margin:auto}.page-modele-simple#page-cgv .contenu__textuel__section ul{padding:0 var(--espace-xl);margin-bottom:1lh;list-style:inside square}.page-modele-simple#page-cgv .contenu__textuel__section p{padding:0 var(--espace-xl)}.page-modele-simple#page-cgv .contenu__textuel__section p:last-of-type{margin-bottom:var(--espace-xl)}@media (width<=50rem){.page-modele-simple .contenu{border-right:initial;border-left:initial}}

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
#page-succes-commande{--page-hauteur-minimale:calc(100svh - var(--en-tete-hauteur) - var(--pied-de-page-hauteur) - var(--espace-xl) - 1px);--page-marges-bloc-debut:var(--en-tete-hauteur);margin-top:var(--page-marges-bloc-debut);margin-bottom:var(--espace-xl);border-bottom:1px solid var(--couleur-noir);flex-flow:column;display:flex}#page-succes-commande .contenu{width:min(50rem,100%);min-height:var(--page-hauteur-minimale);border:1px solid var(--couleur-noir);border-bottom:initial;flex-flow:column;place-items:center;margin:auto;font-style:italic;font-weight:500;display:flex}#page-succes-commande .contenu__en-tete{width:100%;padding:var(--espace-m)var(--espace-xl);color:var(--couleur-blanc);background:var(--couleur-noir)}#page-succes-commande .contenu__en-tete h2{text-transform:uppercase;width:fit-content;letter-spacing:var(--espacement-inter-lettres-etendu-l);margin:auto}#page-succes-commande .contenu__textuel{max-width:34rem;height:100%;padding:var(--espace-xl);flex-flow:column;flex:1;place-content:center;font-style:normal;display:flex}#page-succes-commande .contenu__textuel p+p{margin-top:var(--espace-m)}#page-succes-commande .contenu__rappel-commande{border-top:1px solid var(--couleur-noir);flex-flow:column;display:flex}#page-succes-commande .contenu__rappel-commande__produit{border-bottom:1px solid var(--couleur-noir);grid-template-rows:1fr;grid-template-columns:1fr 1fr;display:grid}#page-succes-commande .contenu__rappel-commande__produit:only-child,#page-succes-commande .contenu__rappel-commande__produit:last-of-type{border-bottom:initial}#page-succes-commande .contenu__rappel-commande__produit__illustratif{border-right:1px solid var(--couleur-noir)}#page-succes-commande .contenu__rappel-commande__produit__illustratif picture{overflow:hidden}#page-succes-commande .contenu__rappel-commande__produit__illustratif img{aspect-ratio:1;height:auto}#page-succes-commande .contenu__rappel-commande .detail-produit{width:100%;padding:0 var(--espace-xl);flex-flow:column;place-content:center;font-style:italic;display:flex}#page-succes-commande .contenu__rappel-commande .detail-produit__nom-prix{column-gap:var(--espace-xl);margin-bottom:var(--espace-xs);font-size:var(--espace-l);line-height:var(--hauteur-ligne-moitie);flex-flow:row;justify-content:space-between;display:flex}#page-succes-commande .contenu__rappel-commande .detail-produit__nom-prix span{min-width:4rem;font-weight:600;font-style:initial;text-align:right}#page-succes-commande .contenu__rappel-commande .detail-produit__description{margin-bottom:var(--espace-l);line-height:var(--hauteur-ligne-moitie);text-transform:lowercase}@media (width<=50rem){#page-succes-commande .contenu{border-right:initial;border-left:initial}} #page-succes-commande{--page-hauteur-minimale:calc(100svh - var(--en-tete-hauteur) - var(--pied-de-page-hauteur) - var(--espace-xl) - 1px);--page-marges-bloc-debut:var(--en-tete-hauteur);margin-top:var(--page-marges-bloc-debut);margin-bottom:var(--espace-xl);border-bottom:1px solid var(--couleur-noir);flex-flow:column;display:flex}#page-succes-commande .contenu{width:min(50rem,100%);min-height:var(--page-hauteur-minimale);border:1px solid var(--couleur-noir);border-bottom:initial;flex-flow:column;place-items:center;margin:auto;font-style:italic;font-weight:500;display:flex}#page-succes-commande .contenu__en-tete{width:100%;padding:var(--espace-m) var(--espace-xl);color:var(--couleur-blanc);background:var(--couleur-noir)}#page-succes-commande .contenu__en-tete h2{text-transform:uppercase;width:fit-content;letter-spacing:var(--espacement-inter-lettres-etendu-l);margin:auto}#page-succes-commande .contenu__textuel{max-width:34rem;height:100%;padding:var(--espace-xl);flex-flow:column;flex:1;place-content:center;font-style:normal;display:flex}#page-succes-commande .contenu__textuel p+p{margin-top:var(--espace-m)}#page-succes-commande .contenu__rappel-commande{border-top:1px solid var(--couleur-noir);flex-flow:column;display:flex}#page-succes-commande .contenu__rappel-commande__produit{border-bottom:1px solid var(--couleur-noir);grid-template-rows:1fr;grid-template-columns:1fr 1fr;display:grid}#page-succes-commande .contenu__rappel-commande__produit:only-child,#page-succes-commande .contenu__rappel-commande__produit:last-of-type{border-bottom:initial}#page-succes-commande .contenu__rappel-commande__produit__illustratif{border-right:1px solid var(--couleur-noir)}#page-succes-commande .contenu__rappel-commande__produit__illustratif picture{overflow:hidden}#page-succes-commande .contenu__rappel-commande__produit__illustratif img{aspect-ratio:1;height:auto}#page-succes-commande .contenu__rappel-commande .detail-produit{width:100%;padding:0 var(--espace-xl);flex-flow:column;place-content:center;font-style:italic;display:flex}#page-succes-commande .contenu__rappel-commande .detail-produit__nom-prix{column-gap:var(--espace-xl);margin-bottom:var(--espace-xs);font-size:var(--espace-l);line-height:var(--hauteur-ligne-moitie);flex-flow:row;justify-content:space-between;display:flex}#page-succes-commande .contenu__rappel-commande .detail-produit__nom-prix span{min-width:4rem;font-weight:600;font-style:initial;text-align:right}#page-succes-commande .contenu__rappel-commande .detail-produit__description{margin-bottom:var(--espace-l);line-height:var(--hauteur-ligne-moitie);text-transform:lowercase}@media (width<=50rem){#page-succes-commande .contenu{border-right:initial;border-left:initial}}

View file

@ -25,12 +25,12 @@ Timber::$dirname = ['views'];
// Charge les Scripts du thème (report d'erreurs) // Charge les Scripts du thème (report d'erreurs)
function load_scripts(): void { function load_scripts(): void {
wp_enqueue_script_module( // wp_enqueue_script_module(
id: 'haiku-atelier-2024-gaffe', // id: 'haiku-atelier-2024-gaffe',
deps: [], // deps: [],
src: get_template_directory_uri() . '/assets/js/gaffe.js', // src: get_template_directory_uri() . '/assets/js/gaffe.js',
version: filemtime(get_template_directory() . '/assets/js/gaffe.js'), // version: filemtime(get_template_directory() . '/assets/js/gaffe.js'),
); // );
wp_enqueue_script_module( wp_enqueue_script_module(
id: 'haiku-atelier-2024-bouton-panier', id: 'haiku-atelier-2024-bouton-panier',
deps: [], deps: [],

View file

@ -15,6 +15,23 @@ use Symfony\Component\Uid\Uuid;
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
// TODO: Appliquer le bon calcul pour les montants vs. percentages
function get_discount_amount(WC_Coupon $coupon) {
if ($coupon->get_discount_type() === 'fixed_cart') {
return $coupon->get_amount() * 100;
} else {
return $coupon->get_amount();
}
}
function get_discount_duration(WC_Coupon $coupon): string {
if ($coupon->get_discount_type() === 'fixed_cart') {
return 'once';
} else {
return 'forever';
}
}
// Récupère les informations nécessaires // Récupère les informations nécessaires
/** @var WC_Session_Handler $session_wc La Session WooCommerce contenant entre autre le Panier. */ /** @var WC_Session_Handler $session_wc La Session WooCommerce contenant entre autre le Panier. */
$session_wc = WC()->session; $session_wc = WC()->session;
@ -101,28 +118,18 @@ if (empty($methode_livraison['nom'])) {
// Sélectionne la clé API Stripe // Sélectionne la clé API Stripe
Stripe::setApiKey(Config::get('STRIPE_API_SECRET')); Stripe::setApiKey(Config::get('STRIPE_API_SECRET'));
// TODO: Appliquer le bon calcul pour les montants vs. percentages
function get_discount_amount(WC_Coupon $coupon) {
if ($coupon->get_discount_type() === 'amount_off') {
return $coupon->get_amount() * 100;
} else {
return $coupon->get_amount() * 100;
}
}
// Met à jour les Codes promos // Met à jour les Codes promos
$coupons_stripe = collect(Coupon::all()->data); $coupons_stripe = collect(Coupon::all()->data);
$coupons_wc = collect(WC()->cart->get_coupons()) $coupons_wc = collect(WC()->cart->get_coupons())
->map(static fn(WC_Coupon $coupon): array => [ ->map(static fn(WC_Coupon $coupon): array => [
'currency' => 'EUR', 'currency' => 'EUR',
'duration' => 'forever', 'duration' => get_discount_duration($coupon),
'fixed_cart' === $coupon->get_discount_type() ? 'amount_off' : 'percent_off' => get_discount_amount($coupon), 'fixed_cart' === $coupon->get_discount_type() ? 'amount_off' : 'percent_off' => get_discount_amount($coupon),
'id' => $coupon->get_code(), 'id' => $coupon->get_code(),
'name' => $coupon->get_code(), 'name' => $coupon->get_code(),
]) ])
->each(static function (array $item) use ($coupons_stripe): void { ->each(static function (array $item) use ($coupons_stripe): void {
// Si le code promo n'existe, le créer // Si le code promo n'existe pas, le créer
if (!$coupons_stripe->contains('name', $item['name'])) { if (!$coupons_stripe->contains('name', $item['name'])) {
Coupon::create($item); Coupon::create($item);
} }

View file

@ -4,9 +4,7 @@
* Le modèle de la Page d'un Produit. * Le modèle de la Page d'un Produit.
*/ */
use function Crell\fp\pipe;
use HaikuAtelier\Data\Product; use HaikuAtelier\Data\Product;
use Timber\Timber; use Timber\Timber;
require_once __DIR__ . '/src/inc/HTML.php'; require_once __DIR__ . '/src/inc/HTML.php';
@ -18,43 +16,16 @@ $templates = ['produit.twig'];
$product = wc_get_product(); $product = wc_get_product();
// Le Produit DOIT exister.
if ($product === null || is_bool($product)) { if ($product === null || is_bool($product)) {
throw new Exception("Le Produit n'existe pas."); throw new Exception("Le Produit n'existe pas.");
} }
// $donnees_produit = recupere_informations_produit_page_produit($product); // Assemble les données d'intérêt pour la page au sein d'une Classe.
$donnees_produit = Product::new($product); $donnees_produit = Product::new($product);
// Un tableau des informations d'affichage de chaque Variation du Produit
$variations_produit = pipe(
$product->get_children(),
// Récupère les Variations
static fn(/** @var list<int> */ $enfants): array => array_map(
callback: wc_get_product(...),
array: $enfants,
),
// Ne conserve que les Informations souhaitées.
static fn(/** @var list<WC_Product> */ $variations): array => array_map(
callback: static fn(WC_Product $variation): array => [
'id' => $variation->get_id(),
// Ne récupère que le titre de l'Attribut unique de la Variation.
'titre' => match (true) {
'' !== $variation->get_attribute('pa_side') => $variation->get_attribute('pa_side'),
'' !== $variation->get_attribute('pa_stone') => $variation->get_attribute('pa_stone'),
'' !== $variation->get_attribute('pa_size') => $variation->get_attribute('pa_size'),
'' !== $variation->get_attribute('pa_giftcard-amount') => $variation->get_attribute(
'pa_giftcard-amount',
),
default => '',
},
'prix' => $variation->get_price(),
],
array: $variations,
),
);
/** @var int $prix_maximal Le prix de la Variation la plus chère */ /** @var int $prix_maximal Le prix de la Variation la plus chère */
$prix_maximal = collect($variations_produit)->max('prix'); $prix_maximal = collect($donnees_produit->variations)->max('price');
$produits_meme_collection = array_map( $produits_meme_collection = array_map(
array: recupere_produits_meme_collection($donnees_produit->collection)($donnees_produit->id), array: recupere_produits_meme_collection($donnees_produit->collection)($donnees_produit->id),
@ -62,8 +33,8 @@ $produits_meme_collection = array_map(
); );
$context['produit'] = $donnees_produit; $context['produit'] = $donnees_produit;
$context['product_json'] = wp_json_encode($donnees_produit);
$context['prix_maximal'] = $prix_maximal; $context['prix_maximal'] = $prix_maximal;
$context['variations_produit'] = $variations_produit;
$context['produits_meme_collection'] = $produits_meme_collection; $context['produits_meme_collection'] = $produits_meme_collection;
/** /**
@ -86,12 +57,8 @@ function charge_scripts_page_produit(): void {
add_action('wp_enqueue_scripts', 'charge_scripts_page_produit'); add_action('wp_enqueue_scripts', 'charge_scripts_page_produit');
// $lal = wp_json_encode($context); $lal = wp_json_encode($context);
// echo "<script>console.debug({$lal});</script>"; echo "<script>console.debug({$lal});</script>";
// $lol = wc_get_product()->get_children();
// $lol = wp_json_encode($lol);
// echo "<script>console.debug({$lol});</script>";
// Rendu // Rendu
Timber::render( Timber::render(

View file

@ -17,7 +17,7 @@ final readonly class Product {
* @param list<Attribute> $attributes * @param list<Attribute> $attributes
* @param list<string> $left_column_photos * @param list<string> $left_column_photos
* @param list<string> $right_column_photos * @param list<string> $right_column_photos
* @param list<int> $variation_ids * @param list<ProductVariation> $variations
*/ */
private function __construct( private function __construct(
public array $attributes, public array $attributes,
@ -33,7 +33,7 @@ final readonly class Product {
public string $hover_photo, public string $hover_photo,
public string $slug, public string $slug,
public int $stock, public int $stock,
public array $variation_ids, public array $variations,
public string $url, public string $url,
) {} ) {}
@ -78,8 +78,9 @@ final readonly class Product {
$hover_photo = $right_column_photos[0] ?? genere_balise_img_multiformats('-1', true); $hover_photo = $right_column_photos[0] ?? genere_balise_img_multiformats('-1', true);
$slug = $product->get_slug(); $slug = $product->get_slug();
$stock = $product->get_stock_quantity() ?? 1; $stock = $product->get_stock_quantity() ?? 1;
/** @var list<int> */ $variations = $product->get_children()
$variation_ids = $product->get_children(); |> (static fn($ids) => Arr::map($ids, wc_get_product(...)))
|> (static fn($products) => Arr::map($products, ProductVariation::new(...)));
$url = $product->get_permalink(); $url = $product->get_permalink();
return new self( return new self(
@ -96,7 +97,7 @@ final readonly class Product {
hover_photo: $hover_photo, hover_photo: $hover_photo,
slug: $slug, slug: $slug,
stock: $stock, stock: $stock,
variation_ids: $variation_ids, variations: $variations,
url: $url, url: $url,
); );
} }

View file

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace HaikuAtelier\Data;
use WC_Product;
final readonly class ProductVariation {
/**
* @param int $id L'ID de la Variation
* @param string $price Le prix de la Variation
* @param list<ProductVariationAttribute> $attributes Les attributs appliqués à la Variation
*/
private function __construct(
public int $id,
public string $price,
public array $attributes,
) {}
/**
* Créé une nouvelle instance de `ProductVariation` à partir d'un `WC_Product`.
*/
public static function new(WC_Product $product): self {
$id = $product->get_id();
$price = $product->get_price();
/** @var list<ProductVariationAttribute> */
$attributes = array_map(
/** @phpstan-ignore argument.type (Impossible à satisfaire) */
static fn(string $key, string $value) => new ProductVariationAttribute($key, $value),
array_keys($product->get_attributes()),
array_values($product->get_attributes()),
);
return new self($id, $price, $attributes);
}
}

View file

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace HaikuAtelier\Data;
final readonly class ProductVariationAttribute {
/**
* @param string $attribute Le slug de l'Attribut
* @param string $value Le slug de la valeur de l'Attribut
*/
public function __construct(
public string $attribute,
public string $value,
) {}
}

View file

@ -197,9 +197,9 @@ function genere_prix_maximal_produit_variable_dans_reponse_rest(
} }
// Assigne le prix de la Variation la plus chère dans la Réponse // Assigne le prix de la Variation la plus chère dans la Réponse
$reponse->data['prix_maximal'] = collect($reponse->data['variations'])->map(wc_get_product(...))->map( $reponse->data['prix_maximal'] = collect($reponse->data['variations'])
static fn($p) => $p->get_price(), ->map(wc_get_product(...))
)->max(); ->map(static fn($p) => $p->get_price())->max();
return $reponse; return $reponse;
} }

View file

@ -1,6 +1,11 @@
import type { InferOutput } from "valibot"; import type { InferOutput } from "valibot";
import type { WCV3ProductsArgsSchema, WCV3ProductsSchema } from "../../../schemas/api/v3/products.ts"; import type {
WCV3ProductsArgsSchema,
WCV3ProductSchema,
WCV3ProductsSchema,
} from "../../../schemas/api/v3/products.ts";
export type WCV3Product = InferOutput<typeof WCV3ProductSchema>;
export type WCV3Products = InferOutput<typeof WCV3ProductsSchema>; export type WCV3Products = InferOutput<typeof WCV3ProductsSchema>;
export type WCV3ProductsArgs = InferOutput<typeof WCV3ProductsArgsSchema>; export type WCV3ProductsArgs = InferOutput<typeof WCV3ProductsArgsSchema>;

View file

@ -67,6 +67,7 @@ const E = {
CONTENUS_ACCORDEON: mustGetElesInDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON), CONTENUS_ACCORDEON: mustGetElesInDocument<HTMLDivElement>(DOM_CONTENUS_ACCORDEON),
DOM_VARIATION: recupereElementDocumentEither<HTMLSelectElement>(DOM_DOM_QUANTITE), DOM_VARIATION: recupereElementDocumentEither<HTMLSelectElement>(DOM_DOM_QUANTITE),
PRIX_PRODUIT: mustGetEleInDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT), PRIX_PRODUIT: mustGetEleInDocument<HTMLParagraphElement>(DOM_PRIX_PRODUIT),
PRODUCT_JSON: mustGetEleInDocument<HTMLScriptElement>("#product-json"),
VARIATION_CHOICE_FORM: mustGetEleInDocument<HTMLFormElement>("#variation-choice"), VARIATION_CHOICE_FORM: mustGetEleInDocument<HTMLFormElement>("#variation-choice"),
}; };
@ -124,26 +125,51 @@ const gereAccordeonDetailsProduit = (): void => {
E.BOUTON_AJOUT_PANIER.addEventListener("click", (event: MouseEvent): void => ajouteProduitAuPanier(event)); E.BOUTON_AJOUT_PANIER.addEventListener("click", (event: MouseEvent): void => ajouteProduitAuPanier(event));
}; };
const getAttributeValuesFromDom = () => { const getAttributesFromDom = (): ReadonlyArray<WCStoreCartAddItemArgsItems> => {
const selectElements = epipe( const selectElements = epipe(
document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"), document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"),
Array.from<HTMLSelectElement>, Array.from<HTMLSelectElement>,
); );
if (selectElements.length === 0) return []; if (selectElements.length === 0) return [];
const attributeValues = selectElements.map(select => { const attributes = selectElements.map((select: HTMLSelectElement) => {
return { return {
attribute: select.id.replace("selecteur-attribut-", ""), attribute: select.id,
value: select.value, value: select.value,
} satisfies WCStoreCartAddItemArgsItems; } satisfies WCStoreCartAddItemArgsItems;
}); });
return attributeValues; return attributes;
};
function areArraysEqual<T>(array1: Array<T>, array2: Array<T>): boolean {
if (array1 !== array2) {
const a1 = JSON.stringify(array1.toSorted());
const a2 = JSON.stringify(array2.toSorted());
return a1 === a2;
}
return true;
}
const updatePriceOnAttributeChange = (): void => {
E.VARIATION_CHOICE_FORM.addEventListener("change", (): void => {
if (!E.VARIATION_CHOICE_FORM.checkValidity()) {
return;
}
const productVariations: Array<unknown> = epipe(E.PRODUCT_JSON.textContent, JSON.parse)?.variations;
const chosenAttributes = getAttributesFromDom();
const chosenVariation = productVariations.find(v => areArraysEqual(v.attributes, chosenAttributes));
const newPrice: string = chosenVariation.price;
E.PRIX_PRODUIT.textContent = `${newPrice}`;
});
}; };
const ajouteProduitAuPanier = (event: MouseEvent): void => { const ajouteProduitAuPanier = (event: MouseEvent): void => {
event.preventDefault(); event.preventDefault();
console.debug("getAttributeValuesFromDom", getAttributeValuesFromDom()); console.debug("getAttributeValuesFromDom", getAttributesFromDom());
// Construis les arguments de la requête au backend // Construis les arguments de la requête au backend
const argsRequete: WCStoreCartAddItemArgs = { const argsRequete: WCStoreCartAddItemArgs = {
@ -153,7 +179,7 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
// .orDefault(ETATS_PAGE.idProduit), // .orDefault(ETATS_PAGE.idProduit),
id: ETATS_PAGE.idProduit, id: ETATS_PAGE.idProduit,
quantity: 1, quantity: 1,
variation: getAttributeValuesFromDom(), variation: getAttributesFromDom(),
}; };
// Réalise la Requête et traite sa Réponse // Réalise la Requête et traite sa Réponse
@ -234,19 +260,27 @@ const ajouteProduitAuPanier = (event: MouseEvent): void => {
}; };
const initAddToCartButtonActivationOnUserChoice = (): void => { const initAddToCartButtonActivationOnUserChoice = (): void => {
const isInStock = E.BOUTON_AJOUT_PANIER.hasAttribute("data-in-stock");
// S'il n'y a pas de stock, ne rien faire.
if (!isInStock) {
return;
}
// S'il n'y a pas de sélecteur de variation, activer le bouton.
const selectElements: ReadonlyArray<HTMLSelectElement> = epipe( const selectElements: ReadonlyArray<HTMLSelectElement> = epipe(
document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"), document.querySelectorAll<HTMLSelectElement>(".selecteur-produit select"),
Array.from<HTMLSelectElement>, Array.from<HTMLSelectElement>,
); );
// S'il n'y a pas de sélecteur de variation, activer le bouton.
if (selectElements.length === 0) { if (selectElements.length === 0) {
E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE); E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE);
} }
// (Dés)active le bouton d'ajout au panier en fonction de la validité du formulaire.
E.VARIATION_CHOICE_FORM.addEventListener("change", (): void => { E.VARIATION_CHOICE_FORM.addEventListener("change", (): void => {
const formValidity = E.VARIATION_CHOICE_FORM.checkValidity(); const isFormValid = E.VARIATION_CHOICE_FORM.checkValidity();
if (formValidity) { if (isFormValid) {
E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE); E.BOUTON_AJOUT_PANIER.removeAttribute(ATTRIBUT_DESACTIVE);
} else { } else {
E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_DESACTIVE, ""); E.BOUTON_AJOUT_PANIER.setAttribute(ATTRIBUT_DESACTIVE, "");
@ -257,4 +291,8 @@ const initAddToCartButtonActivationOnUserChoice = (): void => {
document.addEventListener("DOMContentLoaded", (): void => { document.addEventListener("DOMContentLoaded", (): void => {
gereAccordeonDetailsProduit(); gereAccordeonDetailsProduit();
initAddToCartButtonActivationOnUserChoice(); initAddToCartButtonActivationOnUserChoice();
updatePriceOnAttributeChange();
// DEBUG
console.debug(JSON.parse(document.querySelector("#product-json")?.textContent));
}); });

View file

@ -1,12 +1,14 @@
{% extends 'base.twig' %} {% extends 'base.twig' %}
{% block head %} {% block head %}
<script> {{ include('parts/en-tetes-backend.twig') }}
<script id="injection">
// Injection d'états pour les Scripts de la page. // Injection d'états pour les Scripts de la page.
const _etats = { const _etats = {
nonce: "{{ nonce_wc }}",
authString: "{{ auth_string }}", authString: "{{ auth_string }}",
nonce: "{{ nonce_wc }}",
}; };
</script> </script>
{% endblock head %} {% endblock head %}

View file

@ -1,7 +1,7 @@
{% extends 'base.twig' %} {% extends 'base.twig' %}
{% block head %} {% block head %}
<script> <script id="injection">
// Injection d'états pour les Scripts de la page. // Injection d'états pour les Scripts de la page.
const _etats = { const _etats = {

View file

@ -0,0 +1,6 @@
<script
id="injection-v2"
type="application/json"
>
{ "authString": "{{ auth_string }}", "nonce": "{{ nonce_wc }}" }
</script>

View file

@ -12,7 +12,6 @@
<h3 class="selecteur-produit__nom">{{ produit.name }}</h3> <h3 class="selecteur-produit__nom">{{ produit.name }}</h3>
<div class="selecteur-produit__attribut-variation"> <div class="selecteur-produit__attribut-variation">
{#
{% if produit.attributes %} {% if produit.attributes %}
{% for attribut in produit.attributes %} {% for attribut in produit.attributes %}
<div class="test"> <div class="test">
@ -20,8 +19,8 @@
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
#}
{#
{% if variations_produit|length > 1 %} {% if variations_produit|length > 1 %}
<label <label
for="selecteur-variation" for="selecteur-variation"
@ -54,6 +53,7 @@
</select> </select>
</div> </div>
{% endif %} {% endif %}
#}
</div> </div>
<p class="selecteur-produit__prix">{{ prix_maximal ?? produit.price }}€</p> <p class="selecteur-produit__prix">{{ prix_maximal ?? produit.price }}€</p>
@ -126,6 +126,7 @@
<button <button
class="bouton-case-pleine" class="bouton-case-pleine"
disabled disabled
data-in-stock
for="variation-choice" for="variation-choice"
id="bouton-ajout-panier" id="bouton-ajout-panier"
type="submit" type="submit"

View file

@ -1,15 +1,15 @@
<div class="selecteur-produit__attribut-variation__selecteurs"> <div class="selecteur-produit__attribut-variation__selecteurs">
<label <label
for="selecteur-attribut-{{ attribut.slug }}" for="{{ attribut.slug }}"
id="label-selecteur-attribut-{{ attribut.slug }}" id="label-{{ attribut.slug }}"
> >
{{ attribut.name }}: {{ attribut.name }}:
</label> </label>
<select <select
aria-labelledby="label-selecteur-attribut-{{ atribut.slug }}" aria-labelledby="label-{{ atribut.slug }}"
id="selecteur-attribut-{{ attribut.slug }}" id="{{ attribut.slug }}"
name="attribut-{{ attribut.slug }}" name="{{ attribut.slug }}"
required required
> >
<option <option

View file

@ -1,7 +1,7 @@
{% extends 'base.twig' %} {% extends 'base.twig' %}
{% block head %} {% block head %}
<script> <script id="injection">
// dprint-ignore-file // dprint-ignore-file
// Injection d'états pour les Scripts de la page. // Injection d'états pour les Scripts de la page.
@ -10,12 +10,22 @@
* @property {number} idProduit - L'ID en base de données du Produit. * @property {number} idProduit - L'ID en base de données du Produit.
* @property {string} nonce - Un nonce pour l'authentification de requêtes API. * @property {string} nonce - Un nonce pour l'authentification de requêtes API.
*/ */
/** @type {Etats} */ /** @type {Etats} */
const _etats = { const _etats = {
idProduit: {{ produit.id }}, idProduit: {{ produit.id }},
nonce: "{{ nonce_wc }}", nonce: "{{ nonce_wc }}",
}; };
</script> </script>
<!-- markup-fmt-ignore -->
<script
id="product-json"
type="application/json"
>
// dprint-ignore
{{ product_json }}
</script>
{% endblock head %} {% endblock head %}
{% block contenu %} {% block contenu %}