diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..3e307df --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(npm *)", + "Bash(git *)", + "Bash(pm2 *)", + "Bash(curl *)", + "Bash(ls *)", + "Bash(cat *)", + "Bash(grep *)", + "Bash(mkdir *)", + "Bash(cp *)", + "Bash(mv *)", + "Bash(node *)", + "Bash(npx *)", + "Write(*)" + ] + } +} diff --git a/.env.example b/.env.example index d8ab4ea..39c63b3 100644 --- a/.env.example +++ b/.env.example @@ -10,9 +10,8 @@ REDIS_URL=redis://localhost:6379 # Frontend CORS (virgule-séparé pour multi-origin) FRONTEND_URL=http://localhost:5173 -# SuperOAuth — service externe d'authentification +# SuperOAuth — service externe d'authentification (introspection, pas de secret JWT) SUPER_OAUTH_URL=http://localhost:3000 -SUPER_OAUTH_JWT_SECRET= # Cookie signing COOKIE_SECRET= diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..2a43365 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,59 @@ +name: CI/CD — Build & Deploy + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-deploy: + name: Build & Deploy + runs-on: vps-runner + + steps: + - uses: actions/checkout@v4 + + # ── Backend ────────────────────────────────────────────────────────────── + - name: Install & build backend + run: | + npm ci + npm run build + + - name: Deploy backend + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + mkdir -p /var/www/tetardpg/backend + rsync -a --delete dist/ /var/www/tetardpg/backend/dist/ + rsync -a package.json package-lock.json /var/www/tetardpg/backend/ + cd /var/www/tetardpg/backend && npm ci --omit=dev + + - name: Restart pm2 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + su - tetardtek-brain -c 'pm2 reload tetardpg-backend --update-env' + + # ── Frontend ───────────────────────────────────────────────────────────── + - name: Install & build frontend + working-directory: frontend + env: + VITE_API_URL: https://tetardpg.tetardtek.com/api + VITE_OAUTH_URL: https://superoauth.tetardtek.com + VITE_OAUTH_CLIENT_ID: tetardpg + run: | + npm ci + npm run build + + - name: Deploy frontend + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + mkdir -p /var/www/tetardpg/frontend/dist + rsync -a --delete frontend/dist/ /var/www/tetardpg/frontend/dist/ + + # ── Smoke test ─────────────────────────────────────────────────────────── + - name: Smoke test API + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + sleep 3 + curl -sf http://localhost:4000/api/health | grep -q '"ok"' + echo "✅ API health OK" diff --git a/SPRINT4.md b/SPRINT4.md new file mode 100644 index 0000000..815c0e9 --- /dev/null +++ b/SPRINT4.md @@ -0,0 +1,292 @@ +# TetaRdPG — Brief Sprint 4 + +> Statut : ⬜ À lancer +> Objectif : Succès individuels + Succès communautaires + Hall of Fame + Profil joueur enrichi +> Stack : NestJS · MySQL · TypeORM +> Prérequis : Sprint 3 livré ✅ (items, forge, craft, economy, twitch) +> Source design : `TetaRdPG/Sprint 4 _ Focus Succès & Hall of Fame.docx` + `Annexes/5. Système de succès.docx` + +--- + +## Scope Sprint 4 + +### ✅ In scope + +- Entité `achievements` — catalogue de succès avec critères de déblocage +- Entité `player_achievements` — suivi progression par joueur +- 5 catégories : Progression, Combat, Zones, Équipements, Économie +- Récompenses au déblocage : Or, XP bonus, titres honorifiques +- Entité `community_goals` — objectifs collectifs (monstres tués, TetardCoin cumulés) +- Barre de progression communautaire +- Récompenses communautaires : boosts globaux temporaires (XP/loot) +- Hall of Fame mensuel — classement contributeurs + badges +- Interface profil enrichi : badges, titres, % progression succès +- Seeds : 15 succès individuels + 3 objectifs communautaires +- API : voir section dédiée + +### ❌ Out of scope + +- Notifications Twitch temps réel (extension Twitch) — Sprint 5 +- GIGABOSS communautaire (événement 72h) — Sprint événements +- Marché communautaire (échange joueurs) — Sprint économie avancée +- Guildes et alliances — Sprint social +- Boutique événementielle — Sprint économie avancée +- Frontend React complet + +--- + +## Décisions de design (game-designer) + +| Décision | Valeur | Justification | +|----------|--------|---------------| +| Tracking succès | Event-driven : chaque action (combat, craft, forge, level) émet un check | Pas de cron — cohérent avec le pattern lazy du projet | +| Catégories succès | 5 : progression, combat, zones, equipment, economy | GDD §5.1 | +| Paliers succès | 3 niveaux par succès (bronze/silver/gold) | Engagement long terme | +| Récompenses déblocage | Or + titre. Pas d'item pour éviter la complexité inventaire Sprint 4 | Simplification — items récompense = Sprint 5 | +| Titres joueur | 1 titre actif à la fois, affiché sur le profil | GDD §4 titres liés aux zones | +| Community goals | Reset mensuel, contribution individuelle trackée | GDD §5.2 | +| Hall of Fame | Classement mensuel, top 10, badges persistants | GDD §5.3 | +| Boost communautaire | Stocké en DB, appliqué comme multiplicateur dans combat/craft | Ex: +20% XP pendant 3j | +| Progression communautaire | Compteur global incrémenté à chaque action qualifiante | Pas de WebSocket — poll GET | + +--- + +## Schéma DB + +### `achievements` +``` +id uuid PK +key varchar(50) UNIQUE -- 'combat_100', 'level_50', 'zone_marais_complete' +name varchar(100) +description text +category varchar(20) -- 'progression' | 'combat' | 'zones' | 'equipment' | 'economy' +tier varchar(10) -- 'bronze' | 'silver' | 'gold' +criteria_type varchar(30) -- 'combat_wins' | 'level_reached' | 'gold_accumulated' | ... +criteria_value int -- seuil à atteindre +reward_gold int default 0 +reward_title varchar(100) NULL -- titre débloqué (nullable) +``` + +### `player_achievements` +``` +id uuid PK +character_id uuid FK characters +achievement_id uuid FK achievements +progress int default 0 -- compteur courant +unlocked boolean default false +unlocked_at timestamp NULL +``` + +### `community_goals` +``` +id uuid PK +name varchar(100) +description text +criteria_type varchar(30) -- 'total_monsters_killed' | 'total_tetardcoin' | ... +target_value bigint -- objectif collectif +current_value bigint default 0 +reward_type varchar(30) -- 'xp_boost' | 'loot_boost' +reward_multiplier decimal(3,2) -- ex: 1.20 = +20% +reward_duration_hours int -- durée du boost +period_start date +period_end date +completed boolean default false +completed_at timestamp NULL +``` + +### `community_contributions` +``` +id uuid PK +community_goal_id uuid FK community_goals +character_id uuid FK characters +contribution_value bigint default 0 +``` + +### `hall_of_fame` +``` +id uuid PK +character_id uuid FK characters +period varchar(7) -- '2026-04' format YYYY-MM +rank int +contribution_total bigint +badge varchar(50) -- 'top1_april_2026' +``` + +### `active_boosts` (communautaires) +``` +id uuid PK +boost_type varchar(30) -- 'xp_boost' | 'loot_boost' +multiplier decimal(3,2) +expires_at timestamp +source_goal_id uuid FK community_goals +``` + +--- + +## Seeds + +### Succès individuels (15) + +| Key | Nom | Catégorie | Tier | Critère | Seuil | Récompense Or | Titre | +|-----|-----|-----------|------|---------|-------|---------------|-------| +| `combat_10` | Apprenti Guerrier | combat | bronze | combat_wins | 10 | 50 | — | +| `combat_100` | Guerrier Aguerri | combat | silver | combat_wins | 100 | 200 | Guerrier Aguerri | +| `combat_1000` | Légende du Combat | combat | gold | combat_wins | 1000 | 1000 | Légende | +| `level_10` | Aventurier | progression | bronze | level_reached | 10 | 100 | — | +| `level_50` | Héros | progression | silver | level_reached | 50 | 500 | Héros | +| `level_100` | Légende Vivante | progression | gold | level_reached | 100 | 2000 | Légende Vivante | +| `gold_1000` | Marchand | economy | bronze | gold_accumulated | 1000 | 100 | — | +| `gold_10000` | Négociant | economy | silver | gold_accumulated | 10000 | 500 | Négociant | +| `gold_100000` | Magnat | economy | gold | gold_accumulated | 100000 | 2000 | Magnat | +| `forge_5` | Apprenti Forgeron | equipment | bronze | forge_upgrades | 5 | 100 | — | +| `forge_25` | Maître Forgeron | equipment | silver | forge_upgrades | 25 | 500 | Maître Forgeron | +| `forge_100` | Forgeron Légendaire | equipment | gold | forge_upgrades | 100 | 2000 | Forgeron Légendaire | +| `craft_5` | Artisan Novice | equipment | bronze | craft_completed | 5 | 75 | — | +| `craft_25` | Artisan Confirmé | equipment | silver | craft_completed | 25 | 300 | Artisan | +| `craft_100` | Grand Artisan | equipment | gold | craft_completed | 100 | 1500 | Grand Artisan | + +### Objectifs communautaires (3) + +| Nom | Critère | Cible | Boost | Durée | +|-----|---------|-------|-------|-------| +| Chasse aux Monstres | total_monsters_killed | 10 000 | +20% XP | 72h | +| Trésor Communautaire | total_gold_earned | 1 000 000 | +15% loot | 48h | +| Fièvre de la Forge | total_forge_upgrades | 500 | +10% XP | 48h | + +--- + +## API Sprint 4 + +``` +# Succès individuels +GET /api/achievements → catalogue complet des succès +GET /api/achievements/me → progression du joueur (avec %) +POST /api/achievements/claim/:id → réclamer la récompense d'un succès débloqué + +# Succès communautaires +GET /api/community/goals → objectifs en cours + barre progression +GET /api/community/goals/:id/top → top 10 contributeurs d'un objectif +GET /api/community/boosts → boosts actifs (multiplicateurs en cours) + +# Hall of Fame +GET /api/halloffame/current → classement du mois en cours +GET /api/halloffame/:period → classement historique (ex: 2026-04) + +# Profil enrichi +GET /api/profile/me → stats + titre actif + badges + succès count +PUT /api/profile/title → { title: "Héros" } → changer titre actif +``` + +--- + +## Architecture modules + +``` +src/ +├── achievement/ +│ ├── achievement.entity.ts +│ ├── player-achievement.entity.ts +│ ├── achievement.module.ts +│ ├── achievement.service.ts → check + unlock logic +│ ├── achievement.controller.ts +│ └── achievement.listener.ts → écoute events combat/craft/forge/levelup +├── community/ +│ ├── community-goal.entity.ts +│ ├── community-contribution.entity.ts +│ ├── active-boost.entity.ts +│ ├── community.module.ts +│ ├── community.service.ts +│ └── community.controller.ts +├── halloffame/ +│ ├── hall-of-fame.entity.ts +│ ├── halloffame.module.ts +│ ├── halloffame.service.ts → calcul mensuel + badge attribution +│ └── halloffame.controller.ts +├── profile/ +│ ├── profile.module.ts +│ ├── profile.service.ts +│ └── profile.controller.ts +└── database/ + ├── achievements-seed.ts + └── community-goals-seed.ts +``` + +--- + +## Intégration modules existants + +### CombatService — émission événements succès + +```typescript +// Après résolution combat — émettre pour achievement tracker +if (result.winner === 'player') { + this.eventEmitter.emit('achievement.check', { + characterId: character.id, + type: 'combat_wins', + increment: 1, + }); + this.eventEmitter.emit('community.contribute', { + characterId: character.id, + type: 'total_monsters_killed', + increment: 1, + }); +} +``` + +### ForgeService / CraftService — même pattern + +```typescript +// Après forge réussie +this.eventEmitter.emit('achievement.check', { + characterId, type: 'forge_upgrades', increment: 1, +}); +this.eventEmitter.emit('community.contribute', { + characterId, type: 'total_forge_upgrades', increment: 1, +}); +``` + +### Boosts actifs — application dans CombatEngine + +```typescript +// Dans CombatService — vérifier boosts communautaires actifs +const xpBoost = await this.communityService.getActiveMultiplier('xp_boost'); +rewards.xp = Math.floor(baseXp * xpBoost); // xpBoost = 1.0 si aucun boost +``` + +--- + +## Migration TypeORM + +``` +Sprint4Achievements — 6 tables : + achievements, player_achievements, + community_goals, community_contributions, + hall_of_fame, active_boosts + ++ ALTER characters ADD active_title VARCHAR(100) NULL ++ ALTER characters ADD total_gold_earned BIGINT DEFAULT 0 -- tracking cumulé pour succès +``` + +--- + +## Critères de validation integrator + +- [ ] `GET /api/achievements` → 15 succès seedés, 5 catégories +- [ ] `POST /api/combat/start` → victoire → `player_achievements.progress` incrémenté pour `combat_wins` +- [ ] 10 victoires → succès `combat_10` débloqué automatiquement +- [ ] `GET /api/achievements/me` → progression visible avec % +- [ ] `POST /api/achievements/claim/:id` → or crédité, titre disponible +- [ ] Claim succès déjà réclamé → 400 +- [ ] `PUT /api/profile/title` → titre actif changé +- [ ] `GET /api/profile/me` → titre, badges, count succès +- [ ] `GET /api/community/goals` → 3 objectifs avec barre progression +- [ ] Combat victoire → `community_contributions` incrémentée +- [ ] `GET /api/community/goals/:id/top` → top 10 contributeurs +- [ ] Objectif communautaire atteint → boost créé dans `active_boosts` +- [ ] `GET /api/community/boosts` → multiplicateur actif +- [ ] Combat avec boost actif → XP = baseXp × multiplier +- [ ] Boost expiré → non retourné par GET +- [ ] `GET /api/halloffame/current` → classement mois en cours +- [ ] Sans cookie → 401 sur toutes les routes protégées +- [ ] Level up → `player_achievements.progress` incrémenté pour `level_reached` +- [ ] Forge → incrémente `forge_upgrades` + `total_forge_upgrades` communautaire diff --git a/TetaRdPG/Annexes/1. Formules d_Endurance.docx b/TetaRdPG/Annexes/1. Formules d_Endurance.docx new file mode 100644 index 0000000..6d18cf7 Binary files /dev/null and b/TetaRdPG/Annexes/1. Formules d_Endurance.docx differ diff --git a/TetaRdPG/Annexes/2. Formules Système Combat.docx b/TetaRdPG/Annexes/2. Formules Système Combat.docx new file mode 100644 index 0000000..80bb59f Binary files /dev/null and b/TetaRdPG/Annexes/2. Formules Système Combat.docx differ diff --git a/TetaRdPG/Annexes/3. Formules progression & XP.docx b/TetaRdPG/Annexes/3. Formules progression & XP.docx new file mode 100644 index 0000000..3d5e467 Binary files /dev/null and b/TetaRdPG/Annexes/3. Formules progression & XP.docx differ diff --git a/TetaRdPG/Annexes/4. Artisanat & Forge.docx b/TetaRdPG/Annexes/4. Artisanat & Forge.docx new file mode 100644 index 0000000..726bb2d Binary files /dev/null and b/TetaRdPG/Annexes/4. Artisanat & Forge.docx differ diff --git a/TetaRdPG/Annexes/5. Système de succès.docx b/TetaRdPG/Annexes/5. Système de succès.docx new file mode 100644 index 0000000..c4e6a4b Binary files /dev/null and b/TetaRdPG/Annexes/5. Système de succès.docx differ diff --git a/TetaRdPG/Annexes/6. Système Économique.docx b/TetaRdPG/Annexes/6. Système Économique.docx new file mode 100644 index 0000000..8612595 Binary files /dev/null and b/TetaRdPG/Annexes/6. Système Économique.docx differ diff --git a/TetaRdPG/Feuille de Route Post-Lancement (v1.1+).docx b/TetaRdPG/Feuille de Route Post-Lancement (v1.1+).docx new file mode 100644 index 0000000..e3ea9df Binary files /dev/null and b/TetaRdPG/Feuille de Route Post-Lancement (v1.1+).docx differ diff --git a/TetaRdPG/Game Design Document (GDD) - TetaRdPG.docx b/TetaRdPG/Game Design Document (GDD) - TetaRdPG.docx new file mode 100644 index 0000000..fedde60 Binary files /dev/null and b/TetaRdPG/Game Design Document (GDD) - TetaRdPG.docx differ diff --git a/TetaRdPG/Jalon & Responsabilité.docx b/TetaRdPG/Jalon & Responsabilité.docx new file mode 100644 index 0000000..ebf0bef Binary files /dev/null and b/TetaRdPG/Jalon & Responsabilité.docx differ diff --git a/TetaRdPG/Roadmap de Développement.docx b/TetaRdPG/Roadmap de Développement.docx new file mode 100644 index 0000000..feb8f0f Binary files /dev/null and b/TetaRdPG/Roadmap de Développement.docx differ diff --git a/TetaRdPG/Sprint/Sprint 1 _ Objectifs et Tâches Prioritaires.docx b/TetaRdPG/Sprint/Sprint 1 _ Objectifs et Tâches Prioritaires.docx new file mode 100644 index 0000000..e5105f4 Binary files /dev/null and b/TetaRdPG/Sprint/Sprint 1 _ Objectifs et Tâches Prioritaires.docx differ diff --git a/TetaRdPG/Sprint/Sprint 2 _ Focus Combat PvE.docx b/TetaRdPG/Sprint/Sprint 2 _ Focus Combat PvE.docx new file mode 100644 index 0000000..c6ae9ed Binary files /dev/null and b/TetaRdPG/Sprint/Sprint 2 _ Focus Combat PvE.docx differ diff --git a/TetaRdPG/Sprint/Sprint 3 _ Focus Artisanat & Forge.docx b/TetaRdPG/Sprint/Sprint 3 _ Focus Artisanat & Forge.docx new file mode 100644 index 0000000..3898de3 Binary files /dev/null and b/TetaRdPG/Sprint/Sprint 3 _ Focus Artisanat & Forge.docx differ diff --git a/TetaRdPG/Sprint/Sprint 4 _ Focus Succès & Hall of Fame.docx b/TetaRdPG/Sprint/Sprint 4 _ Focus Succès & Hall of Fame.docx new file mode 100644 index 0000000..8421a29 Binary files /dev/null and b/TetaRdPG/Sprint/Sprint 4 _ Focus Succès & Hall of Fame.docx differ diff --git a/TetaRdPG/Sprint/Sprint 5 _ Tests Bêta Fermée & Équilibrage.docx b/TetaRdPG/Sprint/Sprint 5 _ Tests Bêta Fermée & Équilibrage.docx new file mode 100644 index 0000000..58d22f4 Binary files /dev/null and b/TetaRdPG/Sprint/Sprint 5 _ Tests Bêta Fermée & Équilibrage.docx differ diff --git a/TetaRdPG/Sprint/Sprint 6 _ Lancement Public & Suivi Initial.docx b/TetaRdPG/Sprint/Sprint 6 _ Lancement Public & Suivi Initial.docx new file mode 100644 index 0000000..e34764a Binary files /dev/null and b/TetaRdPG/Sprint/Sprint 6 _ Lancement Public & Suivi Initial.docx differ diff --git a/TetaRdPG/Synthèse 18_03_2025.docx b/TetaRdPG/Synthèse 18_03_2025.docx new file mode 100644 index 0000000..7dc9b53 Binary files /dev/null and b/TetaRdPG/Synthèse 18_03_2025.docx differ diff --git a/archive/ULTRAIMPORTANTNEJAMAISPRIMER.md/Visions1.md b/archive/ULTRAIMPORTANTNEJAMAISPRIMER.md/Visions1.md new file mode 100644 index 0000000..9f5036c --- /dev/null +++ b/archive/ULTRAIMPORTANTNEJAMAISPRIMER.md/Visions1.md @@ -0,0 +1,80 @@ +Cas concrets. Aucun padding. + + --- + NO HANDOFF — le brain qui sait qui il est + + Lundi 7h. Tu ouvres une session sans rien préparer. + Layer 0 charge : KERNEL, BHP schema, règles de collaboration, frozen layer. + La todo dit : brainstorm architecture. + Le brain répond : "session brainstorm — pas de contexte projet chargé, espace libre." + + Tu fais de l'architecture propre parce qu'il n'y a pas de bruit du sprint de vendredi. Le cold start n'est pas un handicap — c'est ce qui rend la pensée architecturale possible. Un brain qui cold-start bien sur un brainstorm vaut plus qu'un brain qui traîne 400 lignes de workspace RAM d'un sprint de jeu. + + --- + SEMI — le chirurgien qui n'a besoin que du dossier + + 22h. Bug critique en prod sur SuperOAuth. Token refresh qui expire. + Layer 0 charge. Layer 1 partiel : position debug × SuperOAuth. + Le brain sait que SuperOAuth tourne port 3006, pm2 cluster, Redis dédié. + Il ne charge pas le sprint entier, pas le workspace de la semaine. + + Tu as ce qu'il faut en 30 secondes. Tu fixes. Tu fermes. + Pas de bruit. Pas de friction. Le bon contexte au bon niveau. + + --- + SEMI+ — lundi matin sur un sprint vivant + + TetaRdPG Sprint 4. Tu n'as pas de session ouverte depuis 3 jours. + Layer 0 + Layer 1 complet : état du sprint, ce qui est livré, ce qui reste, les décisions d'architecture du GDD. + Pas besoin de handoff de la dernière session — l'état du projet suffit. + + Tu reprends en 2 minutes. Le brain n'a pas besoin de ta mémoire — il a la sienne. + + --- + FULL — la continuation chirurgicale + + Hier soir, refacto TypeORM complexe sur OriginsDigital. Session coupée à 23h en plein milieu d'une migration. + Ce matin : FULL HANDOFF. Workspace RAM complet — les 3 fichiers en cours, la décision d'hier sur le cascade delete, le message d'erreur non résolu. + + Tu reprends exactement là. Pas de reconstruction. Pas de "où j'en étais déjà ?". Continuité parfaite sur une tâche qui l'exige. + + --- + Le gradient intelligent dans un sprint + + Lundi matin → SEMI+ reprendre l'état du sprint + Lundi soir → FULL continuation directe session d'avant + Mardi matin → SEMI+ nouveau jour, état suffit + Mercredi, bug → SEMI juste la position debug, rien d'autre + Vendredi close → FULL wrap complet, scribe, metabolism + + Tu ne choisis pas toujours FULL parce que tu n'en as pas toujours besoin. Le niveau juste = moins de bruit, plus de vitesse. + + --- + Multi-agent — le cas qui prouve tout + + Sprint OriginsDigital. Agent A : backend. Agent B : frontend. + + - Layer 0 : identique pour les deux — même identité, même règles + - Layer 1 : partagé — même état de sprint, même API contracts + - Layer 2 : isolé — Agent A a son workspace backend, Agent B le sien + + Agent B n'a pas besoin du workspace RAM d'Agent A. Il a besoin de savoir ce que l'API expose — c'est Layer 1. Le FULL d'Agent A ne pollue pas Agent B. + + C'est ça le vrai gain : des agents qui partagent l'identité et l'état, mais pas la mémoire de travail. + + --- + Brain-as-a-Service — la promesse qui tient + + brain new installe Layer 0 sur une machine vierge. + Première session d'un utilisateur : NO HANDOFF complet. + Pourtant il est productif immédiatement — parce que Layer 0 est solide. + + C'est la promesse qu'on peut vendre : un brain qui cold-start bien est un brain qui scale. + Si NO HANDOFF est cassé, brain new ne marche pas. Layer 0 est le produit. + + --- + ⚡ Coach — tu vois ce que ça implique ? + + Layer 0 n'est pas juste de la philosophie. C'est le critère de qualité mesurable du système. Si NO HANDOFF est productif en moins de 2 minutes → Layer 0 est bon. Sinon il est insuffisant. + + C'est un KPI qu'on peut tester. \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0fca6f0 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..b3e61d0 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3139 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "lucide-react": "^0.577.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "autoprefixer": "^10.4.27", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001779", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz", + "integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "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.0.0-alpha.31", + "esbuild": "^0.27.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 + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7447688 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "lucide-react": "^0.577.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "autoprefixer": "^10.4.27", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.56.1", + "vite": "^8.0.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..23e00e2 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,51 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AuthProvider, useAuth } from './context/AuthContext'; +import { Layout } from './components/Layout'; +import { LoginPage } from './pages/LoginPage'; +import { AuthCallback } from './pages/AuthCallback'; +import { DashboardPage } from './pages/DashboardPage'; +import { CombatPage } from './pages/CombatPage'; +import { InventoryPage } from './pages/InventoryPage'; +import { CraftPage } from './pages/CraftPage'; +import { ForgePage } from './pages/ForgePage'; + +const qc = new QueryClient({ defaultOptions: { queries: { retry: 1, staleTime: 30_000 } } }); + +function ProtectedLayout({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth(); + if (loading) return ( +
+ Chargement… +
+ ); + if (!user) return ; + return {children}; +} + +function AppRoutes() { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ); +} + +export default function App() { + return ( + + + + + + + + ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..88a1e5b --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,59 @@ +const BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:4000/api'; + +let refreshPromise: Promise | null = null; + +async function tryRefresh(): Promise { + if (refreshPromise) return refreshPromise; + refreshPromise = (async () => { + try { + const res = await fetch(`${BASE}/auth/refresh`, { + method: 'POST', + credentials: 'include', + }); + return res.ok; + } catch { + return false; + } finally { + refreshPromise = null; + } + })(); + return refreshPromise; +} + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + credentials: 'include', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + ...options, + }); + + if (res.status === 401 && path !== '/auth/refresh') { + const refreshed = await tryRefresh(); + if (refreshed) { + const retry = await fetch(`${BASE}${path}`, { + credentials: 'include', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + ...options, + }); + if (retry.ok) { + if (retry.status === 204) return undefined as T; + return retry.json(); + } + } + window.dispatchEvent(new Event('auth:expired')); + throw new Error('Session expired'); + } + + if (!res.ok) { + const err = await res.json().catch(() => ({ message: res.statusText })); + throw new Error(err.message ?? `HTTP ${res.status}`); + } + if (res.status === 204) return undefined as T; + return res.json(); +} + +export const api = { + get: (path: string) => request(path), + post: (path: string, body?: unknown) => request(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }), + del: (path: string) => request(path, { method: 'DELETE' }), +}; diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts new file mode 100644 index 0000000..a72e42c --- /dev/null +++ b/frontend/src/api/endpoints.ts @@ -0,0 +1,54 @@ +import { api } from './client'; +import type { + User, Character, Monster, CombatResult, CombatLog, + CharacterItem, CharacterMaterial, Recipe, CraftJob, Item, +} from './types'; + +// Auth +export const authApi = { + setSession: (token: string, refreshToken?: string) => + api.post('/auth/session', { token, refreshToken }), + me: () => api.get('/auth/me'), + logout: () => api.post('/auth/logout'), +}; + +// Character +export const characterApi = { + create: (name: string, stats: Record) => + api.post('/characters', { name, ...stats }), + me: () => api.get('/characters/me'), +}; + +// Combat +export const combatApi = { + monsters: () => api.get('/monsters'), + start: (monsterId: string, attackType: string) => api.post('/combat/start', { monsterId, attackType }), + history: () => api.get('/combat/history'), +}; + +// Items +export const itemApi = { + catalogue: () => api.get('/items'), + inventory: () => api.get('/items/inventory'), + equip: (id: string) => api.post(`/items/equip/${id}`), + unequip: (slot: 'weapon' | 'armor') => api.post(`/items/unequip/${slot}`), +}; + +// Materials +export const materialApi = { + inventory: () => api.get('/materials/inventory'), +}; + +// Craft +export const craftApi = { + recipes: () => api.get('/craft/recipes'), + start: (recipeId: string) => api.post('/craft/start', { recipeId }), + active: () => api.get('/craft/active'), + collect: (jobId: string) => api.post(`/craft/collect/${jobId}`), +}; + +// Forge +export const forgeApi = { + upgrade: (characterItemId: string) => + api.post<{ success: boolean; newForgeLevel: number; item: CharacterItem }>('/forge/upgrade', { characterItemId }), +}; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..cc9411d --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,126 @@ +export interface User { + id: string; + username: string; + email: string | null; + createdAt: string; +} + +export interface Character { + id: string; + name: string; + level: number; + xp: number; + gold: number; + force: number; + agilite: number; + intelligence: number; + chance: number; + vitalite: number; + hpCurrent: number; + hpMax: number; + endurance: number; // calculé à la lecture + enduranceMax: number; + statPoints: number; + createdAt: string; +} + +export interface Monster { + id: string; + name: string; + levelMin: number; + levelMax: number; + hp: number; + attack: number; + defense: number; + attackType: 'melee' | 'ranged' | 'magic'; + xpReward: number; + goldMin: number; + goldMax: number; +} + +export interface CombatRound { + round: number; + playerDamage: number; + playerCrit: boolean; + monsterDodged: boolean; + monsterDamage: number; + playerDodged: boolean; + playerHp: number; + monsterHp: number; + log: string[]; +} + +export interface CombatResult { + winner: 'player' | 'monster'; + rounds: CombatRound[]; + xpGained?: number; + goldGained?: number; + enduranceCost: number; + loot?: { material: Material; quantity: number } | null; +} + +export interface CombatLog { + id: string; + monsterId: string; + monsterName?: string; + winner: 'player' | 'monster'; + xpGained: number; + goldGained: number; + createdAt: string; +} + +export type Rarity = 'common' | 'rare' | 'epic' | 'legendary'; + +export interface Item { + id: string; + name: string; + description: string; + type: 'weapon' | 'armor'; + rarity: Rarity; + attackBonus: number; + defenseBonus: number; + forceBonus: number; + agiliteBonus: number; + intelligenceBonus: number; + chanceBonus: number; + vitaliteBonus: number; +} + +export interface CharacterItem { + id: string; + item: Item; + forgeLevel: number; + equipped: boolean; + acquiredAt: string; +} + +export interface Material { + id: string; + name: string; + description: string; + rarity: Rarity; +} + +export interface CharacterMaterial { + id: string; + material: Material; + quantity: number; +} + +export interface Recipe { + id: string; + name: string; + resultItem: Item; + craftDurationSeconds: number; + enduranceCost: number; + ingredients: { materialId: string; materialName?: string; quantity: number }[]; +} + +export interface CraftJob { + id: string; + recipe: Recipe; + startedAt: string; + completedAt: string; + collected: boolean; + status: 'pending' | 'ready'; +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/Bar.tsx b/frontend/src/components/Bar.tsx new file mode 100644 index 0000000..383089d --- /dev/null +++ b/frontend/src/components/Bar.tsx @@ -0,0 +1,24 @@ +interface BarProps { + value: number; + max: number; + type: 'hp' | 'end' | 'xp'; + label?: string; + showValues?: boolean; +} + +export function Bar({ value, max, type, label, showValues = true }: BarProps) { + const pct = Math.min(100, Math.round((value / Math.max(max, 1)) * 100)); + return ( +
+ {(label || showValues) && ( +
+ {label && {label}} + {showValues && {value} / {max}} +
+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..9dbf059 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,90 @@ +import { Link, useLocation } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { Swords, Package, Hammer, User, LogOut, Shield } from 'lucide-react'; + +const NAV = [ + { to: '/dashboard', icon: User, label: 'Personnage' }, + { to: '/combat', icon: Swords, label: 'Combat' }, + { to: '/inventory', icon: Package, label: 'Inventaire' }, + { to: '/craft', icon: Hammer, label: 'Artisanat' }, + { to: '/forge', icon: Shield, label: 'Forge' }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + const { user, logout } = useAuth(); + const loc = useLocation(); + + return ( +
+ {/* Header */} +
+
+ 🐸 + TetaRdPG +
+ {user && ( +
+ {user.username} + +
+ )} +
+ +
+ {/* Sidebar nav */} + + + {/* Main content */} +
+ {children} +
+
+
+ ); +} diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..cc65988 --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,49 @@ +import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; +import { authApi } from '../api/endpoints'; +import type { User } from '../api/types'; + +interface AuthCtx { + user: User | null; + loading: boolean; + logout: () => Promise; + refresh: () => Promise; +} + +const Ctx = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const refresh = async () => { + try { + const u = await authApi.me(); + setUser(u); + } catch { + setUser(null); + } + }; + + useEffect(() => { + refresh().finally(() => setLoading(false)); + }, []); + + useEffect(() => { + const onExpired = () => setUser(null); + window.addEventListener('auth:expired', onExpired); + return () => window.removeEventListener('auth:expired', onExpired); + }, []); + + const logout = async () => { + await authApi.logout(); + setUser(null); + }; + + return {children}; +} + +export function useAuth() { + const ctx = useContext(Ctx); + if (!ctx) throw new Error('useAuth must be used within AuthProvider'); + return ctx; +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..c05f657 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,94 @@ +@import "tailwindcss"; + +@theme { + --color-rpg-bg: #0d0f14; + --color-rpg-surface: #161b25; + --color-rpg-border: #2a3448; + --color-rpg-gold: #f4c94e; + --color-rpg-gold-dim: #c49c2e; + --color-rpg-red: #e84040; + --color-rpg-green: #3ddc84; + --color-rpg-blue: #5ba4f5; + --color-rpg-purple: #a78bfa; + --color-rpg-text: #dce4f0; + --color-rpg-muted: #6b7a99; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background-color: #0d0f14; + color: #dce4f0; + font-family: system-ui, sans-serif; + min-height: 100vh; +} + +#root { min-height: 100vh; } + +/* Barres */ +.bar-track { background: #1e2535; border-radius: 4px; overflow: hidden; height: 10px; } +.bar-fill-hp { background: linear-gradient(90deg, #c0392b, #e84040); height: 100%; transition: width 0.4s ease; } +.bar-fill-end { background: linear-gradient(90deg, #1d6fa4, #5ba4f5); height: 100%; transition: width 0.4s ease; } +.bar-fill-xp { background: linear-gradient(90deg, #7c3aed, #a78bfa); height: 100%; transition: width 0.4s ease; } + +/* Cards */ +.card { background: #161b25; border: 1px solid #2a3448; border-radius: 8px; padding: 1rem; } +.card-gold { border-color: #c49c2e; } +.card-hover { cursor: pointer; transition: border-color 0.2s; } +.card-hover:hover { border-color: #f4c94e; } + +/* Boutons */ +.btn { font-weight: 600; padding: 0.5rem 1.25rem; border-radius: 6px; border: none; cursor: pointer; transition: opacity 0.2s; font-size: 0.875rem; } +.btn:disabled { opacity: 0.4; cursor: not-allowed; } +.btn-gold { background: linear-gradient(135deg, #c49c2e, #f4c94e); color: #0d0f14; } +.btn-gold:hover:not(:disabled) { opacity: 0.85; } +.btn-red { background: linear-gradient(135deg, #c0392b, #e84040); color: #fff; } +.btn-red:hover:not(:disabled) { opacity: 0.85; } +.btn-ghost { background: #1e2535; color: #dce4f0; border: 1px solid #2a3448; } +.btn-ghost:hover:not(:disabled) { background: #2a3448; } +.btn-blue { background: linear-gradient(135deg, #1d6fa4, #5ba4f5); color: #fff; } +.btn-blue:hover:not(:disabled) { opacity: 0.85; } + +/* Rareté */ +.rarity-common { color: #9ca3af; } +.rarity-rare { color: #5ba4f5; } +.rarity-epic { color: #a78bfa; } +.rarity-legendary { color: #f4c94e; } + +/* Badge */ +.badge { font-size: 0.7rem; font-weight: 700; padding: 2px 8px; border-radius: 99px; text-transform: uppercase; letter-spacing: 0.05em; } +.badge-green { background: #0d2a1a; color: #3ddc84; border: 1px solid #1a5c35; } +.badge-red { background: #2a0d0d; color: #e84040; border: 1px solid #5c1a1a; } +.badge-gold { background: #2a1f0d; color: #f4c94e; border: 1px solid #5c420d; } +.badge-blue { background: #0d1a2a; color: #5ba4f5; border: 1px solid #1a3f5c; } + +/* Scrollbar */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: #0d0f14; } +::-webkit-scrollbar-thumb { background: #2a3448; border-radius: 3px; } + +/* Séparateur */ +.divider { border: none; border-top: 1px solid #2a3448; margin: 1rem 0; } + +/* Input */ +.input-rpg { + background: #1e2535; + border: 1px solid #2a3448; + color: #dce4f0; + padding: 0.5rem 0.75rem; + border-radius: 6px; + font-size: 0.875rem; + width: 100%; + outline: none; + transition: border-color 0.2s; +} +.input-rpg:focus { border-color: #f4c94e; } +.input-rpg::placeholder { color: #6b7a99; } + +/* Combat log */ +.combat-log { background: #0d0f14; border: 1px solid #2a3448; border-radius: 6px; padding: 0.75rem; max-height: 260px; overflow-y: auto; font-size: 0.8rem; font-family: monospace; } +.log-player { color: #3ddc84; } +.log-monster { color: #e84040; } +.log-system { color: #f4c94e; } +.log-crit { color: #a78bfa; font-weight: bold; } diff --git a/frontend/src/lib/oauth.ts b/frontend/src/lib/oauth.ts new file mode 100644 index 0000000..df7b9a6 --- /dev/null +++ b/frontend/src/lib/oauth.ts @@ -0,0 +1,110 @@ +// OAuth 2.0 PKCE client — SuperOAuth consumer for TetaRdPG +// Adapted from OriginsDigital reference pattern + +const OAUTH_URL = import.meta.env.VITE_OAUTH_URL || ''; +const OAUTH_CLIENT_ID = import.meta.env.VITE_OAUTH_CLIENT_ID || ''; + +const SESSION_KEY_VERIFIER = 'trpg_pkce_verifier'; + +// --- PKCE helpers --- + +function base64UrlEncode(buffer: ArrayBuffer): string { + return btoa(String.fromCharCode(...new Uint8Array(buffer))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +export function generateCodeVerifier(): string { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return base64UrlEncode(array.buffer); +} + +export async function generateCodeChallenge(verifier: string): Promise { + const data = new TextEncoder().encode(verifier); + const digest = await crypto.subtle.digest('SHA-256', data); + return base64UrlEncode(digest); +} + +// --- Auth URL --- + +export async function buildAuthUrl( + redirectUri: string, + provider: string, + scope = 'openid profile email', + clientId = OAUTH_CLIENT_ID, +): Promise<{ url: string; verifier: string }> { + const verifier = generateCodeVerifier(); + const challenge = await generateCodeChallenge(verifier); + const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)).buffer); + + const params = new URLSearchParams({ + response_type: 'code', + client_id: clientId, + redirect_uri: redirectUri, + scope, + state, + provider, + code_challenge: challenge, + code_challenge_method: 'S256', + }); + + return { + url: `${OAUTH_URL}/oauth/authorize?${params.toString()}`, + verifier, + }; +} + +// --- Token exchange --- + +export interface TokenResponse { + access_token: string; + refresh_token?: string; + token_type: string; + expires_in: number; + scope?: string; +} + +export async function exchangeCode( + code: string, + verifier: string, + redirectUri: string, + clientId = OAUTH_CLIENT_ID, +): Promise { + const response = await fetch(`${OAUTH_URL}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + client_id: clientId, + code, + code_verifier: verifier, + redirect_uri: redirectUri, + }).toString(), + }); + + if (!response.ok) { + const text = await response.text().catch(() => ''); + throw new Error(`OAuth token exchange failed (${response.status}): ${text}`); + } + + const data = await response.json() as TokenResponse; + if (!data.access_token) throw new Error('No access_token in OAuth response'); + + return data; +} + +// --- PKCE verifier persistence (avant redirect) --- + +export function saveVerifier(verifier: string): void { + sessionStorage.setItem(SESSION_KEY_VERIFIER, verifier); +} + +export function loadVerifier(): string | null { + return sessionStorage.getItem(SESSION_KEY_VERIFIER); +} + +export function clearVerifier(): void { + sessionStorage.removeItem(SESSION_KEY_VERIFIER); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/AuthCallback.tsx b/frontend/src/pages/AuthCallback.tsx new file mode 100644 index 0000000..88788df --- /dev/null +++ b/frontend/src/pages/AuthCallback.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { exchangeCode, loadVerifier, clearVerifier } from '../lib/oauth'; +import { authApi } from '../api/endpoints'; +import { useAuth } from '../context/AuthContext'; + +export function AuthCallback() { + const navigate = useNavigate(); + const { refresh } = useAuth(); + const called = useRef(false); + const [status, setStatus] = useState<'loading' | 'error'>('loading'); + const [errorMsg, setErrorMsg] = useState(''); + + useEffect(() => { + if (called.current) return; + called.current = true; + + const params = new URLSearchParams(window.location.search); + const code = params.get('code'); + const error = params.get('error'); + + if (error) { + setStatus('error'); + setErrorMsg(error); + return; + } + + if (!code) { + navigate('/login?error=no_code', { replace: true }); + return; + } + + const verifier = loadVerifier(); + if (!verifier) { + navigate('/login?error=no_verifier', { replace: true }); + return; + } + + const redirectUri = `${window.location.origin}/auth/callback`; + + exchangeCode(code, verifier, redirectUri) + .then((tokens) => { + clearVerifier(); + return authApi.setSession(tokens.access_token, tokens.refresh_token); + }) + .then(() => refresh()) + .then(() => navigate('/dashboard', { replace: true })) + .catch(() => { + clearVerifier(); + navigate('/login?error=session_failed', { replace: true }); + }); + }, [navigate, refresh]); + + if (status === 'error') { + return ( +
+
+
💀
+

Erreur d'authentification

+

{errorMsg}

+
+
+ ); + } + + return ( +
+
+
⚔️
+

Connexion en cours…

+
+
+ ); +} diff --git a/frontend/src/pages/CombatPage.tsx b/frontend/src/pages/CombatPage.tsx new file mode 100644 index 0000000..09f19d1 --- /dev/null +++ b/frontend/src/pages/CombatPage.tsx @@ -0,0 +1,196 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { combatApi } from '../api/endpoints'; +import type { Monster, CombatResult } from '../api/types'; +import { Swords, Trophy, Skull, Clock } from 'lucide-react'; + +const ATTACK_TYPES = [ + { id: 'melee', label: 'Mêlée', emoji: '⚔️', stat: 'Force × 1.5' }, + { id: 'ranged', label: 'Distance', emoji: '🏹', stat: 'Agilité × 1.5' }, + { id: 'magic', label: 'Magie', emoji: '✨', stat: 'Intelligence × 1.5' }, +]; + +function MonsterCard({ m, selected, onSelect }: { m: Monster; selected: boolean; onSelect: () => void }) { + return ( +
+
+ {m.name} + Niv. {m.levelMin}–{m.levelMax} +
+
+ ❤️ {m.hp} + ⚔️ {m.attack} + 🛡️ {m.defense} + ⭐ {m.xpReward} XP + 💰 {m.goldMin}–{m.goldMax} +
+
+ ); +} + +function CombatLog({ result }: { result: CombatResult }) { + const won = result.winner === 'player'; + return ( +
+ {/* Résultat */} +
+ {won + ?
+ + Victoire ! +{result.xpGained} XP +{result.goldGained} or +
+ :
+ + Défaite… −50 endurance +
+ } + {result.loot && ( +
+ 🎁 Loot : {result.loot.material.name} ×{result.loot.quantity} +
+ )} +
+ + {/* Log de combat */} +

+ Log — {result.rounds.length} tour{result.rounds.length > 1 ? 's' : ''} +

+
+ {result.rounds.flatMap(r => + r.log.map((line, i) => { + const cls = line.includes('frappe') && !line.includes('Monstre') ? 'log-player' + : line.includes('Monstre') || line.includes('frappe') ? 'log-monster' + : line.includes('CRITIQUE') ? 'log-crit' + : 'log-system'; + return
[T{r.round}] {line}
; + }) + )} + {won + ?
══ Victoire ══
+ :
══ Défaite ══
+ } +
+
+ ); +} + +export function CombatPage() { + const qc = useQueryClient(); + const [selectedMonster, setSelectedMonster] = useState(null); + const [attackType, setAttackType] = useState('melee'); + const [lastResult, setLastResult] = useState(null); + + const { data: monsters, isLoading } = useQuery({ + queryKey: ['monsters'], + queryFn: combatApi.monsters, + }); + + const { data: history } = useQuery({ + queryKey: ['combatHistory'], + queryFn: combatApi.history, + }); + + const fight = useMutation({ + mutationFn: () => combatApi.start(selectedMonster!.id, attackType), + onSuccess: (result) => { + setLastResult(result); + qc.invalidateQueries({ queryKey: ['character'] }); + qc.invalidateQueries({ queryKey: ['combatHistory'] }); + }, + }); + + if (isLoading) return
Chargement des monstres…
; + + return ( +
+

⚔️ Combat

+ +
+ {/* Choix monstre */} +
+

+ Adversaire +

+
+ {monsters?.map(m => ( + setSelectedMonster(m)} + /> + ))} +
+
+ + {/* Panneau droite */} +
+ {/* Type d'attaque */} +

+ Type d'attaque +

+
+ {ATTACK_TYPES.map(a => ( +
setAttackType(a.id)} + style={{ display: 'flex', alignItems: 'center', gap: 10 }} + > + {a.emoji} +
+
{a.label}
+
{a.stat}
+
+
+ ))} +
+ + {/* Bouton combattre */} + + + {fight.isError && ( +

{(fight.error as Error).message}

+ )} + + {/* Historique récent */} + {history && history.length > 0 && ( +
+

+ Historique récent +

+
+ {history.slice(0, 5).map(h => ( +
+ + {h.winner === 'player' ? '✓' : '✗'} {h.monsterName ?? 'Monstre'} + + +{h.xpGained}xp +{h.goldGained}or +
+ ))} +
+
+ )} +
+
+ + {/* Résultat du dernier combat */} + {lastResult && } +
+ ); +} diff --git a/frontend/src/pages/CraftPage.tsx b/frontend/src/pages/CraftPage.tsx new file mode 100644 index 0000000..0acc4a2 --- /dev/null +++ b/frontend/src/pages/CraftPage.tsx @@ -0,0 +1,150 @@ +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { craftApi, materialApi } from '../api/endpoints'; +import type { Recipe, CraftJob } from '../api/types'; +import { Hammer, Clock, CheckCircle } from 'lucide-react'; + +function timeLeft(completedAt: string): string { + const diff = new Date(completedAt).getTime() - Date.now(); + if (diff <= 0) return 'Prêt !'; + const s = Math.ceil(diff / 1000); + return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; +} + +function ActiveCraft({ job, onCollect }: { job: CraftJob; onCollect: () => void }) { + const [, tick] = useState(0); + useEffect(() => { + const id = setInterval(() => tick(n => n + 1), 1000); + return () => clearInterval(id); + }, []); + + const ready = job.status === 'ready' || new Date(job.completedAt) <= new Date(); + return ( +
+
+
+ {ready ? : } +
+ {job.recipe.name} + + → {job.recipe.resultItem.name} + +
+
+
+ {!ready && {timeLeft(job.completedAt)}} + +
+
+
+ ); +} + +function RecipeCard({ recipe, onCraft, disabled, materials }: { + recipe: Recipe; + onCraft: () => void; + disabled: boolean; + materials: Map; +}) { + const canCraft = recipe.ingredients.every(ing => (materials.get(ing.materialId) ?? 0) >= ing.quantity); + + return ( +
+
+ {recipe.name} + {recipe.craftDurationSeconds}s · {recipe.enduranceCost} end. +
+
+ → {recipe.resultItem.name} +
+
+ {recipe.ingredients.map(ing => { + const have = materials.get(ing.materialId) ?? 0; + const ok = have >= ing.quantity; + return ( + + {ing.materialName ?? '?'} {have}/{ing.quantity} + + ); + })} +
+ +
+ ); +} + +export function CraftPage() { + const qc = useQueryClient(); + + const { data: recipes } = useQuery({ queryKey: ['recipes'], queryFn: craftApi.recipes }); + const { data: activeCraft, refetch: refetchActive } = useQuery({ queryKey: ['activeCraft'], queryFn: craftApi.active, refetchInterval: 5000 }); + const { data: mats } = useQuery({ queryKey: ['materials'], queryFn: materialApi.inventory }); + + const materialMap = new Map(mats?.map(cm => [cm.material.id, cm.quantity]) ?? []); + + const startMut = useMutation({ + mutationFn: (recipeId: string) => craftApi.start(recipeId), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['activeCraft'] }); qc.invalidateQueries({ queryKey: ['character'] }); qc.invalidateQueries({ queryKey: ['materials'] }); }, + }); + + const collectMut = useMutation({ + mutationFn: (jobId: string) => craftApi.collect(jobId), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['activeCraft'] }); qc.invalidateQueries({ queryKey: ['inventory'] }); refetchActive(); }, + }); + + const hasActive = activeCraft && 'id' in activeCraft; + + return ( +
+

+ Artisanat +

+ + {hasActive && ( + collectMut.mutate((activeCraft as CraftJob).id)} + /> + )} + + {hasActive && ( +
+ Un craft est en cours — tu ne peux pas en lancer un autre. +
+ )} + +
+ {recipes?.map(r => ( + startMut.mutate(r.id)} + disabled={hasActive || startMut.isPending} + materials={materialMap} + /> + ))} +
+ + {recipes?.length === 0 && ( +
+ Aucune recette disponible. +
+ )} +
+ ); +} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..b5134c9 --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,188 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { characterApi } from '../api/endpoints'; +import { Bar } from '../components/Bar'; +import { Zap, Heart, Star, Coins, Sword, Shield } from 'lucide-react'; + +const STATS = ['force', 'agilite', 'intelligence', 'chance', 'vitalite'] as const; +const STAT_LABELS: Record = { + force: 'Force', agilite: 'Agilité', intelligence: 'Intelligence', chance: 'Chance', vitalite: 'Vitalité', +}; + +function CreateCharacter() { + const qc = useQueryClient(); + const [name, setName] = useState(''); + const [pts, setPts] = useState>({ force:1, agilite:1, intelligence:1, chance:1, vitalite:1 }); + const used = Object.values(pts).reduce((a, b) => a + b, 0) - 5; + const remaining = 5 - used; + + const mut = useMutation({ + mutationFn: () => characterApi.create(name, pts), + onSuccess: () => qc.invalidateQueries({ queryKey: ['character'] }), + }); + + const adjust = (stat: string, delta: number) => { + const next = (pts[stat] ?? 1) + delta; + if (next < 1 || next > 10) return; + if (delta > 0 && remaining <= 0) return; + setPts(p => ({ ...p, [stat]: next })); + }; + + return ( +
+
+

Créer ton personnage

+

+ {remaining > 0 ? `${remaining} point${remaining > 1 ? 's' : ''} à répartir` : 'Tous les points répartis'} +

+ + setName(e.target.value)} + style={{ marginBottom: '1rem' }} + maxLength={30} + /> + +
+ {STATS.map(s => ( +
+ {STAT_LABELS[s]} +
+ + {pts[s]} + +
+
+ ))} +
+ + + + {mut.isError &&

{(mut.error as Error).message}

} +
+
+ ); +} + +export function DashboardPage() { + const { data: char, isLoading, isError } = useQuery({ + queryKey: ['character'], + queryFn: characterApi.me, + retry: 1, + }); + + if (isLoading) return
Chargement…
; + if (isError || !char) return ; + + const xpNext = Math.round(100 * Math.pow(char.level, 1.5)); + + return ( +
+ {/* Header perso */} +
+
🐸
+
+
+

{char.name}

+ Niveau {char.level} +
+
+ + {char.gold} or + + + {char.xp} / {xpNext} XP + + {(char as any).statPoints > 0 && ( + +{(char as any).statPoints} pts à répartir + )} +
+
+
+ +
+ {/* Barres vitales */} +
+

État

+
+
+
+ + PV + + {char.hpCurrent} / {char.hpMax} +
+ +
+
+
+ + Endurance + + {char.endurance} / {char.enduranceMax} +
+ +
+
+
+ + XP + + {char.xp} / {xpNext} +
+ +
+
+
+ + {/* Stats */} +
+

Statistiques

+
+ {STATS.map(s => ( +
+ {STAT_LABELS[s]} + {char[s]} +
+ ))} +
+ PV max + {char.hpMax} +
+
+
+ + {/* Équipement résumé */} +
+

Combat actuel

+
+
+ + Attaque : + {Math.floor(char.force * 1.5)} +
+
+ + Critique : + {(5 + char.chance * 0.2).toFixed(1)}% +
+
+ + Esquive : + {(5 + char.chance * 0.1).toFixed(1)}% +
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/ForgePage.tsx b/frontend/src/pages/ForgePage.tsx new file mode 100644 index 0000000..be851e4 --- /dev/null +++ b/frontend/src/pages/ForgePage.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { itemApi, forgeApi } from '../api/endpoints'; +import type { CharacterItem } from '../api/types'; +import { Shield, CheckCircle, XCircle, AlertTriangle } from 'lucide-react'; + +const FORGE_RISK = [0, 0, 0, 20, 30, 40]; +const FORGE_LABEL = ['—', '—', 'Garanti', '20% échec', '30% échec', '40% échec']; + +export function ForgePage() { + const qc = useQueryClient(); + const [selected, setSelected] = useState(null); + const [lastResult, setLastResult] = useState<{ success: boolean; newLevel: number } | null>(null); + + const { data: inventory, isLoading } = useQuery({ + queryKey: ['inventory'], + queryFn: itemApi.inventory, + }); + + const forgeMut = useMutation({ + mutationFn: () => forgeApi.upgrade(selected!.id), + onSuccess: (res) => { + setLastResult({ success: res.success, newLevel: res.newForgeLevel }); + qc.invalidateQueries({ queryKey: ['inventory'] }); + // Refresh selected item from updated inventory + setSelected(res.item); + }, + }); + + if (isLoading) return
Chargement…
; + + const forgeable = inventory ?? []; + const nextLevel = (selected?.forgeLevel ?? 0) + 1; + const risk = FORGE_RISK[nextLevel] ?? 40; + const atMax = selected && selected.forgeLevel >= 5; + + return ( +
+

+ Forge +

+ +
+ {/* Sélection item */} +
+

+ Choisir un équipement +

+ {forgeable.length === 0 ? ( +
+ Aucun item dans l'inventaire +
+ ) : ( +
+ {forgeable.map(ci => ( +
{ setSelected(ci); setLastResult(null); }} + style={{ display: 'flex', alignItems: 'center', gap: 10 }} + > + {ci.item.type === 'weapon' ? '⚔️' : '🛡️'} +
+
+ {ci.item.name} +
+
Niveau forge : {ci.forgeLevel}/5
+
+ {ci.forgeLevel > 0 && ( + +{ci.forgeLevel} + )} +
+ ))} +
+ )} +
+ + {/* Panneau forge */} +
+ {selected ? ( +
+
+
+ {selected.item.type === 'weapon' ? '⚔️' : '🛡️'} +
+
{selected.item.name}
+
Forge actuelle : +{selected.forgeLevel}
+
+ + {!atMax ? ( + <> +
+
Prochain niveau : +{nextLevel}
+
+ {risk === 0 + ? <> Succès garanti + : <> {FORGE_LABEL[nextLevel]} + } +
+
+ + + + ) : ( +
+ ✨ Niveau maximum atteint (+5) +
+ )} + + {/* Résultat */} + {lastResult && ( +
+ {lastResult.success + ?
+ Succès ! Item à +{lastResult.newLevel} +
+ :
+ Échec — l'item est inchangé +
+ } +
+ )} +
+ ) : ( +
+ Sélectionne un équipement à améliorer +
+ )} + + {/* Tableau des risques */} +
+

Risques par niveau

+
+ {[1,2,3,4,5].map(n => ( +
+ Niv. {n} + + {FORGE_LABEL[n]} + +
+ ))} +
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx new file mode 100644 index 0000000..6361576 --- /dev/null +++ b/frontend/src/pages/InventoryPage.tsx @@ -0,0 +1,147 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { itemApi, materialApi } from '../api/endpoints'; +import type { CharacterItem } from '../api/types'; +import { Package, Sword, Shield } from 'lucide-react'; + +const RARITY_LABEL: Record = { + common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire', +}; + +function ItemCard({ ci, onEquip, onUnequip }: { ci: CharacterItem; onEquip: () => void; onUnequip: () => void }) { + const { item } = ci; + const bonuses = [ + item.attackBonus && `+${item.attackBonus} ATK`, + item.defenseBonus && `+${item.defenseBonus} DEF`, + item.forceBonus && `+${item.forceBonus} FOR`, + item.agiliteBonus && `+${item.agiliteBonus} AGI`, + item.intelligenceBonus && `+${item.intelligenceBonus} INT`, + item.chanceBonus && `+${item.chanceBonus} CHA`, + item.vitaliteBonus && `+${item.vitaliteBonus} VIT`, + ].filter(Boolean).join(' · '); + + return ( +
+ {ci.equipped && ( + Équipé + )} + {ci.forgeLevel > 0 && ( + + +{ci.forgeLevel} + + )} +
+ {item.type === 'weapon' ? '⚔️' : '🛡️'} +
+
{item.name}
+
{RARITY_LABEL[item.rarity]}
+
+
+ {bonuses &&
{bonuses}
} +
+ {!ci.equipped + ? + : + } +
+
+ ); +} + +export function InventoryPage() { + const qc = useQueryClient(); + + const { data: inventory, isLoading: loadInv } = useQuery({ + queryKey: ['inventory'], + queryFn: itemApi.inventory, + }); + + const { data: materials, isLoading: loadMat } = useQuery({ + queryKey: ['materials'], + queryFn: materialApi.inventory, + }); + + const equipMut = useMutation({ + mutationFn: (id: string) => itemApi.equip(id), + onSuccess: () => qc.invalidateQueries({ queryKey: ['inventory'] }), + }); + + const unequipMut = useMutation({ + mutationFn: (slot: 'weapon' | 'armor') => itemApi.unequip(slot), + onSuccess: () => qc.invalidateQueries({ queryKey: ['inventory'] }), + }); + + if (loadInv || loadMat) return
Chargement…
; + + const weapons = inventory?.filter(ci => ci.item.type === 'weapon') ?? []; + const armors = inventory?.filter(ci => ci.item.type === 'armor') ?? []; + + return ( +
+

+ Inventaire +

+ + {inventory?.length === 0 && ( +
+ Inventaire vide — gagne des combats pour lootter des matériaux et crafter des équipements ! +
+ )} + + {/* Armes */} + {weapons.length > 0 && ( +
+

+ Armes ({weapons.length}) +

+
+ {weapons.map(ci => ( + equipMut.mutate(ci.id)} + onUnequip={() => unequipMut.mutate('weapon')} + /> + ))} +
+
+ )} + + {/* Armures */} + {armors.length > 0 && ( +
+

+ Armures ({armors.length}) +

+
+ {armors.map(ci => ( + equipMut.mutate(ci.id)} + onUnequip={() => unequipMut.mutate('armor')} + /> + ))} +
+
+ )} + + {/* Matériaux */} + {materials && materials.length > 0 && ( +
+

+ 🌿 Matériaux +

+
+ {materials.map(cm => ( +
+ 🌿 +
+
{cm.material.name}
+
×{cm.quantity}
+
+
+ ))} +
+
+ )} +
+ ); +} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..2bf9e24 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,76 @@ +import { buildAuthUrl, saveVerifier } from '../lib/oauth'; + +const PROVIDERS = [ + { id: 'discord', label: 'Discord', color: '#5865F2', emoji: '🎮' }, + { id: 'github', label: 'GitHub', color: '#24292e', emoji: '🐙' }, + { id: 'google', label: 'Google', color: '#ea4335', emoji: '🌐' }, +]; + +export function LoginPage() { + const login = async (provider: string) => { + const redirectUri = `${window.location.origin}/auth/callback`; + const { url, verifier } = await buildAuthUrl(redirectUri, provider); + saveVerifier(verifier); + window.location.href = url; + }; + + return ( +
+
+ {/* Logo */} +
+
🐸
+

TetaRdPG

+

+ RPG communautaire asynchrone +

+
+ + {/* Card login */} +
+

+ Connecte-toi pour commencer ton aventure +

+
+ {PROVIDERS.map(p => ( + + ))} +
+
+ +

+ En te connectant, tu acceptes les règles de la taverne du Têtard Prophétique. +

+
+
+ ); +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..af516fc --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..52a25ec --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:4000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/src/app.module.ts b/src/app.module.ts index 6d72e0f..1c2c8f1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,7 +25,7 @@ import { HealthController } from './common/health.controller'; type: 'mysql', url: config.get('DATABASE_URL'), autoLoadEntities: true, - synchronize: config.get('NODE_ENV') !== 'production', + synchronize: config.get('DB_SYNC') === 'true' || config.get('NODE_ENV') !== 'production', logging: config.get('NODE_ENV') === 'development', }), }), diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 0167d33..b83a601 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -4,10 +4,11 @@ import { Get, Body, Res, + Req, UseGuards, HttpCode, HttpStatus, - Req, + UnauthorizedException, } from '@nestjs/common'; import { Throttle } from '@nestjs/throttler'; import { Response, Request } from 'express'; @@ -30,6 +31,20 @@ export class AuthController { return this.authService.setSession(dto, res); } + @Post('refresh') + @HttpCode(HttpStatus.OK) + @Throttle({ default: { ttl: 60_000, limit: 10 } }) + async refresh( + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + ) { + const refreshToken = (req.signedCookies as Record)?.refresh_token; + if (!refreshToken) { + throw new UnauthorizedException('Pas de refresh token'); + } + return this.authService.refreshSession(res, refreshToken); + } + @Get('me') @UseGuards(AuthGuard) async getMe(@Req() req: Request & { user: User }) { diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 9623c4f..d396c25 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,23 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { User } from '../user/user.entity'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { AuthGuard } from './guards/auth.guard'; @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - secret: config.get('SUPER_OAUTH_JWT_SECRET'), - }), - }), - ], + imports: [TypeOrmModule.forFeature([User])], controllers: [AuthController], providers: [AuthService, AuthGuard], exports: [AuthGuard, TypeOrmModule], diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 3470692..454229f 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,9 +1,7 @@ import { Injectable, UnauthorizedException, - InternalServerErrorException, } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -11,62 +9,54 @@ import { Response } from 'express'; import { User } from '../user/user.entity'; import { SetSessionDto } from './dto/set-session.dto'; -// Payload émis par SuperOAuth -interface SuperOAuthPayload { - sub: string; // ID provider (Twitch ID, Discord ID…) - provider: string; // 'twitch' | 'discord' | 'google' | 'github' - username: string; - avatar_url?: string; - iat: number; - exp: number; +interface SuperOAuthUser { + id: string; + tenantId: string; + email: string | null; + nickname: string; + isActive: boolean; + linkedProviders: string[]; } +const COOKIE_NAME = 'session'; +const REFRESH_COOKIE_NAME = 'refresh_token'; + @Injectable() export class AuthService { + private readonly superOauthUrl: string; + constructor( @InjectRepository(User) private readonly userRepository: Repository, - private readonly jwtService: JwtService, private readonly configService: ConfigService, - ) {} + ) { + this.superOauthUrl = this.configService.getOrThrow('SUPER_OAUTH_URL'); + } - async setSession(dto: SetSessionDto, res: Response): Promise> { - let payload: SuperOAuthPayload; - - try { - payload = await this.jwtService.verifyAsync(dto.jwt, { - secret: this.configService.get('SUPER_OAUTH_JWT_SECRET'), - }); - } catch { - throw new UnauthorizedException('JWT SuperOAuth invalide ou expiré'); - } - - if (!payload.sub || !payload.provider || !payload.username) { - throw new UnauthorizedException('Payload JWT incomplet'); - } + async setSession(dto: SetSessionDto, res: Response): Promise> { + const oauthUser = await this.introspectToken(dto.token); // Upsert user let user = await this.userRepository.findOne({ - where: { oauthId: payload.sub, provider: payload.provider }, + where: { superOauthId: oauthUser.id }, }); if (!user) { user = this.userRepository.create({ - oauthId: payload.sub, - provider: payload.provider, - username: payload.username, - avatarUrl: payload.avatar_url ?? null, + superOauthId: oauthUser.id, + username: oauthUser.nickname, + email: oauthUser.email, }); } else { - user.username = payload.username; - user.avatarUrl = payload.avatar_url ?? null; + user.username = oauthUser.nickname; + user.email = oauthUser.email; } await this.userRepository.save(user); - // Cookie httpOnly signé — valeur = UUID interne + // Cookie httpOnly — session = UUID interne const isProduction = this.configService.get('NODE_ENV') === 'production'; - res.cookie('session', user.id, { + res.cookie(COOKIE_NAME, user.id, { httpOnly: true, signed: true, secure: isProduction, @@ -74,16 +64,105 @@ export class AuthService { maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours }); - const { oauthId: _, ...safeUser } = user; + // Refresh token cookie si fourni + if (dto.refreshToken) { + res.cookie(REFRESH_COOKIE_NAME, dto.refreshToken, { + httpOnly: true, + signed: true, + secure: isProduction, + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000, // 30 jours + }); + } + + const { superOauthId: _, ...safeUser } = user; return safeUser; } - async getMe(user: User): Promise> { - const { oauthId: _, ...safeUser } = user; + async refreshSession(res: Response, refreshToken: string): Promise<{ success: boolean }> { + // Exchange refresh token for new access token via SuperOAuth + const response = await fetch(`${this.superOauthUrl}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }).toString(), + }); + + if (!response.ok) { + throw new UnauthorizedException('Refresh token invalide ou expiré'); + } + + const data = await response.json(); + if (!data.access_token) { + throw new UnauthorizedException('Refresh échoué — pas de token'); + } + + // Validate the new access token to get user data + const oauthUser = await this.introspectToken(data.access_token); + + const user = await this.userRepository.findOne({ + where: { superOauthId: oauthUser.id }, + }); + + if (!user) { + throw new UnauthorizedException('Utilisateur introuvable après refresh'); + } + + // Set new cookies + const isProduction = this.configService.get('NODE_ENV') === 'production'; + res.cookie(COOKIE_NAME, user.id, { + httpOnly: true, + signed: true, + secure: isProduction, + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + if (data.refresh_token) { + res.cookie(REFRESH_COOKIE_NAME, data.refresh_token, { + httpOnly: true, + signed: true, + secure: isProduction, + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + } + + return { success: true }; + } + + async getMe(user: User): Promise> { + const { superOauthId: _, ...safeUser } = user; return safeUser; } logout(res: Response): void { - res.clearCookie('session'); + res.clearCookie(COOKIE_NAME); + res.clearCookie(REFRESH_COOKIE_NAME); + } + + private async introspectToken(token: string): Promise { + const response = await fetch(`${this.superOauthUrl}/api/v1/auth/token/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }); + + if (!response.ok) { + throw new UnauthorizedException('Token SuperOAuth invalide'); + } + + const data = await response.json(); + if (!data.data?.valid || !data.data.user) { + throw new UnauthorizedException('Token SuperOAuth invalide ou expiré'); + } + + if (!data.data.user.isActive) { + throw new UnauthorizedException('Compte SuperOAuth désactivé'); + } + + return data.data.user as SuperOAuthUser; } } diff --git a/src/auth/dto/set-session.dto.ts b/src/auth/dto/set-session.dto.ts index f5a06f6..7744bd9 100644 --- a/src/auth/dto/set-session.dto.ts +++ b/src/auth/dto/set-session.dto.ts @@ -1,7 +1,11 @@ -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class SetSessionDto { @IsString() @IsNotEmpty() - jwt: string; + token: string; + + @IsString() + @IsOptional() + refreshToken?: string; } diff --git a/src/user/user.entity.ts b/src/user/user.entity.ts index 0a05fd8..e7c7679 100644 --- a/src/user/user.entity.ts +++ b/src/user/user.entity.ts @@ -4,26 +4,21 @@ import { Column, CreateDateColumn, UpdateDateColumn, - Unique, } from 'typeorm'; @Entity('users') -@Unique(['oauthId', 'provider']) export class User { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ name: 'oauth_id', length: 255 }) - oauthId: string; - - @Column({ length: 50 }) - provider: string; + @Column({ name: 'super_oauth_id', length: 255, unique: true }) + superOauthId: string; @Column({ length: 255 }) username: string; - @Column({ name: 'avatar_url', type: 'varchar', length: 500, nullable: true }) - avatarUrl: string | null; + @Column({ type: 'varchar', length: 255, nullable: true }) + email: string | null; @CreateDateColumn({ name: 'created_at' }) createdAt: Date;