Compare commits

...

4 commits

52 changed files with 18600 additions and 1079 deletions

View file

@ -7,7 +7,7 @@
"/vendor/composer/**/*" "/vendor/composer/**/*"
], ],
"language_server.diagnostic_outsource_timeout": 5, "language_server.diagnostic_outsource_timeout": 5,
"language_server.diagnostics_on_update": false, "language_server.diagnostics_on_update": true,
"language_server.diagnostics_on_save": true, "language_server.diagnostics_on_save": true,
"language_server_highlight.enabled": true, "language_server_highlight.enabled": true,
"language_server_php_cs_fixer.enabled": true, "language_server_php_cs_fixer.enabled": true,

915
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"

View file

@ -52,7 +52,7 @@
"roots/bedrock-disallow-indexing": "^2.0", "roots/bedrock-disallow-indexing": "^2.0",
"roots/wordpress": "^6.8.1", "roots/wordpress": "^6.8.1",
"roots/wp-config": "^1.0", "roots/wp-config": "^1.0",
"stripe/stripe-php": "^16.3", "stripe/stripe-php": "^19",
"symfony/uid": "^8", "symfony/uid": "^8",
"timber/timber": "^2.3", "timber/timber": "^2.3",
"vlucas/phpdotenv": "^5.6.1", "vlucas/phpdotenv": "^5.6.1",

572
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

6
docs/TESTS.md Normal file
View file

@ -0,0 +1,6 @@
- Produits
- Aller sur tous les Produits
- La page doit correctement se charger
- 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,16 +7,18 @@
"license": "ISC", "license": "ISC",
"main": "index.js", "main": "index.js",
"keywords": [], "keywords": [],
"scripts": { "knip": "knip" }, "scripts": {
"knip": "knip"
},
"dependencies": { "dependencies": {
"@effect/language-service": "^0.60.0", "@effect/language-service": "^0.75.1",
"@logtape/logtape": "^1.2.2", "@logtape/logtape": "^1.3.7",
"@mobily/ts-belt": "v4.0.0-rc.5", "@mobily/ts-belt": "v4.0.0-rc.5",
"@sentry/browser": "^10.29.0", "@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.9", "effect": "^3.19.19",
"lit-html": "^3.3.1", "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",
"optics-ts": "^2.4.1", "optics-ts": "^2.4.1",
@ -25,44 +27,45 @@
"valibot": "1.1.0" "valibot": "1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.8", "@biomejs/biome": "^2.4.4",
"@cspell/dict-fr-fr": "^2.3.2", "@cspell/dict-fr-fr": "^2.3.2",
"@eslint/js": "^9.39.1", "@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.29.0", "@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": "^24.10.1", "@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.30001759", "caniuse-lite": "^1.0.30001774",
"eslint": "^9.39.1", "eslint": "^10.0.2",
"eslint-plugin-oxlint": "^1.31.0", "eslint-plugin-oxlint": "^1.50.0",
"eslint-plugin-perfectionist": "^4.15.1", "eslint-plugin-perfectionist": "^5.6.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
"globals": "^16.5.0", "globals": "^17.3.0",
"knip": "^5.71.0", "knip": "^5.85.0",
"lightningcss-cli": "^1.30.2", "lightningcss-cli": "^1.31.1",
"oxlint": "^1.31.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.93.3", "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.48.1", "typescript-eslint": "^8.56.1",
"vite": "^7.2.6", "vite": "^8.0.0-beta.0",
"vite-plugin-valibot-env": "^1.0.1", "vite-plugin-valibot-env": "^1.0.1",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18",
"wp-types": "^4.69.0" "wp-types": "^4.69.0"
}, },
"browserslist": [ "browserslist": [
@ -78,5 +81,8 @@
"entry": ["web/app/themes/haiku-atelier-2024/src/scripts/*.ts"], "entry": ["web/app/themes/haiku-atelier-2024/src/scripts/*.ts"],
"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"
}
} }

View file

@ -21,20 +21,22 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
// Définis un délai d'exécution.
timeout: 30000,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "list", reporter: "list",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000', baseURL: "https://haikuatelier.gcch.local",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry", trace: "on-first-retry",
clientCertificates: [ clientCertificates: [
{ {
origin: "https://haikuatelier.gcch.local", origin: "https://haikuatelier.gcch.local",
certPath: "../certs/_wildcard.gcch.local.pem", certPath: "./containers/data/certs/_wildcard.gcch.local.pem",
keyPath: "../certs/_wildcard.gcch.local-key.pem", keyPath: "./containers/data/certs/_wildcard.gcch.local-key.pem",
}, },
], ],
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
@ -46,42 +48,42 @@ export default defineConfig({
name: "desktop-chromium-1920", name: "desktop-chromium-1920",
use: { ...devices["Desktop Chrome"], viewport: { width: 1920, height: 1080 } }, use: { ...devices["Desktop Chrome"], viewport: { width: 1920, height: 1080 } },
}, },
{ // {
name: "desktop-chromium-1536", // name: "desktop-chromium-1536",
use: { ...devices["Desktop Chrome"], viewport: { width: 1536, height: 864 } }, // use: { ...devices["Desktop Chrome"], viewport: { width: 1536, height: 864 } },
}, // },
{ // {
name: "desktop-chromium-1366", // name: "desktop-chromium-1366",
use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 } }, // use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 768 } },
}, // },
{ {
name: "desktop-firefox-1920", name: "desktop-firefox-1920",
use: { ...devices["Desktop Firefox"], viewport: { width: 1920, height: 1080 } }, use: { ...devices["Desktop Firefox"], viewport: { width: 1920, height: 1080 } },
}, },
{ // {
name: "desktop-firefox-1536", // name: "desktop-firefox-1536",
use: { ...devices["Desktop Firefox"], viewport: { width: 1536, height: 864 } }, // use: { ...devices["Desktop Firefox"], viewport: { width: 1536, height: 864 } },
}, // },
{ // {
name: "desktop-firefox-1366", // name: "desktop-firefox-1366",
use: { ...devices["Desktop Firefox"], viewport: { width: 1366, height: 768 } }, // use: { ...devices["Desktop Firefox"], viewport: { width: 1366, height: 768 } },
}, // },
{ // {
name: "tablet-chromium-portrait", // name: "tablet-chromium-portrait",
use: { ...devices["Galaxy Tab S9"] }, // use: { ...devices["Galaxy Tab S9"] },
}, // },
{ // {
name: "tablet-chromium-landscape", // name: "tablet-chromium-landscape",
use: { ...devices["Galaxy Tab S9 landscape"] }, // use: { ...devices["Galaxy Tab S9 landscape"] },
}, // },
{ // {
name: "mobile-chromium-portrait", // name: "mobile-chromium-portrait",
use: { ...devices["Pixel 7"] }, // use: { ...devices["Pixel 7"] },
}, // },
{ // {
name: "mobile-chromium-landscape", // name: "mobile-chromium-landscape",
use: { ...devices["Pixel 7 landscape"] }, // use: { ...devices["Pixel 7 landscape"] },
}, // },
], ],
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
// webServer: { // webServer: {

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

@ -38,7 +38,7 @@ Array.from<TestPage>([
url: "https://haikuatelier.gcch.local/product/fuyou-long-earrings-silver/", url: "https://haikuatelier.gcch.local/product/fuyou-long-earrings-silver/",
}, },
]).forEach(({ pageName, url }) => { ]).forEach(({ pageName, url }) => {
test(pageName, async ({ page }, testInfo) => { test.skip(pageName, async ({ page }, testInfo) => {
await page.goto(url); await page.goto(url);
const projectName = testInfo.project.name; const projectName = testInfo.project.name;

View file

@ -0,0 +1,120 @@
import { test as base, expect, Response } from "@playwright/test";
import {
WCV3Product,
WCV3Products,
} 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
* On doit pouvoir naviguer sur la page
* Le produit ne DOIT pas avoir de variations (pour l'instant)
* Le bouton d'ajout du panier doit être visible
* (Si le produit n'est pas en stock) On ne doit pas pouvoir ajouter le produit au panier
* Le bouton doit afficher le texte "Out of stock"
* Le bouton doit être désactivé
*/
type ProductsFixture = {
products: ProductsKinds;
};
type ProductsKinds = {
allProducts: WCV3Products;
simpleProducts: WCV3Products;
simpleProductsWithStock: WCV3Products;
simpleProductsWithoutStock: WCV3Products;
};
export const test = base.extend<ProductsFixture>({
products: async ({ page, request }, use) => {
await page.goto("/shop");
const backendHeaders: BackendHeaders = await getBackendHeadersFromHtml(page);
const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", {
headers: { Nonce: backendHeaders.nonce, Authorization: `Basic ${backendHeaders.authString}` },
});
expect(response.ok(), "The API returned the list of every Product").toBeTruthy();
const isSimpleProduct = (product: WCV3Product) => product.type === "simple";
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 }) => {
// Prend un produit au hasard.
const randomProductIndex = getRandomIntInclusive(0, products.simpleProductsWithStock.length - 1);
const randomProduct = products.simpleProductsWithStock.at(randomProductIndex);
expect(randomProduct, "The selected random Product must exist").toBeTruthy();
if (randomProduct === undefined) {
throw new Error("The random product can't be undefined");
}
// Va à la page du Produit.
await page.goto(randomProduct.permalink);
// Vérifie le bon état du bouton de l'ajout au Panier.
const addToCartButton = page.getByRole("button", { name: "Add to cart", disabled: false });
await addToCartButton.scrollIntoViewIfNeeded();
await expect(addToCartButton, "The add to cart button must be visible").toBeVisible();
await expect(addToCartButton, "The add to cart button must be enabled").toBeEnabled();
// Vérifie qu'au clic sur le bouton, l'ajout au Panier retourne un succès.
const addToCartResponse: Promise<Response> = page.waitForResponse(
new RegExp(".*/wp-json/wc/store/cart/add-item"),
);
await addToCartButton.click();
const addToCartStatus = (await addToCartResponse).ok();
expect(addToCartStatus, "The cart addition must succeed").toBeTruthy();
// Vérifie que le bouton ait changé de texte.
const addedToCartButton = page.getByRole("button", { name: "Added to cart!" });
expect(addedToCartButton, "The add to cart button's text has changed").toBeDefined();
// Vérifie que le compteur d'articles dans le Panier soit incrémenté.
const cartLink = page.getByRole("link", { name: "cart (1)" });
expect(cartLink, "The cart items' indicator has been incremented").toBeDefined();
});
test("can't add a Product without variation without stock to the Cart", async ({ products, page }) => {
// Prend un produit au hasard.
const randomProductIndex = getRandomIntInclusive(0, products.simpleProductsWithoutStock.length - 1);
const randomProduct = products.simpleProductsWithoutStock.at(randomProductIndex);
expect(randomProduct, "The selected random Product must exist").toBeTruthy();
if (randomProduct === undefined) {
throw new Error("The random product can't be undefined");
}
// Va à la page du Produit.
await page.goto(randomProduct.permalink);
// Vérifie le bon état du bouton de l'ajout au Panier.
const outOfStockButton = page.getByRole("button", { name: "Out of stock", disabled: true });
await outOfStockButton.scrollIntoViewIfNeeded();
await expect(outOfStockButton, "The add to cart button must be visible").toBeVisible();
await expect(outOfStockButton, "The add to cart button must be disabled").toBeDisabled();
});
const getRandomIntInclusive = (min: number, max: number): number => {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled);
};

View file

@ -0,0 +1,71 @@
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 { BackendHeaders, getBackendHeadersFromHtml } from "./utils.ts";
test.describe.configure({ mode: "parallel", timeout: 60000 });
test("can scroll to the end of the grid", async ({ page }): Promise<void> => {
await scrollToGridsEnd(page);
});
test("can access all Products' pages", async ({ page, request }): Promise<void> => {
await page.goto("https://haikuatelier.gcch.local/shop/");
const links = await getAllProductsLinks(page, request);
for (const link of links) {
// Vérifie que le lien de la page retourne OK.
const req = await request.get(link as string);
expect(req, "The Product's page is accessible").toBeOK();
}
});
const getAllProductsLinks = async (page: Page, request: APIRequestContext): Promise<Array<string>> => {
const backendHeaders: BackendHeaders = await getBackendHeadersFromHtml(page);
const response = await request.get("/wp-json/wc/v3/products?page=1&per_page=100&status=publish", {
headers: { Nonce: backendHeaders.nonce, Authorization: `Basic ${backendHeaders.authString}` },
});
const json = await response.json() as WCV3Products;
const links = json.map(p => p.permalink);
return links;
};
const scrollToGridsEnd = async (page: Page): Promise<void> => {
await page.goto("https://haikuatelier.gcch.local/shop/");
let hasMoreProducts = true;
let currentPageNumber = "1";
const productsGrid: Locator = page.locator(".grille-produits");
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" });
await expect(showMoreButton, "The 'Show more' button is visible").toBeVisible();
while (hasMoreProducts) {
const newProductsResponse: Promise<Response> = page.waitForResponse(
new RegExp(".*wp-json\/wc\/v3\/products.*"),
);
await showMoreButton.click();
await newProductsResponse;
const newPageNumber = String(Number(currentPageNumber) + 1);
// Créé un nouveau Locator que l'on attend pour s'assurer que l'attribut soit bien mis à jour.
const gridWithNewPageNumber = page.locator(`.grille-produits[data-page="${newPageNumber}"]`);
await gridWithNewPageNumber.waitFor();
// Redondance pour expliciter la raison de l'assertion.
expect(await (productsGrid.getAttribute("data-page")), "The page number attribute is incremented").toBe(
newPageNumber,
);
currentPageNumber = newPageNumber;
// La fin de la grille est atteint.
if (await showMoreButton.isHidden()) {
hasMoreProducts = false;
}
}
};

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

View file

@ -44,5 +44,5 @@
"useUnknownInCatchVariables": true "useUnknownInCatchVariables": true
}, },
"exclude": ["vendor", "web/app/plugins", "web/wp"], "exclude": ["vendor", "web/app/plugins", "web/wp"],
"include": ["*.js", "lib", "web/app/themes/haiku-atelier-2024/src", "vite.config.ts"] "include": ["*.js", "lib", "web/app/themes/haiku-atelier-2024/src", "vite.config.ts", "tests"]
} }

File diff suppressed because one or more lines are too long

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 %}