From 1dee8b528f82ca08d0f8dd4d3d36193c8f8cbb26 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Tue, 21 Jul 2026 10:09:25 +0200 Subject: [PATCH] Migrate to React/Vite & Sass --- .gitignore | 1 + README.md | 59 +- package-lock.json | 1295 ++++++++++ package.json | 15 +- server.mjs | 9 +- tests/static-smoke.test.mjs | 107 +- vite.config.js | 39 + website/{public => }/index.html | 5 +- website/public/data/site.json | 2 +- website/public/static/icons/zoom.svg | 17 + website/src/app.js | 2300 ----------------- website/src/components/Icon.jsx | 4 + website/src/features/games/GameRoute.jsx | 30 + website/src/features/games/GamesPage.jsx | 35 + .../features/games/mhwilds/MhwildsFilters.jsx | 54 + .../features/games/mhwilds/MhwildsListing.jsx | 52 + .../games/mhwilds/MhwildsOverview.jsx | 27 + .../features/games/mhwilds/MhwildsPage.jsx | 45 + .../games/mhwilds/cards/DamageTable.jsx | 34 + .../games/mhwilds/cards/EndemicCard.jsx | 21 + .../features/games/mhwilds/cards/IconRow.jsx | 14 + .../games/mhwilds/cards/MonsterCard.jsx | 47 + website/src/features/games/mhwilds/utils.js | 38 + .../toolboxes/modules/ChecklistModule.jsx | 66 + .../toolboxes/modules/NotepadModule.jsx | 18 + .../toolboxes/modules/ScreenshotsModule.jsx | 72 + .../src/features/toolboxes/modules/index.jsx | 132 + website/src/main.jsx | 1205 +++++++++ website/src/styles/_tokens.scss | 89 + website/src/{styles.css => styles/main.scss} | 103 +- 30 files changed, 3491 insertions(+), 2444 deletions(-) create mode 100644 package-lock.json create mode 100644 vite.config.js rename website/{public => }/index.html (70%) create mode 100644 website/public/static/icons/zoom.svg delete mode 100644 website/src/app.js create mode 100644 website/src/components/Icon.jsx create mode 100644 website/src/features/games/GameRoute.jsx create mode 100644 website/src/features/games/GamesPage.jsx create mode 100644 website/src/features/games/mhwilds/MhwildsFilters.jsx create mode 100644 website/src/features/games/mhwilds/MhwildsListing.jsx create mode 100644 website/src/features/games/mhwilds/MhwildsOverview.jsx create mode 100644 website/src/features/games/mhwilds/MhwildsPage.jsx create mode 100644 website/src/features/games/mhwilds/cards/DamageTable.jsx create mode 100644 website/src/features/games/mhwilds/cards/EndemicCard.jsx create mode 100644 website/src/features/games/mhwilds/cards/IconRow.jsx create mode 100644 website/src/features/games/mhwilds/cards/MonsterCard.jsx create mode 100644 website/src/features/games/mhwilds/utils.js create mode 100644 website/src/features/toolboxes/modules/ChecklistModule.jsx create mode 100644 website/src/features/toolboxes/modules/NotepadModule.jsx create mode 100644 website/src/features/toolboxes/modules/ScreenshotsModule.jsx create mode 100644 website/src/features/toolboxes/modules/index.jsx create mode 100644 website/src/main.jsx create mode 100644 website/src/styles/_tokens.scss rename website/src/{styles.css => styles/main.scss} (94%) diff --git a/.gitignore b/.gitignore index 5a00c13..e466090 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ +website/dist/ .env .DS_Store diff --git a/README.md b/README.md index 8d8b827..955f823 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sokko G -Webapp locale pour préparer et accompagner des sessions gaming sur second écran. +Webapp React locale pour préparer et accompagner des sessions gaming sur second écran. Sokko G propose des toolboxes modulaires stockées dans le navigateur, ainsi que des pages de guides de jeu maintenues dans le dépôt. Aucune donnée utilisateur n’est envoyée côté serveur. @@ -23,6 +23,15 @@ npm run dev Par défaut, le site démarre sur `http://localhost:5173`. +Pour générer la version statique de production : + +```bash +npm run build +``` + +Le build est généré dans `website/dist`. +`npm start` sert cette version statique avec `server.mjs`. + ## Configuration Le serveur lit automatiquement un fichier `.env` à la racine du projet. @@ -47,7 +56,7 @@ Avant un push ou une mise en ligne : npm run check ``` -Cette commande vérifie la syntaxe de `website/src/app.js`, celle de `server.mjs`, puis lance les tests. +Cette commande vérifie la configuration Node/Vite, lance les tests, puis exécute le build React. ## Contenu éditable @@ -65,6 +74,35 @@ Les contenus maintenus à la main sont regroupés dans `website/public/data`. Les images publiques sont dans `website/public/static`. +## Pages jeux + +Les pages jeux sont dans `website/src/features/games`. + +- `website/src/features/games/GamesPage.jsx` : liste des jeux disponibles. +- `website/src/features/games/GameRoute.jsx` : route vers la page du jeu demandé. +- `website/src/features/games/mhwilds/` : vues et composants propres à Monster Hunter Wilds. + +Pour ajouter un jeu : + +- ajouter son entrée dans `website/public/data/games.json` ; +- créer son dossier dans `website/src/features/games/` si la page nécessite un rendu spécifique ; +- brancher sa route dans `website/src/features/games/GameRoute.jsx`. + +## Outils de toolbox + +Les outils de toolbox sont déclarés dans `website/src/features/toolboxes/modules/index.jsx`. +Chaque outil possède son propre fichier de composant : + +- `website/src/features/toolboxes/modules/NotepadModule.jsx` +- `website/src/features/toolboxes/modules/ChecklistModule.jsx` +- `website/src/features/toolboxes/modules/ScreenshotsModule.jsx` + +Pour ajouter ou maintenir un outil : + +- créer son fichier dans `website/src/features/toolboxes/modules/` ; +- l’ajouter au registre `MODULE_COMPONENTS` dans `website/src/features/toolboxes/modules/index.jsx` ; +- garder les données persistées via les helpers de `website/src/main.jsx` tant que le stockage reste en localStorage. + ## Structure ```text @@ -72,15 +110,24 @@ Les images publiques sont dans `website/public/static`. ├── DESIGN_SYSTEM.md ├── package.json ├── server.mjs +├── vite.config.js ├── tests/ └── website/ + ├── index.html ├── public/ │ ├── data/ - │ ├── static/ - │ └── index.html + │ └── static/ └── src/ - ├── app.js - └── styles.css + ├── main.jsx + ├── components/ + ├── features/ + │ ├── games/ + │ │ └── mhwilds/ + │ └── toolboxes/ + │ └── modules/ + └── styles/ + ├── _tokens.scss + └── main.scss ``` ## Stockage local diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..27ea61e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1295 @@ +{ + "name": "sokko-g", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sokko-g", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^6.0.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "vite": "^8.1.5" + }, + "devDependencies": { + "sass": "^1.101.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/sass": { + "version": "1.101.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.3.tgz", + "integrity": "sha512-Z1lLHhtAII+dyLNIQB6JQTZMy7sDxk3f5NzbINRc9ks1P0HCGvSuKev0wUhULFpLSaHBIMZrcTs9WDQUZerrgA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json index 04b5cb6..4c15a5f 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,19 @@ "type": "module", "scripts": { "start": "node server.mjs", - "dev": "node server.mjs", - "check": "node --check website/src/app.js && node --check server.mjs && npm test", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "node --check server.mjs && node --check vite.config.js && npm test && npm run build", "test": "node --test" + }, + "dependencies": { + "@vitejs/plugin-react": "^6.0.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "vite": "^8.1.5" + }, + "devDependencies": { + "sass": "^1.101.3" } } diff --git a/server.mjs b/server.mjs index 35bbafb..be8b420 100644 --- a/server.mjs +++ b/server.mjs @@ -3,8 +3,11 @@ import { createServer } from "node:http"; import { extname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const root = resolve(fileURLToPath(new URL(".", import.meta.url)), "website"); -const publicRoot = join(root, "public"); +const projectRoot = resolve(fileURLToPath(new URL(".", import.meta.url))); +const websiteRoot = join(projectRoot, "website"); +const distRoot = join(websiteRoot, "dist"); +const root = existsSync(distRoot) ? distRoot : websiteRoot; +const publicRoot = existsSync(distRoot) ? distRoot : join(websiteRoot, "public"); function loadEnvFile(file = ".env") { if (!existsSync(file)) return; @@ -63,7 +66,7 @@ function resolvePath(url) { const publicCandidate = fileIfReadable(resolve(publicRoot, `.${requested}`)); if (publicCandidate && isInsideRoot(publicRoot, publicCandidate)) return publicCandidate; - return join(publicRoot, "index.html"); + return join(root, "index.html"); } const server = createServer((req, res) => { diff --git a/tests/static-smoke.test.mjs b/tests/static-smoke.test.mjs index b59ae26..8e55779 100644 --- a/tests/static-smoke.test.mjs +++ b/tests/static-smoke.test.mjs @@ -2,59 +2,85 @@ import { readFile } from "node:fs/promises"; import { test } from "node:test"; import assert from "node:assert/strict"; -test("static entrypoint loads the application assets", async () => { - const html = await readFile("website/public/index.html", "utf8"); +test("vite entrypoint loads the react application", async () => { + const html = await readFile("website/index.html", "utf8"); assert.match(html, /
<\/div>/); assert.match(html, /\/favicon\.ico/); - assert.match(html, /\/src\/app\.js/); - assert.match(html, /\/src\/styles\.css/); + assert.match(html, /\/src\/main\.jsx/); }); -test("application defines the expected local toolbox primitives", async () => { - const source = await readFile("website/src/app.js", "utf8"); +test("react application defines the expected local toolbox primitives", async () => { + const source = await readFile("website/src/main.jsx", "utf8"); + const styles = await readFile("website/src/styles/main.scss", "utf8"); + const styleTokens = await readFile("website/src/styles/_tokens.scss", "utf8"); + const iconComponent = await readFile("website/src/components/Icon.jsx", "utf8"); + const gamesPage = await readFile("website/src/features/games/GamesPage.jsx", "utf8"); + const gameRoute = await readFile("website/src/features/games/GameRoute.jsx", "utf8"); + const mhwildsPage = await readFile("website/src/features/games/mhwilds/MhwildsPage.jsx", "utf8"); + const mhwildsListing = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8"); + const mhwildsFilters = await readFile("website/src/features/games/mhwilds/MhwildsFilters.jsx", "utf8"); + const monsterCard = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8"); + const endemicCard = await readFile("website/src/features/games/mhwilds/cards/EndemicCard.jsx", "utf8"); + const damageTable = await readFile("website/src/features/games/mhwilds/cards/DamageTable.jsx", "utf8"); + const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8"); + const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8"); + const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8"); + const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8"); assert.match(source, /sokkog:toolboxes/); assert.match(source, /sokkog:game-toolbox-links/); + assert.match(source, /styles\/main\.scss/); + assert.match(styles, /@use "tokens"/); + assert.match(styleTokens, /--gradient-brand/); + assert.match(source, /features\/toolboxes\/modules\/index\.jsx/); + assert.match(source, /features\/games\/GamesPage\.jsx/); + assert.match(source, /features\/games\/GameRoute\.jsx/); + assert.match(iconComponent, /export function Icon/); + assert.match(gamesPage, /export function GamesPage/); + assert.match(gameRoute, /export function GameRoute/); + assert.match(gameRoute, /MhwildsPage/); + assert.match(mhwildsPage, /export function MhwildsPage/); + assert.match(mhwildsListing, /export function MhwildsListing/); + assert.match(mhwildsFilters, /export function MhwildsFilters/); + assert.match(monsterCard, /export function MonsterCard/); + assert.match(endemicCard, /export function EndemicCard/); + assert.match(damageTable, /rowIndex/); + assert.match(moduleRegistry, /export const TOOLBOX_MODULES/); + assert.match(moduleRegistry, /export function ToolboxModules/); + assert.match(moduleRegistry, /MODULE_COMPONENTS/); + assert.match(moduleRegistry, /notepad:/); + assert.match(moduleRegistry, /checklist:/); + assert.match(moduleRegistry, /screenshots:/); + assert.match(notepadModule, /export function NotepadModule/); + assert.match(checklistModule, /export function ChecklistModule/); + assert.match(screenshotsModule, /export function ScreenshotsModule/); assert.match(source, /DEFAULT_SITE_CONTENT/); - assert.match(source, /function loadSiteContent/); assert.match(source, /\/data\/site\.json/); assert.match(source, /APP_STORAGE_LIMIT_BYTES/); - assert.match(source, /function renderStorageQuota/); + assert.match(source, /function StorageQuota/); assert.match(source, /function writeStorageValue/); assert.match(source, /role="progressbar"/); - assert.match(source, /export-all-toolboxes/); - assert.match(source, /import-all-toolboxes/); assert.match(source, /function createGlobalExportPayload/); - assert.match(source, /function importAllToolboxes/); - assert.match(source, /open-link-toolbox-modal/); - assert.match(source, /function showToolboxLinkModal/); - assert.match(source, /data-game-id/); - assert.match(source, /notepad/); - assert.match(source, /checklist/); - assert.match(source, /screenshots/); + assert.match(source, /async function importAllToolboxes/); + assert.match(source, /function LinkToolboxModal/); assert.doesNotMatch(source, /description: _description/); - assert.match(source, /function showToolboxCreateModal/); - assert.match(source, /data-action="toolbox-create-form"/); - assert.match(source, /function getToolboxGameId/); + assert.match(source, /function CreateToolboxModal/); + assert.match(source, /const getToolboxGameId/); assert.match(source, /dragon\.png/); - assert.match(source, /const game = getToolboxGame\(toolbox\)/); + assert.match(source, /getToolboxGame\(toolbox\)/); assert.match(source, /moduleColumns/); - assert.match(source, /data-action="set-module-layout"/); - assert.match(source, /data-action="edit-toolbox-title"/); - assert.match(source, /function renderToolboxModules/); - assert.match(source, /function moveToolboxModule/); - assert.match(source, /dragstart/); + assert.match(source, /function ToolboxView/); + assert.match(moduleRegistry, /onDragStart/); assert.match(source, /qtyTarget/); assert.match(source, /qtyCurrent/); - assert.match(source, /adjust-check-qty/); - assert.match(source, /paste-screenshot/); - assert.match(source, /clipboardData/); - assert.match(source, /function addScreenshotFiles/); - assert.match(source, /view-screenshot/); - assert.match(source, /function showScreenshotViewer/); - assert.match(source, /drop-screenshot/); - assert.match(source, /dataTransfer\.files/); + assert.match(checklistModule, /ChecklistItem/); + assert.match(screenshotsModule, /clipboardData/); + assert.match(source, /async function addScreenshotFiles/); + assert.match(source, /function ScreenshotViewer/); + assert.match(source, /function openImageInNewTab/); + assert.match(source, / { @@ -63,7 +89,7 @@ test("mhwilds data and assets are available", async () => { const monsters = JSON.parse(await readFile("website/public/data/mhwilds/monsters.json", "utf8")); const endemicLife = JSON.parse(await readFile("website/public/data/mhwilds/endemic_life.json", "utf8")); const translations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/fr.json", "utf8")); - const appSource = await readFile("website/src/app.js", "utf8"); + const listingSource = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8"); assert.equal(site.brand.name, "Sokko G"); assert.equal(site.home.hero.title, "Sokko G"); @@ -74,17 +100,21 @@ test("mhwilds data and assets are available", async () => { assert.ok(monsters.monsters.length > 0); assert.ok(endemicLife.endemicLife.length > 0); assert.equal(translations.monsters, "monstres"); - assert.match(appSource, /renderMhwildsListing/); - assert.match(appSource, /flip-monster-card/); + assert.match(listingSource, /function MhwildsListing/); + const monsterCardSource = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8"); + assert.match(monsterCardSource, /monster-card/); await readFile("website/public/static/img/mhwilds/chatacabra.png"); }); -test("server supports local env configuration", async () => { +test("server and vite support local env configuration", async () => { const server = await readFile("server.mjs", "utf8"); + const viteConfig = await readFile("vite.config.js", "utf8"); const envExample = await readFile(".env.example", "utf8"); assert.match(server, /function loadEnvFile/); assert.match(server, /process\.env\.PORT/); + assert.match(viteConfig, /function loadEnvFile/); + assert.match(viteConfig, /process\.env\.PORT/); assert.match(envExample, /PORT=5173/); }); @@ -102,6 +132,7 @@ test("legacy svg icons are available for reuse", async () => { await readFile("website/public/static/icons/rows.svg", "utf8"); await readFile("website/public/static/icons/rubber.svg", "utf8"); await readFile("website/public/static/icons/trashcan.svg", "utf8"); + await readFile("website/public/static/icons/zoom.svg", "utf8"); await readFile("website/public/favicon.ico"); await readFile("website/public/static/img/dragon.png"); await readFile("website/public/static/img/toolbox-icons/controller.svg", "utf8"); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c850759 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,39 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { existsSync, readFileSync } from "node:fs"; + +function loadEnvFile(file = ".env") { + if (!existsSync(file)) return; + + readFileSync(file, "utf8").split(/\r?\n/).forEach((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) return; + + const separatorIndex = trimmed.indexOf("="); + if (separatorIndex < 1) return; + + const key = trimmed.slice(0, separatorIndex).trim(); + const value = trimmed.slice(separatorIndex + 1).trim().replace(/^["']|["']$/g, ""); + if (!process.env[key]) process.env[key] = value; + }); +} + +loadEnvFile(); + +export default defineConfig({ + root: "website", + publicDir: "public", + plugins: [react()], + server: { + host: "0.0.0.0", + port: Number(process.env.PORT || 5173) + }, + preview: { + host: "0.0.0.0", + port: Number(process.env.PORT || 4173) + }, + build: { + outDir: "dist", + emptyOutDir: true + } +}); diff --git a/website/public/index.html b/website/index.html similarity index 70% rename from website/public/index.html rename to website/index.html index c0c10ee..2acf2da 100644 --- a/website/public/index.html +++ b/website/index.html @@ -10,12 +10,9 @@ Sokko G - - -
- + diff --git a/website/public/data/site.json b/website/public/data/site.json index 3a0133f..74584ad 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -6,7 +6,7 @@ "navigation": { "home": "Accueil", "toolboxes": "Toolboxes", - "games": "Pages jeux", + "games": "Jeux", "mobileGames": "Infos" }, "sidebar": { diff --git a/website/public/static/icons/zoom.svg b/website/public/static/icons/zoom.svg new file mode 100644 index 0000000..ecac73b --- /dev/null +++ b/website/public/static/icons/zoom.svg @@ -0,0 +1,17 @@ + + + + + zoom + Created with Sketch Beta. + + + + + + + + + + + \ No newline at end of file diff --git a/website/src/app.js b/website/src/app.js deleted file mode 100644 index 1f282b3..0000000 --- a/website/src/app.js +++ /dev/null @@ -1,2300 +0,0 @@ -const STORAGE_KEYS = { - registry: "sokkog:toolboxes", - links: "sokkog:game-toolbox-links", - drawerWidth: "sokkog:drawer-width" -}; -const APP_STORAGE_PREFIX = "sokkog:"; -const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024; -const APP_STORAGE_WARNING_RATIO = 0.85; - -const MODULES = { - notepad: { label: "Bloc notes", icon: "notepad" }, - checklist: { label: "Checklist", icon: "checklist" }, - screenshots: { label: "Screenshots", icon: "picture" } -}; - -const DEFAULT_SITE_CONTENT = { - brand: { name: "Sokko G", homeAriaLabel: "Accueil Sokko G" }, - navigation: { home: "Accueil", toolboxes: "Toolboxes", games: "Pages jeux", mobileGames: "Infos" }, - sidebar: { badge: "Local only", note: "Données stockées dans ce navigateur." }, - topbar: { dashboard: "Dashboard", toolbox: "Toolbox active", games: "Informations jeu" }, - home: { - hero: { - eyebrow: "Session gaming efficace", - title: "Sokko G", - description: "Centralisez vos outils et repères de jeu dans une interface locale, rapide à consulter, pensée pour accompagner vos sessions sans interrompre l’action.", - primaryAction: "Voir les toolboxes", - secondaryAction: "Voir les jeux" - }, - stats: { - toolboxSingular: "toolbox créée", - toolboxPlural: "toolboxs créées", - toolSingular: "outil disponible", - toolPlural: "outils disponibles" - }, - origin: { - eyebrow: "Origine du nom", - title: "Pourquoi Sokko G ?", - visualAlt: "Dragon Sokko G", - dialogueAriaLabel: "Dialogue d’origine du nom Sokko G", - caption: "\"G\" c'est pour Gaming", - lines: [] - } - }, - toolboxes: { - eyebrow: "Données locales", - title: "Toolboxes", - newButton: "Nouvelle toolbox", - importAll: "Importer tout", - exportAll: "Exporter tout", - importOne: "Importer une toolbox", - storageHelp: { - title: "Sauvegardes locales", - text: "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.", - items: [ - "Un nettoyage du navigateur ou un changement d’appareil peut supprimer les données.", - "Surveillez le quota, surtout si vous ajoutez des screenshots.", - "Faites un export global régulier pour garder une sauvegarde." - ] - }, - emptyTitle: "Aucune toolbox", - emptyText: "Créez une première toolbox pour stocker vos outils dans ce navigateur." - } -}; - -const app = document.querySelector("#app"); -const MHWILDS_IMG_PATH = "/static/img/mhwilds"; -const siteState = { - loaded: false, - loading: false, - content: DEFAULT_SITE_CONTENT -}; -const gamesState = { - loaded: false, - loading: false, - error: "", - games: [] -}; -const mhwildsState = { - loaded: false, - loading: false, - error: "", - translations: {}, - monsters: [], - endemic: [], - filters: { - monsters: { name: "", weaknesses: [], logic: "and" }, - endemic: { name: "", locations: [], logic: "and" } - } -}; -let draggedModule = null; - -const ID_PREFIXES = { - tbx: "t", - mod: "m", - item: "i", - shot: "s" -}; -const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"; - -function randomToken(length = 6) { - const bytes = new Uint8Array(length); - if (globalThis.crypto?.getRandomValues) { - globalThis.crypto.getRandomValues(bytes); - return [...bytes].map((byte) => ID_ALPHABET[byte % ID_ALPHABET.length]).join(""); - } - - return Math.random().toString(36).slice(2, 2 + length).padEnd(length, "0"); -} - -function uid(prefix) { - return `${ID_PREFIXES[prefix] || "x"}${randomToken()}`; -} - -function readJson(key, fallback) { - try { - const value = localStorage.getItem(key); - return value ? JSON.parse(value) : fallback; - } catch { - return fallback; - } -} - -function writeJson(key, value) { - return writeStorageValue(key, JSON.stringify(value)); -} - -function stringStorageBytes(value) { - return String(value || "").length * 2; -} - -function getAppStorageUsage(projected = {}) { - let used = 0; - for (let index = 0; index < localStorage.length; index += 1) { - const key = localStorage.key(index); - if (!key?.startsWith(APP_STORAGE_PREFIX) || Object.prototype.hasOwnProperty.call(projected, key)) continue; - used += stringStorageBytes(key) + stringStorageBytes(localStorage.getItem(key)); - } - - Object.entries(projected).forEach(([key, value]) => { - if (!key.startsWith(APP_STORAGE_PREFIX) || value == null) return; - used += stringStorageBytes(key) + stringStorageBytes(value); - }); - - return { - used, - limit: APP_STORAGE_LIMIT_BYTES, - ratio: Math.min(1, used / APP_STORAGE_LIMIT_BYTES) - }; -} - -function formatBytes(bytes) { - if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1).replace(".", ",")} Mio`; - if (bytes >= 1024) return `${Math.ceil(bytes / 1024)} Kio`; - return `${bytes} o`; -} - -function renderStorageQuota() { - const usage = getAppStorageUsage(); - const percent = Math.round(usage.ratio * 100); - const state = usage.ratio >= 1 ? "danger" : usage.ratio >= APP_STORAGE_WARNING_RATIO ? "warning" : "ok"; - - return ` -
-
- Stockage local - ${formatBytes(usage.used)} / ${formatBytes(usage.limit)} -
-
- -
-
- `; -} - -function refreshStorageQuotaIndicators() { - document.querySelectorAll(".storage-quota").forEach((quota) => { - quota.outerHTML = renderStorageQuota(); - }); -} - -function showStorageQuotaModal(usage) { - if (document.querySelector(".storage-quota-modal")) return; - queueMicrotask(() => { - showConfirmModal({ - title: "Quota local atteint", - message: `Sokko G utilise ${formatBytes(usage.used)} sur ${formatBytes(usage.limit)}. Supprimez des outils, des screenshots ou des toolboxes avant d'ajouter de nouvelles données.`, - confirmLabel: "Compris", - cancelLabel: "Fermer", - danger: true, - className: "storage-quota-modal" - }); - }); -} - -function writeStorageValue(key, value) { - const projectedUsage = getAppStorageUsage({ [key]: value }); - if (projectedUsage.used > APP_STORAGE_LIMIT_BYTES) { - showStorageQuotaModal(projectedUsage); - return false; - } - - try { - localStorage.setItem(key, value); - if (key !== STORAGE_KEYS.drawerWidth) refreshStorageQuotaIndicators(); - return true; - } catch (error) { - if (error?.name === "QuotaExceededError" || error?.code === 22) { - showStorageQuotaModal(projectedUsage); - return false; - } - throw error; - } -} - -function getDefaultModuleTitle(type) { - return MODULES[type]?.label || "Outil"; -} - -function normalizeToolboxModule(module) { - if (!module || typeof module !== "object" || !module.type) return null; - const title = String(module.title || "").trim(); - const normalized = { - id: module.id || uid("mod"), - type: module.type - }; - - if (title && title !== getDefaultModuleTitle(module.type)) { - normalized.title = title; - } - - return normalized; -} - -function normalizeToolbox(toolbox) { - if (!toolbox || typeof toolbox !== "object") return null; - return { - id: toolbox.id || uid("tbx"), - name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox", - moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2, - modules: (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean), - updatedAt: toolbox.updatedAt || new Date().toISOString() - }; -} - -function normalizeToolboxes(toolboxes) { - return (Array.isArray(toolboxes) ? toolboxes : []).map(normalizeToolbox).filter(Boolean); -} - -function compactToolboxForStorage(toolbox) { - const normalized = normalizeToolbox(toolbox); - if (!normalized) return null; - - const compact = { - id: normalized.id, - name: normalized.name, - modules: normalized.modules, - updatedAt: normalized.updatedAt - }; - - if (normalized.moduleColumns === 1) compact.moduleColumns = 1; - - return compact; -} - -function compactToolboxesForStorage(toolboxes) { - return (Array.isArray(toolboxes) ? toolboxes : []).map(compactToolboxForStorage).filter(Boolean); -} - -function getToolboxes() { - return normalizeToolboxes(readJson(STORAGE_KEYS.registry, [])); -} - -function saveToolboxes(toolboxes) { - return writeJson(STORAGE_KEYS.registry, compactToolboxesForStorage(toolboxes)); -} - -function getLinks() { - return readJson(STORAGE_KEYS.links, {}); -} - -function saveLinks(links) { - return writeJson(STORAGE_KEYS.links, links); -} - -function moduleStorageKey(toolboxId, moduleId) { - return `sokkog:toolbox:${toolboxId}:module:${moduleId}`; -} - -function globalModuleKey(toolboxId, moduleId) { - return `${toolboxId}:${moduleId}`; -} - -function downloadJson(payload, filename) { - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = filename; - link.click(); - URL.revokeObjectURL(link.href); -} - -function getModuleData(toolboxId, moduleId, fallback) { - const value = readJson(moduleStorageKey(toolboxId, moduleId), undefined); - return value == null ? fallback : value; -} - -function getToolboxModule(toolboxId, moduleId) { - return getToolboxes() - .find((toolbox) => toolbox.id === toolboxId) - ?.modules.find((module) => module.id === moduleId); -} - -function compactChecklistItemForStorage(item) { - const normalized = normalizeChecklistItem(item); - const compact = { - id: normalized.id, - label: normalized.label - }; - - if (normalized.qtyTarget !== 1) compact.qtyTarget = normalized.qtyTarget; - if (normalized.qtyCurrent !== 0) compact.qtyCurrent = normalized.qtyCurrent; - - return compact; -} - -function compactModuleDataForStorage(type, value) { - if (type === "notepad") { - const text = String(value?.text || ""); - return text ? { text } : null; - } - - if (type === "checklist") { - const items = normalizeChecklistData(value).items.map(compactChecklistItemForStorage); - return items.length ? { items } : null; - } - - if (type === "screenshots") { - const shots = (Array.isArray(value?.shots) ? value.shots : []) - .filter((shot) => shot?.dataUrl) - .map((shot) => ({ - id: shot.id || uid("shot"), - dataUrl: shot.dataUrl - })); - return shots.length ? { shots } : null; - } - - return value; -} - -function createExportIdFactory() { - const counts = {}; - return (prefix) => { - counts[prefix] = (counts[prefix] || 0) + 1; - return `${ID_PREFIXES[prefix] || "x"}${counts[prefix].toString(36)}`; - }; -} - -function remapModuleDataForExport(type, data, nextId) { - const compact = compactModuleDataForStorage(type, data); - if (!compact) return null; - - if (type === "checklist") { - return { - items: compact.items.map((item) => ({ - ...item, - id: nextId("item") - })) - }; - } - - if (type === "screenshots") { - return { - shots: compact.shots.map((shot) => ({ - ...shot, - id: nextId("shot") - })) - }; - } - - return compact; -} - -function createToolboxExportPayload(toolbox) { - const source = normalizeToolbox(toolbox); - const nextId = createExportIdFactory(); - const moduleIdMap = new Map(); - const exportedToolbox = compactToolboxForStorage({ - ...source, - id: nextId("tbx"), - modules: source.modules.map((module) => { - const id = nextId("mod"); - moduleIdMap.set(module.id, id); - return { ...module, id }; - }) - }); - const modules = Object.fromEntries(source.modules - .map((module) => [ - moduleIdMap.get(module.id), - remapModuleDataForExport(module.type, getModuleData(source.id, module.id, null), nextId) - ]) - .filter(([, data]) => data != null)); - - return { toolbox: exportedToolbox, modules }; -} - -function createGlobalExportPayload() { - const toolboxes = getToolboxes(); - const modules = {}; - - toolboxes.forEach((toolbox) => { - toolbox.modules.forEach((module) => { - const data = compactModuleDataForStorage(module.type, getModuleData(toolbox.id, module.id, null)); - if (data) modules[globalModuleKey(toolbox.id, module.id)] = data; - }); - }); - - return { - version: 1, - exportedAt: new Date().toISOString(), - toolboxes: compactToolboxesForStorage(toolboxes), - modules, - links: getLinks() - }; -} - -function setModuleData(toolboxId, moduleId, value) { - const module = getToolboxModule(toolboxId, moduleId); - const compact = compactModuleDataForStorage(module?.type, value); - const key = moduleStorageKey(toolboxId, moduleId); - - if (compact == null) { - localStorage.removeItem(key); - refreshStorageQuotaIndicators(); - return true; - } - - return writeJson(key, compact); -} - -function parsePositiveInt(value, fallback = 1) { - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; -} - -function clampQty(value, target) { - const parsed = Number.parseInt(value, 10); - const safeValue = Number.isFinite(parsed) ? parsed : 0; - return Math.min(Math.max(safeValue, 0), Math.max(1, target)); -} - -function normalizeChecklistItem(item) { - const qtyTarget = Math.max(1, parsePositiveInt(item?.qtyTarget, 1)); - const qtyCurrent = clampQty(item?.qtyCurrent, qtyTarget); - - return { - id: item?.id || uid("item"), - label: String(item?.label || "").trim(), - qtyTarget, - qtyCurrent - }; -} - -function normalizeChecklistData(data) { - return { - items: (data?.items || []) - .map(normalizeChecklistItem) - .filter((item) => item.label) - }; -} - -function isChecklistItemDone(item) { - return clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget; -} - -function getGames() { - return gamesState.games; -} - -function getGame(gameId) { - return getGames().find((item) => item.id === gameId); -} - -function getToolboxGameId(toolbox) { - const links = getLinks(); - return Object.entries(links).find(([, toolboxId]) => toolboxId === toolbox?.id)?.[0] || ""; -} - -function getToolboxGame(toolbox) { - return getGame(getToolboxGameId(toolbox)); -} - -async function loadGames() { - if (gamesState.loaded || gamesState.loading) return; - gamesState.loading = true; - gamesState.error = ""; - - try { - const response = await fetch("/data/games.json"); - if (!response.ok) throw new Error("Impossible de charger la liste des jeux."); - - const payload = await response.json(); - gamesState.games = Array.isArray(payload.games) ? payload.games : []; - gamesState.loaded = true; - } catch (error) { - gamesState.error = error.message; - gamesState.games = []; - gamesState.loaded = true; - } finally { - gamesState.loading = false; - } -} - -async function loadSiteContent() { - if (siteState.loaded || siteState.loading) return; - siteState.loading = true; - - try { - const response = await fetch("/data/site.json"); - if (!response.ok) throw new Error("Impossible de charger le contenu du site."); - - const payload = await response.json(); - siteState.content = mergeContent(DEFAULT_SITE_CONTENT, payload); - } catch { - siteState.content = DEFAULT_SITE_CONTENT; - } finally { - siteState.loaded = true; - siteState.loading = false; - } -} - -function getDrawerWidth() { - const value = Number(localStorage.getItem(STORAGE_KEYS.drawerWidth)); - if (!Number.isFinite(value) || value <= 0) return ""; - - const maxWidth = Math.floor(window.innerWidth * 0.94); - return Math.min(Math.max(value, 360), maxWidth); -} - -function escapeHtml(value) { - return String(value).replace(/[&<>"']/g, (char) => ({ - "&": "&", - "<": "<", - ">": ">", - "\"": """, - "'": "'" - })[char]); -} - -function escapeAttr(value) { - return escapeHtml(value).replace(/`/g, "`"); -} - -function assetPath(name) { - return encodeURI(`${MHWILDS_IMG_PATH}/${name}.png`); -} - -function t(key, { capitalize = false } = {}) { - const value = mhwildsState.translations[key] || key; - if (!capitalize) return value; - return value.charAt(0).toUpperCase() + value.slice(1); -} - -function mergeContent(defaults, overrides) { - if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) return defaults; - return Object.fromEntries(Object.entries(defaults).map(([key, value]) => { - const override = overrides[key]; - if (Array.isArray(value)) return [key, Array.isArray(override) ? override : value]; - if (value && typeof value === "object") return [key, mergeContent(value, override)]; - return [key, override ?? value]; - })); -} - -function siteContent() { - return siteState.content; -} - -function formatContentText(value) { - return escapeHtml(value).replace(/<i>(.+?)<\/i>/g, "$1").replace(/\n/g, "
"); -} - -function formatDate(value) { - return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); -} - -function navigate(path) { - location.hash = path; -} - -function currentRoute() { - const hashRoute = location.hash.replace(/^#/, ""); - if (hashRoute) return hashRoute; - - const path = location.pathname.replace(/\/+$/, "") || "/"; - if (path === "/mhwilds") return "/games/mhwilds"; - if (path === "/mhwilds/monsters") return "/games/mhwilds/monsters"; - if (path === "/mhwilds/endemic") return "/games/mhwilds/endemic"; - if (path === "/toolboxes") return "/toolboxes"; - if (path === "/games") return "/games"; - if (path === "/games/mhwilds") return "/games/mhwilds"; - if (path === "/games/mhwilds/monsters") return "/games/mhwilds/monsters"; - if (path === "/games/mhwilds/endemic") return "/games/mhwilds/endemic"; - - return "/"; -} - -function getRouteGameId(route) { - const [, section, gameId] = route.split("/"); - return section === "games" ? gameId || "" : ""; -} - -function renderTopbarActions(route) { - const gameId = getRouteGameId(route); - const game = getGame(gameId); - if (!game) return ""; - - const links = getLinks(); - const linkedToolbox = getToolboxes().find((item) => item.id === links[game.id]); - const label = linkedToolbox - ? `Ouvrir la toolbox ${linkedToolbox.name}` - : "Associer une toolbox"; - - return ` -
- -
- `; -} - -function shell(content) { - const route = currentRoute(); - const contentConfig = siteContent(); - const topbarLabel = route.startsWith("/toolbox") - ? contentConfig.topbar.toolbox - : route.startsWith("/games") - ? contentConfig.topbar.games - : contentConfig.topbar.dashboard; - app.innerHTML = ` -
- -
-
-
- ${escapeHtml(topbarLabel)} -
- ${renderTopbarActions(route)} -
-
${content}
-
- -
- `; -} - -function createToolbox({ name }) { - const now = new Date().toISOString(); - const toolbox = { - id: uid("tbx"), - name: name.trim() || "Nouvelle toolbox", - updatedAt: now, - modules: [ - { id: uid("mod"), type: "notepad", title: "Notes rapides" }, - { id: uid("mod"), type: "checklist" } - ] - }; - return saveToolboxes([toolbox, ...getToolboxes()]) ? toolbox : null; -} - -function updateToolbox(toolbox) { - toolbox.updatedAt = new Date().toISOString(); - return saveToolboxes(getToolboxes().map((item) => item.id === toolbox.id ? toolbox : item)); -} - -function moveToolboxModule(toolboxId, fromModuleId, toModuleId, placement = "before") { - if (!fromModuleId || !toModuleId || fromModuleId === toModuleId) return false; - - const toolbox = getToolboxes().find((item) => item.id === toolboxId); - if (!toolbox) return false; - - const modules = [...toolbox.modules]; - const fromIndex = modules.findIndex((module) => module.id === fromModuleId); - const toIndex = modules.findIndex((module) => module.id === toModuleId); - if (fromIndex < 0 || toIndex < 0) return false; - - const [moved] = modules.splice(fromIndex, 1); - const targetIndex = modules.findIndex((module) => module.id === toModuleId); - const insertIndex = placement === "after" ? targetIndex + 1 : targetIndex; - modules.splice(insertIndex, 0, moved); - toolbox.modules = modules; - updateToolbox(toolbox); - return true; -} - -function deleteToolbox(id) { - const toolbox = getToolboxes().find((item) => item.id === id); - if (toolbox) { - toolbox.modules.forEach((module) => localStorage.removeItem(moduleStorageKey(id, module.id))); - } - saveToolboxes(getToolboxes().filter((item) => item.id !== id)); - const links = getLinks(); - Object.entries(links).forEach(([gameId, toolboxId]) => { - if (toolboxId === id) delete links[gameId]; - }); - saveLinks(links); -} - -function linkToolboxToGame(gameId, toolboxId) { - const previousLinks = getLinks(); - const previousToolboxId = previousLinks[gameId] || ""; - const links = getLinks(); - if (toolboxId) links[gameId] = toolboxId; - else delete links[gameId]; - saveLinks(links); - - const touchedToolboxIds = new Set([previousToolboxId, toolboxId].filter(Boolean)); - const now = new Date().toISOString(); - const nextToolboxes = getToolboxes().map((toolbox) => { - return touchedToolboxIds.has(toolbox.id) ? { ...toolbox, updatedAt: now } : toolbox; - }); - saveToolboxes(nextToolboxes); -} - -function renderHome() { - const toolboxes = getToolboxes(); - const toolCount = Object.keys(MODULES).length; - const content = siteContent().home; - const dialogue = content.origin.lines.map((line) => ` -

${formatContentText(line.text || "")}

- `).join(""); - - shell(` -
-
-

${escapeHtml(content.hero.eyebrow)}

-

${escapeHtml(content.hero.title)}

-

${escapeHtml(content.hero.description)}

- -
-
-
- ${toolboxes.length} - ${escapeHtml(toolboxes.length > 1 ? content.stats.toolboxPlural : content.stats.toolboxSingular)} -
-
- ${toolCount} - ${escapeHtml(toolCount > 1 ? content.stats.toolPlural : content.stats.toolSingular)} -
-
-
-
-
-

${escapeHtml(content.origin.eyebrow)}

-

${escapeHtml(content.origin.title)}

-
- ${dialogue} - ${escapeHtml(content.origin.caption)} -
-
-
- ${escapeAttr(content.origin.visualAlt)} -
-
- `); -} - -function renderToolboxes() { - const toolboxes = getToolboxes(); - const content = siteContent().toolboxes; - shell(` -
-
-
-

${escapeHtml(content.eyebrow)}

-

${escapeHtml(content.title)}

-
-
- - -
-
-
-
- - -
-
-
- ${storageHelpCard(content.storageHelp)} - ${toolboxes.length ? toolboxes.map(toolboxCard).join("") : ` -
-

${escapeHtml(content.emptyTitle)}

-

${escapeHtml(content.emptyText)}

-
- `} -
- ${renderStorageQuota()} -
- `); -} - -function storageHelpCard(help) { - return ` -
-
-
- -
-

LocalStorage

-

${escapeHtml(help.title)}

-
-
-

${escapeHtml(help.text)}

-
    - ${help.items.map((item, index) => ` -
  • - ${index + 1} -

    ${escapeHtml(item)}

    -
  • - `).join("")} -
-
-
- `; -} - -function toolboxCard(toolbox) { - const game = getToolboxGame(toolbox); - return ` -
-
- ${game?.image ? `${escapeAttr(game.title)}` : ""} -
-
-

${game ? escapeHtml(game.title) : "Toolbox libre"}

-

${escapeHtml(toolbox.name)}

- Modifiée le ${formatDate(toolbox.updatedAt)} -
- - - - - -
-
-
- `; -} - -function renderToolbox(id, { embedded = false } = {}) { - const toolbox = getToolboxes().find((item) => item.id === id); - if (!toolbox) { - if (embedded) return `

Toolbox introuvable.

`; - shell(`

Toolbox introuvable

Retour
`); - return; - } - - const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2; - const content = ` -
-
-
-

Toolbox

- ${embedded ? `

${escapeHtml(toolbox.name)}

` : ` -

${escapeHtml(toolbox.name)}

- `} -
-
- -
-
- ${embedded ? "" : ` -
- ${renderModuleLayoutSwitch(toolbox, moduleColumns)} -
- `} -
- ${renderToolboxModules(toolbox, moduleColumns)} -
- ${embedded ? "" : renderStorageQuota()} -
- `; - - if (embedded) return content; - shell(content); -} - -function renderModuleLayoutSwitch(toolbox, moduleColumns) { - return ` -
- - -
- `; -} - -function renderToolboxModules(toolbox, moduleColumns) { - if (moduleColumns === 1) { - return toolbox.modules.map((module) => renderModule(toolbox, module)).join(""); - } - - const columns = [[], []]; - toolbox.modules.forEach((module, index) => { - columns[index % 2].push(module); - }); - - return columns.map((modules) => ` -
- ${modules.map((module) => renderModule(toolbox, module)).join("")} -
- `).join(""); -} - -function renderModule(toolbox, module) { - const label = MODULES[module.type]?.label || module.type; - return ` -
-
-
- - -

${escapeHtml(module.title || label)}

-
-
- -
-
- ${module.type === "notepad" ? renderNotepad(toolbox.id, module.id) : ""} - ${module.type === "checklist" ? renderChecklist(toolbox.id, module.id) : ""} - ${module.type === "screenshots" ? renderScreenshots(toolbox.id, module.id) : ""} -
- `; -} - -function renderNotepad(toolboxId, moduleId) { - const data = getModuleData(toolboxId, moduleId, { text: "" }); - return ``; -} - -function renderChecklist(toolboxId, moduleId) { - const data = normalizeChecklistData(getModuleData(toolboxId, moduleId, { items: [] })); - return ` -
- - - -
-
    - ${data.items.map((item) => renderChecklistItem(toolboxId, moduleId, item)).join("")} -
- `; -} - -function renderChecklistItem(toolboxId, moduleId, item) { - const done = isChecklistItemDone(item); - return ` -
  • -
    - ${item.qtyTarget === 1 ? ` - - ` : ` -
    - - ${clampQty(item.qtyCurrent, item.qtyTarget)}/${item.qtyTarget} - -
    - `} - ${escapeHtml(item.label)} -
    - -
  • - `; -} - -function renderScreenshots(toolboxId, moduleId) { - const data = getModuleData(toolboxId, moduleId, { shots: [] }); - return ` - -
    Coller une image ici
    -
    - ${data.shots.map((shot) => ` -
    - - -
    - `).join("")} -
    - `; -} - -function renderGames() { - const games = getGames(); - - shell(` -
    -
    -

    Pages informatives

    -

    Jeux disponibles

    -

    Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.

    -
    -
    -
    - ${games.length ? games.map((game) => ` -
    -
    - ${escapeAttr(game.title)} -
    -
    -

    ${escapeHtml(game.eyebrow || "Guide de jeu")}

    -

    ${escapeHtml(game.title)}

    -

    ${escapeHtml(game.summary)}

    -
    - Ouvrir -
    -
    -
    - `).join("") : ` -
    -

    Aucun jeu disponible

    -

    ${escapeHtml(gamesState.error || "Ajoutez des entrées dans /data/games.json.")}

    -
    - `} -
    - `); -} - -function renderGame(gameId) { - const game = getGame(gameId); - if (!game) { - renderGames(); - return; - } - - if (game.id === "mhwilds") { - renderMhwilds(currentRoute().split("/")[3] || ""); - return; - } - - shell(` -
    -
    -

    ${escapeHtml(game.eyebrow || "Guide de jeu")}

    -

    ${escapeHtml(game.title)}

    -

    ${escapeHtml(game.summary)}

    -
    -
    -
    - ${game.sections.map((section) => ` -
    -

    ${escapeHtml(section.title)}

    -
      ${section.items.map((item) => `
    • ${escapeHtml(item)}
    • `).join("")}
    -
    - `).join("")} -
    - - `); -} - -async function loadMhwildsData() { - if (mhwildsState.loaded || mhwildsState.loading) return; - mhwildsState.loading = true; - mhwildsState.error = ""; - - try { - const [monstersResponse, endemicResponse, translationsResponse] = await Promise.all([ - fetch("/data/mhwilds/monsters.json"), - fetch("/data/mhwilds/endemic_life.json"), - fetch("/data/mhwilds/i18n/fr.json") - ]); - - if (!monstersResponse.ok || !endemicResponse.ok || !translationsResponse.ok) { - throw new Error("Impossible de charger les données Monster Hunter Wilds."); - } - - const [monstersJson, endemicJson, translations] = await Promise.all([ - monstersResponse.json(), - endemicResponse.json(), - translationsResponse.json() - ]); - - mhwildsState.monsters = monstersJson.monsters || []; - mhwildsState.endemic = [ - ...(endemicJson.endemicLife || []), - ...(endemicJson.aquaticLife || []) - ]; - mhwildsState.translations = translations; - mhwildsState.loaded = true; - } catch (error) { - mhwildsState.error = error.message; - } finally { - mhwildsState.loading = false; - } -} - -function renderMhwilds(category) { - const activeCategory = category === "monsters" || category === "endemic" ? category : ""; - - if (!mhwildsState.loaded) { - shell(` -
    -
    -

    Monster Hunter Wilds

    -

    Monster Hunter: Wilds

    -

    Chargement des données de chasse, faune endémique et filtres associés.

    -
    -
    -

    Chargement

    Préparation des données locales...

    - `); - loadMhwildsData().then(rerender); - return; - } - - if (mhwildsState.error) { - shell(` -
    -

    Impossible de charger MH Wilds

    -

    ${escapeHtml(mhwildsState.error)}

    - -
    - `); - return; - } - - if (!activeCategory) { - renderMhwildsOverview(); - return; - } - - renderMhwildsListing(activeCategory); -} - -function renderMhwildsOverview() { - shell(` -
    -
    -

    Guide de jeu

    -

    Monster Hunter: Wilds

    -

    Consultez rapidement les monstres, faiblesses, afflictions, hitzones et emplacements de faune endémique.

    -
    -
    -
    - - - Monstres - Recherche, filtres par faiblesse et tableau de dégâts par partie. - - - - Faune endémique - Faune endémique et aquatique filtrable par localisation. - -
    - - `); -} - -function renderMhwildsListing(category) { - const isMonsters = category === "monsters"; - const label = isMonsters ? t("monsters", { capitalize: true }) : t("endemic life", { capitalize: true }); - const filterKey = isMonsters ? "weaknesses" : "locations"; - const options = getUniqueConditionValues(isMonsters ? mhwildsState.monsters : mhwildsState.endemic, filterKey); - const total = isMonsters ? mhwildsState.monsters.length : mhwildsState.endemic.length; - const visible = getFilteredMhwildsItems(category).length; - - shell(` -
    -
    -

    Monster Hunter Wilds

    -
    -

    ${escapeHtml(label)}

    - ${visible} / ${total} -
    -

    ${isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}

    -
    -
    - Monstres - Faune -
    -
    -
    - -
    - ${renderMhwildsResults(category)} -
    -
    - - `); -} - -function renderMhwildsFilters(category, filterKey, options) { - const filters = mhwildsState.filters[category]; - const selected = new Set(filters[filterKey]); - const logicLabel = filters.logic === "or" ? t("or", { capitalize: true }) : t("and", { capitalize: true }); - - return ` -
    -
    -
    -

    Filtres

    -

    ${category === "monsters" ? t("weaknesses", { capitalize: true }) : t("locations", { capitalize: true })}

    -
    - -
    - -
    - Correspondance - -
    -
    - ${options.map((option) => ` - - `).join("")} -
    -
    - `; -} - -function renderMhwildsResults(category) { - const items = getFilteredMhwildsItems(category); - - return ` -
    - ${items.length ? items.map((item) => category === "monsters" ? renderMonsterCard(item) : renderEndemicCard(item)).join("") : ` -
    -

    Aucun résultat

    -

    Ajustez la recherche ou réinitialisez les filtres actifs.

    -
    - `} -
    - `; -} - -function refreshMhwildsResults(category) { - const results = document.querySelector(".mhwilds-results"); - if (results) results.innerHTML = renderMhwildsResults(category); - const count = document.querySelector("[data-mhwilds-count]"); - if (count) { - const total = category === "monsters" ? mhwildsState.monsters.length : mhwildsState.endemic.length; - count.textContent = `${getFilteredMhwildsItems(category).length} / ${total}`; - } -} - -function renderMonsterCard(monster) { - const weaknesses = getConditionValues(monster.weaknesses); - const ailments = getConditionValues(monster.ailments).filter((value) => value !== "none"); - - return ` -
    -
    -
    -
    - ${escapeAttr(t(monster.name, { capitalize: true }))} -
    -
    -

    ${escapeHtml(t(monster.type, { capitalize: true }))}

    -

    ${escapeHtml(t(monster.name, { capitalize: true }))}

    - ${renderIconRow(t("weaknesses", { capitalize: true }), weaknesses)} - ${renderIconRow(t("ailments", { capitalize: true }), ailments.length ? ailments : ["none"])} -
    -
    -
    -
    -

    Détails

    -

    ${escapeHtml(t(monster.name, { capitalize: true }))}

    -
    - ${renderDamageTable(monster.damage || [])} -
    -
    -
    - `; -} - -function renderEndemicCard(item) { - const locations = getConditionValues(item.locations); - const description = t(item.description); - const hasDescription = normalizeText(description) && normalizeText(description) !== normalizeText("à compléter"); - - return ` -
    -
    - ${escapeAttr(t(item.name, { capitalize: true }))} -
    -
    -

    ${escapeHtml(t(item.name, { capitalize: true }))}

    - ${renderIconRow(t("locations", { capitalize: true }), locations)} - ${hasDescription ? `

    ${escapeHtml(description)}

    ` : ""} -
    -
    - `; -} - -function renderIconRow(label, values) { - return ` -
    - ${escapeHtml(label)} -
    - ${values.map((value) => value === "none" ? `-` : ` - ${escapeAttr(t(value, { capitalize: true }))} - `).join("")} -
    -
    - `; -} - -function renderDamageTable(rows) { - if (!rows.length) return ""; - const columns = Object.keys(rows[0]); - return ` -
    - - - - ${columns.map((column) => ` - - `).join("")} - - - - ${rows.map((row) => ` - - ${columns.map((column) => column === "name" ? ` - - ` : ` - - `).join("")} - - `).join("")} - -
    ${column === "name" ? "" : `${escapeAttr(t(column, { capitalize: true }))}`}
    ${escapeHtml(t(row[column], { capitalize: true }))}${row[column]} étoiles
    -
    - `; -} - -function getFilteredMhwildsItems(category) { - const items = category === "monsters" ? mhwildsState.monsters : mhwildsState.endemic; - const filters = mhwildsState.filters[category]; - const filterKey = category === "monsters" ? "weaknesses" : "locations"; - const selected = filters[filterKey]; - const search = normalizeText(filters.name); - - return items.filter((item) => { - const nameMatches = !search || normalizeText(t(item.name)).includes(search) || normalizeText(item.name).includes(search); - if (!nameMatches) return false; - if (!selected.length) return true; - - const values = getConditionValues(item[filterKey]); - if (filters.logic === "or" && selected.length >= 2) { - return selected.some((value) => values.includes(value)); - } - - return selected.every((value) => values.includes(value)); - }); -} - -function getConditionValues(conditions = []) { - return [...new Set(conditions.flatMap((condition) => condition.values || []))]; -} - -function getUniqueConditionValues(items, property) { - return [...new Set(items.flatMap((item) => getConditionValues(item[property])))] - .filter((value) => value !== "none") - .sort((a, b) => t(a).localeCompare(t(b), "fr")); -} - -function normalizeText(value) { - return String(value || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, " "); -} - -function openDrawer(gameId) { - const drawer = document.querySelector("#toolbox-drawer"); - const toolboxes = getToolboxes(); - const links = getLinks(); - const selectedId = links[gameId] || ""; - const drawerWidth = getDrawerWidth(); - drawer.innerHTML = ` -
    -
    -
    - -
    -
    -

    Toolbox liée

    -
    - - - -
    -
    - -
    - ${selectedId ? renderToolbox(selectedId, { embedded: true }) : ` -
    -

    Aucune toolbox associée à cette page jeu.

    - -
    - `} - ${renderStorageQuota()} -
    -
    - `; - drawer.dataset.gameId = gameId; - drawer.setAttribute("aria-hidden", "false"); -} - -function closeDrawer() { - const drawer = document.querySelector("#toolbox-drawer"); - if (drawer) { - drawer.setAttribute("aria-hidden", "true"); - drawer.innerHTML = ""; - delete drawer.dataset.gameId; - } -} - -function refreshToolboxView(sourceElement = null) { - const drawer = sourceElement?.closest?.("#toolbox-drawer"); - if (drawer?.dataset.gameId) { - openDrawer(drawer.dataset.gameId); - return; - } - - rerender(); -} - -function showToolboxCreateModal({ gameId = "" } = {}) { - return new Promise((resolve) => { - const host = document.createElement("div"); - host.className = "confirm-modal-root"; - host.innerHTML = ` -
    - - `; - - const previousFocus = document.activeElement; - document.body.appendChild(host); - const input = host.querySelector("input[name='name']"); - - function close(result) { - host.remove(); - document.removeEventListener("keydown", onKeydown); - previousFocus?.focus?.(); - resolve(result); - } - - function onKeydown(event) { - if (event.key === "Escape") close(null); - } - - host.addEventListener("click", (event) => { - if (event.target.closest("[data-toolbox-create-result='cancel']")) close(null); - }); - - host.querySelector("form").addEventListener("submit", (event) => { - event.preventDefault(); - const name = new FormData(event.currentTarget).get("name")?.trim(); - if (!name) return; - - const toolbox = createToolbox({ name }); - if (!toolbox) return; - if (gameId) { - const links = getLinks(); - links[gameId] = toolbox.id; - saveLinks(links); - } - close(toolbox); - }); - - document.addEventListener("keydown", onKeydown); - input.focus(); - }); -} - -function showToolboxLinkModal(gameId) { - return new Promise((resolve) => { - const toolboxes = getToolboxes(); - const selectedId = getLinks()[gameId] || ""; - const host = document.createElement("div"); - host.className = "confirm-modal-root"; - host.innerHTML = ` -
    - - `; - - const previousFocus = document.activeElement; - document.body.appendChild(host); - const select = host.querySelector("select"); - - function close(result) { - host.remove(); - document.removeEventListener("keydown", onKeydown); - previousFocus?.focus?.(); - resolve(result); - } - - function onKeydown(event) { - if (event.key === "Escape") close(null); - } - - host.addEventListener("click", (event) => { - if (event.target.closest("[data-toolbox-link-result='cancel']")) close(null); - }); - - host.querySelector("form").addEventListener("submit", (event) => { - event.preventDefault(); - close(new FormData(event.currentTarget).get("toolboxId") || ""); - }); - - document.addEventListener("keydown", onKeydown); - select.focus(); - }); -} - -async function compressImage(file) { - const bitmap = await createImageBitmap(file); - const maxSide = 1400; - const ratio = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height)); - const canvas = document.createElement("canvas"); - canvas.width = Math.round(bitmap.width * ratio); - canvas.height = Math.round(bitmap.height * ratio); - canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height); - return canvas.toDataURL("image/jpeg", 0.78); -} - -async function addScreenshotFiles(toolboxId, moduleId, files) { - const imageFiles = [...files].filter((file) => file?.type?.startsWith("image/")); - if (!imageFiles.length) return false; - - const data = getModuleData(toolboxId, moduleId, { shots: [] }); - for (const file of imageFiles) { - data.shots.unshift({ - id: uid("shot"), - dataUrl: await compressImage(file) - }); - } - return setModuleData(toolboxId, moduleId, data); -} - -function exportToolbox(id) { - const toolbox = normalizeToolbox(getToolboxes().find((item) => item.id === id)); - if (!toolbox) return; - const payload = createToolboxExportPayload(toolbox); - downloadJson(payload, `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`); -} - -function exportAllToolboxes() { - downloadJson(createGlobalExportPayload(), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`); -} - -async function importToolbox(file) { - const payload = JSON.parse(await file.text()); - if (!payload.toolbox?.modules) throw new Error("Format d'outils invalide"); - const imported = normalizeToolbox({ ...payload.toolbox, id: uid("tbx"), updatedAt: new Date().toISOString() }); - const moduleIdMap = new Map(); - imported.modules = imported.modules.map((module) => { - const nextId = uid("mod"); - moduleIdMap.set(module.id, nextId); - return { ...module, id: nextId }; - }); - saveToolboxes([imported, ...getToolboxes()]); - Object.entries(payload.modules || {}).forEach(([oldId, data]) => { - const nextId = moduleIdMap.get(oldId); - if (nextId) setModuleData(imported.id, nextId, data); - }); - return imported; -} - -async function importAllToolboxes(file) { - const payload = JSON.parse(await file.text()); - if (!Array.isArray(payload.toolboxes) || !payload.modules || typeof payload.modules !== "object") { - throw new Error("Format d'import global invalide"); - } - - const toolboxIdMap = new Map(); - const moduleIdMap = new Map(); - const projected = {}; - const importedToolboxes = payload.toolboxes.map((toolbox) => { - const nextToolboxId = uid("tbx"); - toolboxIdMap.set(toolbox.id, nextToolboxId); - const modules = (toolbox.modules || []).map((module) => { - const nextModuleId = uid("mod"); - moduleIdMap.set(globalModuleKey(toolbox.id, module.id), nextModuleId); - return { ...module, id: nextModuleId }; - }); - - return normalizeToolbox({ - ...toolbox, - id: nextToolboxId, - name: `${toolbox.name || "Toolbox"} (import)`, - modules, - updatedAt: new Date().toISOString() - }); - }); - - const nextToolboxes = [...importedToolboxes, ...getToolboxes()]; - const importedToolboxMap = new Map(importedToolboxes.map((toolbox) => [toolbox.id, toolbox])); - projected[STORAGE_KEYS.registry] = JSON.stringify(compactToolboxesForStorage(nextToolboxes)); - - Object.entries(payload.modules).forEach(([key, data]) => { - const [oldToolboxId] = key.split(":"); - const nextToolboxId = toolboxIdMap.get(oldToolboxId); - const nextModuleId = moduleIdMap.get(key); - const toolbox = importedToolboxMap.get(nextToolboxId); - const module = toolbox?.modules.find((item) => item.id === nextModuleId); - const compact = compactModuleDataForStorage(module?.type, data); - if (nextToolboxId && nextModuleId && compact) { - projected[moduleStorageKey(nextToolboxId, nextModuleId)] = JSON.stringify(compact); - } - }); - - const links = getLinks(); - Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => { - const nextToolboxId = toolboxIdMap.get(oldToolboxId); - if (nextToolboxId) links[gameId] = nextToolboxId; - }); - projected[STORAGE_KEYS.links] = JSON.stringify(links); - - const projectedUsage = getAppStorageUsage(projected); - if (projectedUsage.used > APP_STORAGE_LIMIT_BYTES) { - showStorageQuotaModal(projectedUsage); - return false; - } - - Object.entries(projected).forEach(([key, value]) => writeStorageValue(key, value)); - return true; -} - -function showConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel = "Annuler", danger = false, className = "" }) { - return new Promise((resolve) => { - const host = document.createElement("div"); - host.className = ["confirm-modal-root", className].filter(Boolean).join(" "); - host.innerHTML = ` -
    - - `; - - const previousFocus = document.activeElement; - document.body.appendChild(host); - - const cancelButton = host.querySelector('[data-confirm-result="cancel"]'); - const confirmButton = host.querySelector('[data-confirm-result="confirm"]'); - - function close(result) { - host.remove(); - document.removeEventListener("keydown", onKeydown); - previousFocus?.focus?.(); - resolve(result); - } - - function onKeydown(event) { - if (event.key === "Escape") close(false); - if (event.key !== "Tab") return; - - const focusable = [cancelButton, confirmButton]; - const currentIndex = focusable.indexOf(document.activeElement); - const nextIndex = event.shiftKey - ? (currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1) - : (currentIndex === focusable.length - 1 ? 0 : currentIndex + 1); - - event.preventDefault(); - focusable[nextIndex].focus(); - } - - host.addEventListener("click", (event) => { - const result = event.target.closest("[data-confirm-result]")?.dataset.confirmResult; - if (!result) return; - close(result === "confirm"); - }); - - document.addEventListener("keydown", onKeydown); - confirmButton.focus(); - }); -} - -function showScreenshotViewer(shot) { - const host = document.createElement("div"); - host.className = "screenshot-viewer-root"; - host.innerHTML = ` -
    - - `; - - document.body.appendChild(host); - - function close() { - host.remove(); - document.removeEventListener("keydown", onKeydown); - } - - function onKeydown(event) { - if (event.key === "Escape") close(); - } - - host.addEventListener("click", (event) => { - if (event.target.closest("[data-action='close-screenshot-viewer']")) close(); - }); - document.addEventListener("keydown", onKeydown); -} - -function rerender() { - if (!siteState.loaded) { - shell(` -
    -

    Chargement

    -

    Préparation du contenu...

    -
    - `); - loadSiteContent().then(rerender); - return; - } - - if (!gamesState.loaded) { - shell(` -
    -

    Chargement

    -

    Préparation des jeux disponibles...

    -
    - `); - loadGames().then(rerender); - return; - } - - const route = currentRoute(); - if (route === "/") renderHome(); - else if (route === "/toolboxes") renderToolboxes(); - else if (route.startsWith("/toolbox/")) renderToolbox(route.split("/")[2]); - else if (route === "/games") renderGames(); - else if (route.startsWith("/games/")) renderGame(route.split("/")[2]); - else navigate("/"); -} - -document.addEventListener("click", async (event) => { - const target = event.target.closest("[data-action]"); - if (!target) return; - const action = target.dataset.action; - - if (action === "flip-monster-card") { - target.classList.toggle("is-flipped"); - target.setAttribute("aria-pressed", target.classList.contains("is-flipped") ? "true" : "false"); - return; - } - - if (action === "new-toolbox") { - const toolbox = await showToolboxCreateModal(); - if (toolbox) navigate(`/toolbox/${toolbox.id}`); - } - if (action === "delete-toolbox") { - const toolbox = getToolboxes().find((item) => item.id === target.dataset.id); - const confirmed = await showConfirmModal({ - title: "Supprimer la toolbox", - message: `Supprimer "${toolbox?.name || "cette toolbox"}" et ses données locales ?`, - confirmLabel: "Supprimer", - danger: true - }); - - if (confirmed) { - deleteToolbox(target.dataset.id); - rerender(); - } - } - if (action === "export-toolbox") exportToolbox(target.dataset.id); - if (action === "export-all-toolboxes") exportAllToolboxes(); - if (action === "open-link-toolbox-modal") { - const toolboxId = await showToolboxLinkModal(target.dataset.gameId); - if (toolboxId == null) return; - linkToolboxToGame(target.dataset.gameId, toolboxId); - openDrawer(target.dataset.gameId); - } - if (action === "set-module-layout") { - const toolbox = getToolboxes().find((item) => item.id === target.dataset.id); - if (toolbox) { - toolbox.moduleColumns = Number(target.dataset.columns) === 1 ? 1 : 2; - updateToolbox(toolbox); - refreshToolboxView(target); - } - } - if (action === "view-screenshot") { - const data = getModuleData(target.dataset.toolboxId, target.dataset.moduleId, { shots: [] }); - const shot = data.shots.find((item) => item.id === target.dataset.shotId); - if (shot) showScreenshotViewer(shot); - } - if (action === "close-screenshot-viewer") { - target.closest(".screenshot-viewer-root")?.remove(); - } - if (action === "delete-module") { - const toolbox = getToolboxes().find((item) => item.id === target.dataset.toolboxId); - const module = toolbox?.modules.find((item) => item.id === target.dataset.moduleId); - const confirmed = await showConfirmModal({ - title: "Retirer l'outil", - message: `Retirer "${module?.title || "cet outil"}" de la toolbox ?`, - confirmLabel: "Retirer", - danger: true - }); - - if (confirmed && toolbox) { - toolbox.modules = toolbox.modules.filter((item) => item.id !== target.dataset.moduleId); - localStorage.removeItem(moduleStorageKey(toolbox.id, target.dataset.moduleId)); - updateToolbox(toolbox); - refreshToolboxView(target); - } - } - if (action === "open-drawer") openDrawer(target.dataset.gameId); - if (action === "close-drawer") closeDrawer(); - if (action === "new-toolbox-for-game") { - const toolbox = await showToolboxCreateModal({ gameId: target.dataset.gameId }); - if (toolbox) openDrawer(target.dataset.gameId); - } - if (action === "retry-mhwilds") { - mhwildsState.loaded = false; - mhwildsState.loading = false; - mhwildsState.error = ""; - rerender(); - } - if (action === "reset-mhwilds-filters") { - const category = target.dataset.category; - const filterKey = category === "monsters" ? "weaknesses" : "locations"; - mhwildsState.filters[category] = { name: "", [filterKey]: [], logic: "and" }; - rerender(); - } - if (action === "toggle-mhwilds-logic") { - const category = target.dataset.category; - const filters = mhwildsState.filters[category]; - filters.logic = filters.logic === "and" ? "or" : "and"; - rerender(); - } - if (action === "delete-check-item") { - const data = normalizeChecklistData(getModuleData(target.dataset.toolboxId, target.dataset.moduleId, { items: [] })); - data.items = data.items.filter((item) => item.id !== target.dataset.itemId); - setModuleData(target.dataset.toolboxId, target.dataset.moduleId, data); - refreshToolboxView(target); - } - if (action === "adjust-check-qty") { - const data = normalizeChecklistData(getModuleData(target.dataset.toolboxId, target.dataset.moduleId, { items: [] })); - const item = data.items.find((entry) => entry.id === target.dataset.itemId); - if (item) { - const delta = Number.parseInt(target.dataset.delta, 10) || 0; - item.qtyCurrent = clampQty(item.qtyCurrent + delta, item.qtyTarget); - setModuleData(target.dataset.toolboxId, target.dataset.moduleId, data); - refreshToolboxView(target); - } - } - if (action === "delete-screenshot") { - const data = getModuleData(target.dataset.toolboxId, target.dataset.moduleId, { shots: [] }); - data.shots = data.shots.filter((shot) => shot.id !== target.dataset.shotId); - setModuleData(target.dataset.toolboxId, target.dataset.moduleId, data); - refreshToolboxView(target); - } -}); - -document.addEventListener("pointerdown", (event) => { - const handle = event.target.closest(".drawer-resize-handle"); - if (!handle) return; - - const panel = handle.closest(".drawer-panel"); - if (!panel) return; - - event.preventDefault(); - const startX = event.clientX; - const startWidth = panel.getBoundingClientRect().width; - const minWidth = 360; - const maxWidth = Math.floor(window.innerWidth * 0.94); - - document.body.classList.add("is-resizing-drawer"); - - function resizeDrawer(moveEvent) { - const nextWidth = Math.min(Math.max(startWidth + startX - moveEvent.clientX, minWidth), maxWidth); - panel.style.width = `${nextWidth}px`; - writeStorageValue(STORAGE_KEYS.drawerWidth, String(Math.round(nextWidth))); - } - - function stopResize() { - document.body.classList.remove("is-resizing-drawer"); - window.removeEventListener("pointermove", resizeDrawer); - window.removeEventListener("pointerup", stopResize); - window.removeEventListener("pointercancel", stopResize); - } - - window.addEventListener("pointermove", resizeDrawer); - window.addEventListener("pointerup", stopResize); - window.addEventListener("pointercancel", stopResize); -}); - -document.addEventListener("focusin", (event) => { - const pasteTarget = event.target.closest("[data-action='paste-screenshot']"); - if (pasteTarget && pasteTarget.textContent.trim() === "Coller une image ici") { - pasteTarget.textContent = ""; - return; - } - - const title = event.target.closest("[data-action='edit-module-title'], [data-action='edit-toolbox-title']"); - if (!title) return; - - title.dataset.previousTitle = title.textContent.trim(); -}); - -document.addEventListener("focusout", (event) => { - const pasteTarget = event.target.closest("[data-action='paste-screenshot']"); - if (pasteTarget && !pasteTarget.textContent.trim()) { - pasteTarget.textContent = "Coller une image ici"; - return; - } - - const title = event.target.closest("[data-action='edit-module-title'], [data-action='edit-toolbox-title']"); - if (!title) return; - - if (title.dataset.action === "edit-toolbox-title") { - const toolbox = getToolboxes().find((item) => item.id === title.dataset.id); - if (!toolbox) return; - - const nextTitle = title.textContent.trim() || "Nouvelle toolbox"; - title.textContent = nextTitle; - - if (nextTitle !== toolbox.name) { - toolbox.name = nextTitle; - updateToolbox(toolbox); - } - return; - } - - const toolbox = getToolboxes().find((item) => item.id === title.dataset.toolboxId); - const module = toolbox?.modules.find((item) => item.id === title.dataset.moduleId); - if (!toolbox || !module) return; - - const fallback = MODULES[module.type]?.label || "Outil"; - const nextTitle = title.textContent.trim() || fallback; - title.textContent = nextTitle; - - if (nextTitle !== module.title) { - module.title = nextTitle; - updateToolbox(toolbox); - } -}); - -document.addEventListener("dragstart", (event) => { - const moduleElement = event.target.closest(".module[draggable='true']"); - if (!moduleElement) return; - - if (event.target.closest("button, input, select, textarea, [contenteditable='true']")) { - event.preventDefault(); - return; - } - - draggedModule = { - toolboxId: moduleElement.dataset.toolboxId, - moduleId: moduleElement.dataset.moduleId - }; - moduleElement.classList.add("is-dragging"); - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", draggedModule.moduleId); -}); - -document.addEventListener("dragover", (event) => { - const dropzone = event.target.closest("[data-action='drop-screenshot']"); - if (dropzone) { - event.preventDefault(); - event.dataTransfer.dropEffect = "copy"; - dropzone.classList.add("is-drag-over"); - return; - } - - const moduleElement = event.target.closest(".module[draggable='true']"); - if (!moduleElement || !draggedModule || moduleElement.dataset.toolboxId !== draggedModule.toolboxId) return; - - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - document.querySelectorAll(".module.is-drop-target").forEach((item) => item.classList.remove("is-drop-target", "drop-after")); - const rect = moduleElement.getBoundingClientRect(); - moduleElement.classList.add("is-drop-target"); - moduleElement.classList.toggle("drop-after", event.clientY > rect.top + rect.height / 2); -}); - -document.addEventListener("dragleave", (event) => { - const dropzone = event.target.closest("[data-action='drop-screenshot']"); - if (!dropzone || dropzone.contains(event.relatedTarget)) return; - dropzone.classList.remove("is-drag-over"); -}); - -document.addEventListener("drop", async (event) => { - const dropzone = event.target.closest("[data-action='drop-screenshot']"); - if (dropzone) { - event.preventDefault(); - dropzone.classList.remove("is-drag-over"); - if (await addScreenshotFiles(dropzone.dataset.toolboxId, dropzone.dataset.moduleId, event.dataTransfer.files)) { - refreshToolboxView(dropzone); - } - return; - } - - const moduleElement = event.target.closest(".module[draggable='true']"); - if (!moduleElement || !draggedModule || moduleElement.dataset.toolboxId !== draggedModule.toolboxId) return; - - event.preventDefault(); - const rect = moduleElement.getBoundingClientRect(); - const placement = event.clientY > rect.top + rect.height / 2 ? "after" : "before"; - if (moveToolboxModule(draggedModule.toolboxId, draggedModule.moduleId, moduleElement.dataset.moduleId, placement)) { - refreshToolboxView(moduleElement); - } -}); - -document.addEventListener("dragend", () => { - draggedModule = null; - document.querySelectorAll(".module.is-dragging, .module.is-drop-target").forEach((item) => { - item.classList.remove("is-dragging", "is-drop-target", "drop-after"); - }); -}); - -document.addEventListener("keydown", (event) => { - const editableTitle = event.target.closest("[data-action='edit-module-title'], [data-action='edit-toolbox-title']"); - if (editableTitle && event.key === "Enter") { - event.preventDefault(); - editableTitle.blur(); - return; - } - - if (editableTitle && event.key === "Escape") { - event.preventDefault(); - editableTitle.textContent = editableTitle.dataset.previousTitle || editableTitle.textContent; - editableTitle.blur(); - return; - } - - const target = event.target.closest("[data-action='flip-monster-card']"); - if (!target || (event.key !== "Enter" && event.key !== " ")) return; - - event.preventDefault(); - target.classList.toggle("is-flipped"); - target.setAttribute("aria-pressed", target.classList.contains("is-flipped") ? "true" : "false"); -}); - -document.addEventListener("change", async (event) => { - const target = event.target.closest("[data-action]"); - if (!target) return; - if (target.dataset.action === "add-module" && target.value) { - const toolbox = getToolboxes().find((item) => item.id === target.dataset.id); - toolbox.modules.push({ id: uid("mod"), type: target.value }); - updateToolbox(toolbox); - refreshToolboxView(target); - } - if (target.dataset.action === "toggle-check-item") { - const data = normalizeChecklistData(getModuleData(target.dataset.toolboxId, target.dataset.moduleId, { items: [] })); - const item = data.items.find((entry) => entry.id === target.dataset.itemId); - if (item) { - item.qtyTarget = 1; - item.qtyCurrent = target.checked ? 1 : 0; - } - setModuleData(target.dataset.toolboxId, target.dataset.moduleId, data); - target.closest(".checklist-item")?.classList.toggle("is-complete", target.checked); - } - if (target.dataset.action === "add-screenshot") { - if (await addScreenshotFiles(target.dataset.toolboxId, target.dataset.moduleId, target.files)) { - refreshToolboxView(target); - } - } - if (target.dataset.action === "link-toolbox") { - linkToolboxToGame(target.dataset.gameId, target.value); - openDrawer(target.dataset.gameId); - } - if (target.dataset.action === "toggle-mhwilds-option") { - const { category, filterKey, value } = target.dataset; - const values = new Set(mhwildsState.filters[category][filterKey]); - if (target.checked) values.add(value); - else values.delete(value); - mhwildsState.filters[category][filterKey] = [...values]; - target.closest(".filter-chip")?.classList.toggle("active", target.checked); - refreshMhwildsResults(category); - } - if (target.dataset.action === "import-toolbox" && target.files[0]) { - try { - const imported = await importToolbox(target.files[0]); - if (target.dataset.gameId && imported) { - linkToolboxToGame(target.dataset.gameId, imported.id); - openDrawer(target.dataset.gameId); - } else { - rerender(); - } - } catch (error) { - showConfirmModal({ - title: "Import impossible", - message: error.message, - confirmLabel: "Compris", - cancelLabel: "Fermer", - danger: true - }); - } - } - if (target.dataset.action === "import-all-toolboxes" && target.files[0]) { - try { - if (await importAllToolboxes(target.files[0])) rerender(); - } catch (error) { - showConfirmModal({ - title: "Import global impossible", - message: error.message, - confirmLabel: "Compris", - cancelLabel: "Fermer", - danger: true - }); - } - } -}); - -document.addEventListener("paste", async (event) => { - const target = event.target.closest("[data-action='paste-screenshot']"); - if (!target) return; - - const files = [...(event.clipboardData?.items || [])] - .filter((item) => item.type.startsWith("image/")) - .map((item) => item.getAsFile()) - .filter(Boolean); - - if (!files.length) return; - - event.preventDefault(); - target.textContent = "Coller une image ici"; - if (await addScreenshotFiles(target.dataset.toolboxId, target.dataset.moduleId, files)) { - refreshToolboxView(target); - } -}); - -document.addEventListener("submit", (event) => { - const form = event.target.closest("[data-action='add-check-item']"); - if (!form) return; - event.preventDefault(); - const label = new FormData(form).get("label")?.trim(); - if (!label) return; - const qtyTarget = parsePositiveInt(new FormData(form).get("qty"), 1); - const data = normalizeChecklistData(getModuleData(form.dataset.toolboxId, form.dataset.moduleId, { items: [] })); - data.items.push({ id: uid("item"), label, qtyTarget, qtyCurrent: 0 }); - setModuleData(form.dataset.toolboxId, form.dataset.moduleId, data); - form.reset(); - refreshToolboxView(form); -}); - -document.addEventListener("input", (event) => { - const mhwildsNameInput = event.target.closest("[data-action='set-mhwilds-name']"); - if (mhwildsNameInput) { - const category = mhwildsNameInput.dataset.category; - mhwildsState.filters[category].name = mhwildsNameInput.value; - refreshMhwildsResults(category); - return; - } - - const target = event.target.closest("[data-action='save-note']"); - if (!target) return; - setModuleData(target.dataset.toolboxId, target.dataset.moduleId, { text: target.value }); -}); - -window.addEventListener("hashchange", rerender); -rerender(); diff --git a/website/src/components/Icon.jsx b/website/src/components/Icon.jsx new file mode 100644 index 0000000..2cc6b7f --- /dev/null +++ b/website/src/components/Icon.jsx @@ -0,0 +1,4 @@ +export function Icon({ name }) { + const className = name === "trash" ? "ui-icon-trash" : `ui-icon-${name}`; + return