From 1ecd78324c2659ddeab3a642d95f840396c4c1ab Mon Sep 17 00:00:00 2001 From: Corentin JOGUET Date: Wed, 17 Jun 2026 12:10:46 +0200 Subject: [PATCH] feat(borne): modale allergenes generale (14 INCO) sur carte et fiche + harnais tests JS (P3) (#36) --- .forgejo/workflows/ci.yml | 29 +- package-lock.json | 545 ++++++++++++++++++++ package.json | 12 + src/public/borne/assets/css/style.css | 116 +++++ src/public/borne/assets/js/allergens.js | 132 +++++ src/public/borne/assets/js/data.js | 18 + src/public/borne/assets/js/package.json | 4 + src/public/borne/assets/js/page-product.js | 15 +- src/public/borne/assets/js/page-products.js | 18 +- src/public/borne/data/allergens.json | 16 + tests/js/allergens.test.js | 110 ++++ tests/js/package.json | 4 + 12 files changed, 1015 insertions(+), 4 deletions(-) create mode 100755 package-lock.json create mode 100755 package.json create mode 100644 src/public/borne/assets/js/allergens.js create mode 100644 src/public/borne/assets/js/package.json create mode 100644 src/public/borne/data/allergens.json create mode 100644 tests/js/allergens.test.js create mode 100644 tests/js/package.json diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 02ddf7a..95ccd8e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -142,14 +142,39 @@ jobs: # du correctif : plus aucun skip silencieux des chemins securite. 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: # 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 # `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 # 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' runs-on: docker steps: diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..25a3089 --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..eec3096 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/public/borne/assets/css/style.css b/src/public/borne/assets/css/style.css index 5f11afb..3cff763 100644 --- a/src/public/borne/assets/css/style.css +++ b/src/public/borne/assets/css/style.css @@ -1437,6 +1437,122 @@ button { 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 { font-size: var(--font-size-sm); color: var(--color-text-muted); diff --git a/src/public/borne/assets/js/allergens.js b/src/public/borne/assets/js/allergens.js new file mode 100644 index 0000000..62f1244 --- /dev/null +++ b/src/public/borne/assets/js/allergens.js @@ -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); +} diff --git a/src/public/borne/assets/js/data.js b/src/public/borne/assets/js/data.js index 38f03b1..378d82c 100644 --- a/src/public/borne/assets/js/data.js +++ b/src/public/borne/assets/js/data.js @@ -19,6 +19,9 @@ * ----------------------------------------------------------------------- */ const CATEGORIES_URL = 'data/categories.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 */ let _categoriesCache = null; @@ -26,6 +29,9 @@ let _categoriesCache = null; /** @type {Object|null} */ let _productsCache = null; +/** @type {Array|null} */ +let _allergensCache = null; + /** * Fetches and caches the categories list. * @returns {Promise} @@ -50,6 +56,18 @@ export async function loadProducts() { return _productsCache; } +/** + * Fetches and caches the 14 INCO allergens (general info modal). + * @returns {Promise} + */ +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 [] if the slug is not found. diff --git a/src/public/borne/assets/js/package.json b/src/public/borne/assets/js/package.json new file mode 100644 index 0000000..a6f8a22 --- /dev/null +++ b/src/public/borne/assets/js/package.json @@ -0,0 +1,4 @@ +{ + "//": "Marque les scripts du kiosk comme ESM (import/export) pour Node (tests). Le navigateur les charge via