feat(borne): modale allergenes generale (14 INCO) sur carte et fiche + harnais tests JS (P3)
All checks were successful
CI / secret-scan (pull_request) Successful in 7s
CI / php-lint (pull_request) Successful in 19s
CI / static-tests (pull_request) Successful in 39s
CI / js-tests (pull_request) Successful in 19s
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 18s
CI / static-tests (push) Successful in 39s
CI / js-tests (push) Successful in 16s
CI / auto-merge (pull_request) Successful in 4s
CI / auto-merge (push) Has been skipped
All checks were successful
CI / secret-scan (pull_request) Successful in 7s
CI / php-lint (pull_request) Successful in 19s
CI / static-tests (pull_request) Successful in 39s
CI / js-tests (pull_request) Successful in 19s
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 18s
CI / static-tests (push) Successful in 39s
CI / js-tests (push) Successful in 16s
CI / auto-merge (pull_request) Successful in 4s
CI / auto-merge (push) Has been skipped
PR-C du lot P3. Icone "i" allergenes sur la borne ouvrant une modale GENERALE listant les 14 allergenes a declaration obligatoire (UE INCO 1169/2011). Info generale, PAS un calcul par produit (mapping ingredient_allergen reste differe). - data/allergens.json : les 14 INCO (liste fixe data borne). loadAllergens() ajoute a data.js avec point de swap P4 documente (-> /api/allergens). - allergens.js : module CSP-safe (DOM API, aucun handler inline). buildAllergenInfoButton (bouton "i", stopPropagation sur la carte), openAllergenModal (idempotent), fermeture par bouton / clic overlay / touche Echap. - Integration : carte produit (page-products.js) ET fiche produit simple (page-product.js) ; CSS modale + badge reutilisant le pattern overlay du composer. Harnais de tests front (premier du depot) : node:test + jsdom. tests/js/allergens.test.js couvre les 14 INCO, la construction du bouton, l'ouverture/listing/fermeture et l'idempotence (7 tests). Scoping ESM local (src/public/borne/assets/js + tests/js) pour garder la racine en CommonJS (hooks, _byan, bin inchanges). Job CI js-tests (Node 20 epingle) ajoute aux checks requis de l'auto-merge.
This commit is contained in:
parent
ed392d4c14
commit
eb2891238f
12 changed files with 1015 additions and 4 deletions
|
|
@ -142,14 +142,39 @@ jobs:
|
||||||
# du correctif : plus aucun skip silencieux des chemins securite.
|
# du correctif : plus aucun skip silencieux des chemins securite.
|
||||||
php phpunit.phar -c phpunit.xml --fail-on-skipped
|
php phpunit.phar -c phpunit.xml --fail-on-skipped
|
||||||
|
|
||||||
|
js-tests:
|
||||||
|
# Tests du front borne (kiosk) : node:test + jsdom, sans navigateur.
|
||||||
|
# GARDE : ne s'active que si package.json + tests/js/ existent.
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Node.js 20
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
# Node 20 epingle via NodeSource (self-contained, comme les .phar/gitleaks)
|
||||||
|
# plutot que l'apt bookworm (18.x, limite basse pour jsdom). Reproductible.
|
||||||
|
apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null
|
||||||
|
apt-get install -y -qq nodejs >/dev/null
|
||||||
|
node --version && npm --version
|
||||||
|
- name: Install deps + run kiosk JS tests
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if [ ! -f package.json ] || [ ! -d tests/js ]; then
|
||||||
|
echo "JS tests skipped: no package.json + tests/js/ yet"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
npm ci
|
||||||
|
npm run test:js
|
||||||
|
|
||||||
auto-merge:
|
auto-merge:
|
||||||
# Fusion automatique OPT-IN : poser le label `auto-merge` sur la PR.
|
# Fusion automatique OPT-IN : poser le label `auto-merge` sur la PR.
|
||||||
# Ne s'execute que si les 3 checks passent (needs).
|
# Ne s'execute que si tous les checks requis passent (needs).
|
||||||
# IMPORTANT : le filtrage par label se fait DANS le step via l'API, pas dans
|
# IMPORTANT : le filtrage par label se fait DANS le step via l'API, pas dans
|
||||||
# `if:` — l'expression contains(github.event.pull_request.labels.*.name, ...)
|
# `if:` — l'expression contains(github.event.pull_request.labels.*.name, ...)
|
||||||
# de Forgejo n'est pas fiable (elle s'evalue a vrai meme sans label, ce qui
|
# de Forgejo n'est pas fiable (elle s'evalue a vrai meme sans label, ce qui
|
||||||
# fusionnait toute PR verte). La verification shell sur l'API est le vrai gate.
|
# fusionnait toute PR verte). La verification shell sur l'API est le vrai gate.
|
||||||
needs: [secret-scan, php-lint, static-tests]
|
needs: [secret-scan, php-lint, static-tests, js-tests]
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
545
package-lock.json
generated
Executable file
545
package-lock.json
generated
Executable file
|
|
@ -0,0 +1,545 @@
|
||||||
|
{
|
||||||
|
"name": "wakdo",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "wakdo",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"jsdom": "^26.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^2.1.3",
|
||||||
|
"@csstools/css-color-parser": "^3.0.9",
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.3",
|
||||||
|
"lru-cache": "^10.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^5.1.0",
|
||||||
|
"@csstools/css-calc": "^2.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cssstyle": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^3.2.0",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "26.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||||
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssstyle": "^4.2.1",
|
||||||
|
"data-urls": "^5.0.0",
|
||||||
|
"decimal.js": "^10.5.0",
|
||||||
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"nwsapi": "^2.2.16",
|
||||||
|
"parse5": "^7.2.1",
|
||||||
|
"rrweb-cssom": "^0.8.0",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^5.1.1",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0",
|
||||||
|
"whatwg-encoding": "^3.1.1",
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.1.1",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nwsapi": {
|
||||||
|
"version": "2.2.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
|
||||||
|
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^6.1.86"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^6.1.32"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
package.json
Executable file
12
package.json
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "wakdo",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Wakdo - tests front borne (kiosk). Back-office PHP teste via PHPUnit (phpunit.phar). NB: pas de \"type\":\"module\" a la racine -> les .js du depot (hooks .claude, _byan, bin) restent CommonJS. L'ESM est declare localement la ou il s'applique (src/public/borne/assets/js, tests/js).",
|
||||||
|
"scripts": {
|
||||||
|
"test:js": "node --test tests/js/"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jsdom": "^26.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1437,6 +1437,122 @@ button {
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* Allergenes — bouton "i" + modale generale (14 INCO)
|
||||||
|
* Info reglementaire generale (pas un calcul par produit). La modale reutilise
|
||||||
|
* le pattern overlay du composer ; z-index 220 pour passer au-dessus de lui.
|
||||||
|
* ============================================================ */
|
||||||
|
|
||||||
|
/* Le bouton "i" se superpose au coin de l'image de la carte. */
|
||||||
|
.product-card__image-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-info-btn {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--color-brand-dark);
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
color: var(--color-brand-dark);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: var(--shadow-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card__image-wrap .allergen-info-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-2);
|
||||||
|
right: var(--space-2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail__info .allergen-info-btn {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
z-index: 220;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-4);
|
||||||
|
animation: composer-fade-in var(--transition-base) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal {
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-overlay);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-6);
|
||||||
|
animation: composer-slide-up var(--transition-base) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-3);
|
||||||
|
right: var(--space-3);
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal-title {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal-intro {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-modal-list li {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-name {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.allergen-desc {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.composer-card__price {
|
.composer-card__price {
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
|
|
|
||||||
132
src/public/borne/assets/js/allergens.js
Normal file
132
src/public/borne/assets/js/allergens.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* allergens.js — Modale GENERALE d'information allergenes (front borne).
|
||||||
|
*
|
||||||
|
* Information reglementaire (UE INCO 1169/2011) presentee au client : la liste
|
||||||
|
* des 14 allergenes a declaration obligatoire. C'est une info GENERALE (pas un
|
||||||
|
* calcul par produit) ; le mapping ingredient_allergen par produit reste differe.
|
||||||
|
*
|
||||||
|
* CSP 'self' : aucun script inline, aucun handler inline. Le DOM est construit par
|
||||||
|
* l'API (createElement/textContent) ; textContent neutralise toute injection.
|
||||||
|
* Les donnees viennent de data.js (loadAllergens) : liste fixe en P5, /api/allergens
|
||||||
|
* au swap P4. openAllergenModal prend la liste en parametre pour rester independant
|
||||||
|
* de la couche de chargement (et testable sans fetch).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const OVERLAY_CLASS = 'allergen-modal-overlay';
|
||||||
|
|
||||||
|
/* Reference stable du handler clavier pour pouvoir le retirer a la fermeture. */
|
||||||
|
function onKeydown(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeAllergenModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit le bouton "i" qui ouvre la modale. `onOpen` est appele au clic ;
|
||||||
|
* la propagation est stoppee pour ne pas declencher le clic de la carte produit
|
||||||
|
* (sur la carte, le bouton est superpose a une zone cliquable).
|
||||||
|
* @param {() => void} onOpen
|
||||||
|
* @returns {HTMLButtonElement}
|
||||||
|
*/
|
||||||
|
export function buildAllergenInfoButton(onOpen) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'allergen-info-btn';
|
||||||
|
btn.setAttribute('aria-label', 'Informations allergenes');
|
||||||
|
btn.title = 'Informations allergenes';
|
||||||
|
btn.textContent = 'i';
|
||||||
|
btn.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (typeof onOpen === 'function') {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ouvre la modale generale listant les allergenes fournis. Idempotent : une
|
||||||
|
* eventuelle modale ouverte est d'abord fermee (pas de doublon empile).
|
||||||
|
* @param {Array<{id:number, name:string, description?:string}>} allergens
|
||||||
|
* @returns {HTMLElement} l'overlay cree
|
||||||
|
*/
|
||||||
|
export function openAllergenModal(allergens) {
|
||||||
|
closeAllergenModal();
|
||||||
|
|
||||||
|
const list = Array.isArray(allergens) ? allergens : [];
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = OVERLAY_CLASS;
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.setAttribute('aria-label', 'Informations allergenes');
|
||||||
|
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'allergen-modal';
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.type = 'button';
|
||||||
|
closeBtn.className = 'allergen-modal-close';
|
||||||
|
closeBtn.setAttribute('aria-label', 'Fermer');
|
||||||
|
closeBtn.textContent = 'x';
|
||||||
|
closeBtn.addEventListener('click', closeAllergenModal);
|
||||||
|
modal.appendChild(closeBtn);
|
||||||
|
|
||||||
|
const title = document.createElement('h2');
|
||||||
|
title.className = 'allergen-modal-title';
|
||||||
|
title.textContent = 'Allergenes';
|
||||||
|
modal.appendChild(title);
|
||||||
|
|
||||||
|
const intro = document.createElement('p');
|
||||||
|
intro.className = 'allergen-modal-intro';
|
||||||
|
intro.textContent = 'Les 14 allergenes a declaration obligatoire (reglement UE INCO 1169/2011). Pour toute question, demandez en caisse.';
|
||||||
|
modal.appendChild(intro);
|
||||||
|
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.className = 'allergen-modal-list';
|
||||||
|
for (const allergen of list) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
|
||||||
|
const name = document.createElement('span');
|
||||||
|
name.className = 'allergen-name';
|
||||||
|
name.textContent = String(allergen.name ?? '');
|
||||||
|
li.appendChild(name);
|
||||||
|
|
||||||
|
if (allergen.description) {
|
||||||
|
const desc = document.createElement('span');
|
||||||
|
desc.className = 'allergen-desc';
|
||||||
|
desc.textContent = ' - ' + String(allergen.description);
|
||||||
|
li.appendChild(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
modal.appendChild(ul);
|
||||||
|
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
|
||||||
|
// Clic sur le fond (hors du panneau) = fermeture ; clic dans le panneau, non.
|
||||||
|
overlay.addEventListener('click', (event) => {
|
||||||
|
if (event.target === overlay) {
|
||||||
|
closeAllergenModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', onKeydown);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ferme la modale si elle est ouverte et retire le handler clavier. Sans effet
|
||||||
|
* si aucune modale n'est ouverte (sur appel ou Echap repete).
|
||||||
|
*/
|
||||||
|
export function closeAllergenModal() {
|
||||||
|
const existing = document.querySelector('.' + OVERLAY_CLASS);
|
||||||
|
if (existing && existing.parentNode) {
|
||||||
|
existing.parentNode.removeChild(existing);
|
||||||
|
}
|
||||||
|
document.removeEventListener('keydown', onKeydown);
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
* ----------------------------------------------------------------------- */
|
* ----------------------------------------------------------------------- */
|
||||||
const CATEGORIES_URL = 'data/categories.json';
|
const CATEGORIES_URL = 'data/categories.json';
|
||||||
const PRODUCTS_URL = 'data/produits.json';
|
const PRODUCTS_URL = 'data/produits.json';
|
||||||
|
/* Liste fixe des 14 allergenes INCO (info generale, modale borne). TODO(P4):
|
||||||
|
* remplacer par '/api/allergens'. Le reste du fichier est API-agnostique. */
|
||||||
|
const ALLERGENS_URL = 'data/allergens.json';
|
||||||
|
|
||||||
/** @type {Array|null} — in-memory cache to avoid repeated fetches */
|
/** @type {Array|null} — in-memory cache to avoid repeated fetches */
|
||||||
let _categoriesCache = null;
|
let _categoriesCache = null;
|
||||||
|
|
@ -26,6 +29,9 @@ let _categoriesCache = null;
|
||||||
/** @type {Object|null} */
|
/** @type {Object|null} */
|
||||||
let _productsCache = null;
|
let _productsCache = null;
|
||||||
|
|
||||||
|
/** @type {Array|null} */
|
||||||
|
let _allergensCache = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches and caches the categories list.
|
* Fetches and caches the categories list.
|
||||||
* @returns {Promise<Array>}
|
* @returns {Promise<Array>}
|
||||||
|
|
@ -50,6 +56,18 @@ export async function loadProducts() {
|
||||||
return _productsCache;
|
return _productsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and caches the 14 INCO allergens (general info modal).
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
*/
|
||||||
|
export async function loadAllergens() {
|
||||||
|
if (_allergensCache) return _allergensCache;
|
||||||
|
const res = await fetch(ALLERGENS_URL);
|
||||||
|
if (!res.ok) throw new Error(`Failed to load allergens: HTTP ${res.status}`);
|
||||||
|
_allergensCache = await res.json();
|
||||||
|
return _allergensCache;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the array of products for a given category slug.
|
* Returns the array of products for a given category slug.
|
||||||
* Returns [] if the slug is not found.
|
* Returns [] if the slug is not found.
|
||||||
|
|
|
||||||
4
src/public/borne/assets/js/package.json
Normal file
4
src/public/borne/assets/js/package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"//": "Marque les scripts du kiosk comme ESM (import/export) pour Node (tests). Le navigateur les charge via <script type=module> independamment de ce fichier. Scoping local : evite d'imposer type:module a la racine (qui casserait les hooks CommonJS).",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,11 @@
|
||||||
* 3. Redirect to products.html?category=<slug>
|
* 3. Redirect to products.html?category=<slug>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { findProduct } from './data.js';
|
import { findProduct, loadAllergens } from './data.js';
|
||||||
import { addToCart, formatPrice, escHtml } from './state.js';
|
import { addToCart, formatPrice, escHtml } from './state.js';
|
||||||
import { refreshCartBadge } from './nav.js';
|
import { refreshCartBadge } from './nav.js';
|
||||||
import { openMenuComposer } from './page-product-menu.js';
|
import { openMenuComposer } from './page-product-menu.js';
|
||||||
|
import { buildAllergenInfoButton, openAllergenModal } from './allergens.js';
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const productId = parseInt(params.get('id'), 10);
|
const productId = parseInt(params.get('id'), 10);
|
||||||
|
|
@ -78,6 +79,18 @@ async function renderProduct() {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Bouton "i" allergenes (modale generale) dans le bloc info de la fiche.
|
||||||
|
// Echec de chargement non bloquant : la fiche reste fonctionnelle.
|
||||||
|
try {
|
||||||
|
const allergens = await loadAllergens();
|
||||||
|
const info = container.querySelector('.product-detail__info');
|
||||||
|
if (info) {
|
||||||
|
info.appendChild(buildAllergenInfoButton(() => openAllergenModal(allergens)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadAllergens error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('add-to-cart-btn').addEventListener('click', () => {
|
document.getElementById('add-to-cart-btn').addEventListener('click', () => {
|
||||||
addToCart({
|
addToCart({
|
||||||
id: product.id,
|
id: product.id,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
* On product card click, navigates to product.html?id=<id>&category=<slug>.
|
* On product card click, navigates to product.html?id=<id>&category=<slug>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
|
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG, loadAllergens } from './data.js';
|
||||||
import { formatPrice, escHtml } from './state.js';
|
import { formatPrice, escHtml } from './state.js';
|
||||||
|
import { buildAllergenInfoButton, openAllergenModal } from './allergens.js';
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const categoryId = parseInt(params.get('category'), 10) || 1;
|
const categoryId = parseInt(params.get('category'), 10) || 1;
|
||||||
|
|
@ -49,6 +50,15 @@ async function renderProducts() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Liste generale des allergenes (modale "i"). Chargee une fois, partagee par
|
||||||
|
// toutes les cartes ; un echec ne doit pas casser l'affichage produits.
|
||||||
|
let allergens = [];
|
||||||
|
try {
|
||||||
|
allergens = await loadAllergens();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadAllergens error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
products.forEach(product => {
|
products.forEach(product => {
|
||||||
const card = document.createElement('a');
|
const card = document.createElement('a');
|
||||||
|
|
@ -71,6 +81,12 @@ async function renderProducts() {
|
||||||
<span class="product-card__price">${formatPrice(product.prix)}</span>
|
<span class="product-card__price">${formatPrice(product.prix)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Bouton "i" allergenes superpose a l'image ; son clic ouvre la modale
|
||||||
|
// generale et ne declenche pas la navigation de la carte (stopPropagation).
|
||||||
|
const infoBtn = buildAllergenInfoButton(() => openAllergenModal(allergens));
|
||||||
|
card.querySelector('.product-card__image-wrap').appendChild(infoBtn);
|
||||||
|
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
16
src/public/borne/data/allergens.json
Normal file
16
src/public/borne/data/allergens.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[
|
||||||
|
{ "id": 1, "name": "Cereales contenant du gluten", "description": "Ble, seigle, orge, avoine, epeautre, kamut et produits derives." },
|
||||||
|
{ "id": 2, "name": "Crustaces", "description": "Et produits a base de crustaces." },
|
||||||
|
{ "id": 3, "name": "Oeufs", "description": "Et produits a base d'oeufs." },
|
||||||
|
{ "id": 4, "name": "Poissons", "description": "Et produits a base de poissons." },
|
||||||
|
{ "id": 5, "name": "Arachides", "description": "Et produits a base d'arachides." },
|
||||||
|
{ "id": 6, "name": "Soja", "description": "Et produits a base de soja." },
|
||||||
|
{ "id": 7, "name": "Lait", "description": "Et produits a base de lait (y compris le lactose)." },
|
||||||
|
{ "id": 8, "name": "Fruits a coque", "description": "Amandes, noisettes, noix, noix de cajou, pistaches et autres." },
|
||||||
|
{ "id": 9, "name": "Celeri", "description": "Et produits a base de celeri." },
|
||||||
|
{ "id": 10, "name": "Moutarde", "description": "Et produits a base de moutarde." },
|
||||||
|
{ "id": 11, "name": "Graines de sesame", "description": "Et produits a base de graines de sesame." },
|
||||||
|
{ "id": 12, "name": "Anhydride sulfureux et sulfites", "description": "En concentration de plus de 10 mg/kg ou 10 mg/l." },
|
||||||
|
{ "id": 13, "name": "Lupin", "description": "Et produits a base de lupin." },
|
||||||
|
{ "id": 14, "name": "Mollusques", "description": "Et produits a base de mollusques." }
|
||||||
|
]
|
||||||
110
tests/js/allergens.test.js
Normal file
110
tests/js/allergens.test.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Tests du module allergens du front borne (node:test + jsdom).
|
||||||
|
*
|
||||||
|
* Couvre le contrat de PR-C : la liste fixe des 14 allergenes INCO (data borne,
|
||||||
|
* se branchera sur /api/allergens au swap P4), la construction du bouton "i", et
|
||||||
|
* la modale GENERALE (ouverture, listing des 14, fermeture par bouton/overlay/
|
||||||
|
* Escape, idempotence). DOM simule par jsdom : aucun navigateur requis.
|
||||||
|
*/
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildAllergenInfoButton,
|
||||||
|
openAllergenModal,
|
||||||
|
closeAllergenModal,
|
||||||
|
} from '../../src/public/borne/assets/js/allergens.js';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const allergensJsonPath = join(here, '../../src/public/borne/data/allergens.json');
|
||||||
|
|
||||||
|
function setupDom() {
|
||||||
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||||
|
global.window = dom.window;
|
||||||
|
global.document = dom.window.document;
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAllergensFixture() {
|
||||||
|
return JSON.parse(readFileSync(allergensJsonPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('data/allergens.json liste exactement les 14 allergenes INCO', () => {
|
||||||
|
const list = loadAllergensFixture();
|
||||||
|
assert.ok(Array.isArray(list));
|
||||||
|
assert.equal(list.length, 14);
|
||||||
|
for (const a of list) {
|
||||||
|
assert.equal(typeof a.id, 'number');
|
||||||
|
assert.equal(typeof a.name, 'string');
|
||||||
|
assert.ok(a.name.trim().length > 0);
|
||||||
|
}
|
||||||
|
const names = list.map((a) => a.name);
|
||||||
|
assert.equal(new Set(names).size, 14, 'noms uniques');
|
||||||
|
// Quelques jalons de la liste reglementaire (UE INCO 1169/2011 annexe II).
|
||||||
|
const joined = names.join(' | ').toLowerCase();
|
||||||
|
for (const expected of ['gluten', 'lait', 'arachide', 'soja', 'mollusque']) {
|
||||||
|
assert.ok(joined.includes(expected), `attendu: ${expected}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildAllergenInfoButton cree un bouton "i" qui declenche onOpen', () => {
|
||||||
|
setupDom();
|
||||||
|
let opened = 0;
|
||||||
|
const btn = buildAllergenInfoButton(() => { opened += 1; });
|
||||||
|
|
||||||
|
assert.equal(btn.tagName, 'BUTTON');
|
||||||
|
assert.equal(btn.type, 'button');
|
||||||
|
assert.ok(btn.className.includes('allergen-info-btn'));
|
||||||
|
assert.ok(btn.getAttribute('aria-label'));
|
||||||
|
|
||||||
|
btn.click();
|
||||||
|
assert.equal(opened, 1, 'le clic ouvre la modale');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openAllergenModal affiche une modale listant les 14 allergenes', () => {
|
||||||
|
setupDom();
|
||||||
|
const list = loadAllergensFixture();
|
||||||
|
const overlay = openAllergenModal(list);
|
||||||
|
|
||||||
|
assert.ok(document.body.contains(overlay));
|
||||||
|
assert.equal(overlay.getAttribute('role'), 'dialog');
|
||||||
|
assert.equal(overlay.getAttribute('aria-modal'), 'true');
|
||||||
|
const items = overlay.querySelectorAll('.allergen-modal-list li');
|
||||||
|
assert.equal(items.length, 14);
|
||||||
|
assert.ok(overlay.textContent.toLowerCase().includes('lait'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la modale se ferme via le bouton de fermeture', () => {
|
||||||
|
setupDom();
|
||||||
|
openAllergenModal(loadAllergensFixture());
|
||||||
|
document.querySelector('.allergen-modal-close').click();
|
||||||
|
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la modale se ferme par clic sur l overlay (hors contenu)', () => {
|
||||||
|
const dom = setupDom();
|
||||||
|
const overlay = openAllergenModal(loadAllergensFixture());
|
||||||
|
overlay.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
|
||||||
|
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la modale se ferme avec la touche Echap', () => {
|
||||||
|
const dom = setupDom();
|
||||||
|
openAllergenModal(loadAllergensFixture());
|
||||||
|
document.dispatchEvent(new dom.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
|
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ouvrir deux fois ne duplique pas la modale (idempotent)', () => {
|
||||||
|
setupDom();
|
||||||
|
const list = loadAllergensFixture();
|
||||||
|
openAllergenModal(list);
|
||||||
|
openAllergenModal(list);
|
||||||
|
assert.equal(document.querySelectorAll('.allergen-modal-overlay').length, 1);
|
||||||
|
closeAllergenModal();
|
||||||
|
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
|
||||||
|
});
|
||||||
4
tests/js/package.json
Normal file
4
tests/js/package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"//": "Les tests front borne sont en ESM (import). Scoping local pour ne pas imposer type:module a la racine.",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue