diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..04ffa28 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + } + ] +} diff --git a/.gitignore b/.gitignore index 5ef6a52..f390d12 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/src/generated/prisma diff --git a/STRIPE_INTEGRATION.md b/STRIPE_INTEGRATION.md new file mode 100644 index 0000000..d3b5bcf --- /dev/null +++ b/STRIPE_INTEGRATION.md @@ -0,0 +1,88 @@ +# IntĂ©gration Stripe pour Diabetix + +## 📋 Aperçu + +L'intĂ©gration Stripe permet aux utilisateurs de passer du plan FREE au plan PREMIUM (4,99 € / mois) avec gestion automatique des abonnements. + +## đŸ—ïž Architecture + +### ModĂšles Prisma +- **User.stripeId** : ID du client Stripe +- **Subscription** : Table pour tracker l'Ă©tat de l'abonnement + - `stripeId` : ID de l'abonnement + - `status` : État (active, past_due, unpaid, canceled) + - `currentPeriodEnd` : Date d'expiration + +### Routes API + +#### POST `/api/stripe/create-checkout` +CrĂ©e une session de paiement Stripe +- **Auth** : Requis (proxy.ts) +- **Response** : `{ sessionId: string }` +- **Redirection** : Vers Stripe Checkout → `/dashboard?checkout=success` + +#### POST `/api/stripe/webhook` +Reçoit les webhooks Stripe (non protĂ©gĂ©) +- **ÉvĂ©nements gĂ©rĂ©s** : + - `customer.subscription.created` : Nouvel abonnement → plan PREMIUM + - `customer.subscription.updated` : Mise Ă  jour + - `customer.subscription.deleted` : Annulation → plan FREE + - `invoice.payment_failed` : Paiement Ă©chouĂ© + +### Composants + +#### `` +Bouton pour initier le paiement (client-side) +- Charge Stripe.js via CDN +- CrĂ©e une session de checkout +- Redirige vers le formulaire de paiement Stripe + +#### Page `/pricing` +- Affiche les deux plans (FREE et PREMIUM) +- Bouton "Commencer" pour non-connectĂ©s +- `` pour utilisateurs connectĂ©s +- Affiche le statut PREMIUM si applicable + +## 🔧 Configuration + +Voir `STRIPE_SETUP.md` pour les Ă©tapes dĂ©taillĂ©es. + +Variables d'environnement requises : +``` +NEXT_PUBLIC_STRIPE_KEY=pk_test_... +STRIPE_SECRET_KEY=sk_test_... +STRIPE_PRICE_ID=price_... +STRIPE_WEBHOOK_SECRET=whsec_... +``` + +## 🔄 Flux d'achat + +1. **Utilisateur accĂšde `/pricing`** + - S'il est FREE, affiche `` + +2. **Clique sur "Passer Ă  Premium"** + - `UpgradeButton` appelle `POST /api/stripe/create-checkout` + - Reçoit `sessionId` + - Redirige vers Stripe Checkout + +3. **Paiement sur Stripe Checkout** + - SuccĂšs : Redirige vers `/dashboard?checkout=success` + - Annulation : Redirige vers `/pricing` + +4. **Webhook Stripe `customer.subscription.created`** + - CrĂ©e un `Subscription` en DB + - Met Ă  jour `User.plan = "PREMIUM"` + +5. **NextAuth JWT refresh** + - JWT callback lit le nouveau plan + - Prochains appels utilisent `plan: "PREMIUM"` + - ChatBot et features IA activĂ©s + +## 🚀 Prochaines Ă©tapes + +- [ ] Page de confirmation de paiement +- [ ] Email de bienvenue Premium +- [ ] Historique des factures +- [ ] Gestion du renouvellement / annulation +- [ ] Facturation annuelle +- [ ] Tests de webhook locaux avec Stripe CLI diff --git a/STRIPE_SETUP.md b/STRIPE_SETUP.md new file mode 100644 index 0000000..d649121 --- /dev/null +++ b/STRIPE_SETUP.md @@ -0,0 +1,73 @@ +# Configuration Stripe + +## 1. CrĂ©er un compte Stripe + +1. AccĂ©dez Ă  [https://stripe.com](https://stripe.com) +2. CrĂ©ez un compte ou connectez-vous +3. AccĂ©dez au [Dashboard Stripe](https://dashboard.stripe.com) + +## 2. RĂ©cupĂ©rer les clĂ©s API + +1. Dans le sidebar, allez Ă  **Developers** > **API Keys** +2. Copiez les clĂ©s : + - `pk_test_...` → `NEXT_PUBLIC_STRIPE_KEY` + - `sk_test_...` → `STRIPE_SECRET_KEY` + +## 3. CrĂ©er un produit et un plan + +1. Allez Ă  **Products** dans le sidebar +2. Cliquez sur **Create Product** +3. Remplissez les dĂ©tails : + - **Name** : `Diabetix Premium` + - **Description** : `AccĂšs illimitĂ©, analyse IA et coaching` +4. Dans la section **Pricing**, crĂ©ez un plan : + - **Amount** : `4.99` EUR + - **Billing period** : Monthly +5. Copiez l'ID du prix (commence par `price_...`) → `STRIPE_PRICE_ID` + +## 4. Configurer le webhook + +1. Allez Ă  **Developers** > **Webhooks** +2. Cliquez sur **Create Endpoint** +3. Remplissez : + - **Endpoint URL** : `https://votre-domaine.com/api/stripe/webhook` + - **Events** : SĂ©lectionnez : + - `customer.subscription.created` + - `customer.subscription.updated` + - `customer.subscription.deleted` + - `invoice.payment_failed` +4. Copiez le **Signing Secret** → `STRIPE_WEBHOOK_SECRET` + +## 5. Mettre Ă  jour .env.local + +```bash +NEXT_PUBLIC_STRIPE_KEY=pk_test_... +STRIPE_SECRET_KEY=sk_test_... +STRIPE_PRICE_ID=price_... +STRIPE_WEBHOOK_SECRET=whsec_... +``` + +## 6. Tester localement avec Stripe CLI + +```bash +# Installer Stripe CLI +npm install -g @stripe/cli + +# Se connecter +stripe login + +# Faire suivre les webhooks Ă  votre serveur local +stripe listen --forward-to localhost:3000/api/stripe/webhook + +# Simuler un paiement +stripe trigger customer.subscription.created +``` + +## Mode production + +Avant de passer en production : + +1. Utilisez les clĂ©s **Live** au lieu des clĂ©s **Test** +2. Mettez Ă  jour l'URL du webhook vers votre domaine de production +3. Testez le processus complet de paiement +4. Configurez une page de confirmation aprĂšs paiement diff --git a/TESTING_STRIPE.md b/TESTING_STRIPE.md new file mode 100644 index 0000000..de880e7 --- /dev/null +++ b/TESTING_STRIPE.md @@ -0,0 +1,143 @@ +# Guide de test - IntĂ©gration Stripe + +## đŸ§Ș Tester le flux de paiement complet + +### Étape 1 : S'inscrire et se connecter + +1. AccĂ©dez Ă  http://localhost:3000/auth/register +2. CrĂ©ez un compte (ex: `test@example.com`) +3. VĂ©rifiez l'email (rĂ©cupĂ©rez le lien du serveur logs) +4. Connectez-vous + +### Étape 2 : AccĂ©dez Ă  la page pricing + +1. Allez Ă  http://localhost:3000/pricing +2. Vous devriez voir : + - Le plan FREE avec le bouton "Commencer" + - Le plan PREMIUM avec le bouton "Passer Ă  Premium" + +### Étape 3 : Initiez un paiement + +1. Cliquez sur "Passer Ă  Premium" +2. Vous serez redirigĂ© vers Stripe Checkout +3. Utilisez les **cartes de test Stripe** : + +#### Paiements rĂ©ussis : +- **NumĂ©ro** : `4242 4242 4242 4242` +- **Expiration** : Futur (ex: 12/25) +- **CVC** : N'importe quel 3 chiffres +- **Nom** : N'importe quel nom + +#### Paiement Ă©chouĂ© : +- **NumĂ©ro** : `4000 0000 0000 0002` +- MĂȘme autres dĂ©tails + +#### Besoin d'authentification 3D Secure : +- **NumĂ©ro** : `4000 0025 0000 3155` + +### Étape 4 : VĂ©rifiez les webhooks + +AprĂšs un paiement rĂ©ussi, le webhook Stripe devrait : + +1. ✅ CrĂ©er un `Subscription` en base de donnĂ©es +2. ✅ Mettre Ă  jour `User.plan = "PREMIUM"` +3. ✅ Rediriger vers `/dashboard?checkout=success` + +VĂ©rifiez en : +- Ouvrant DevTools → Console +- Cherchant les logs du serveur pour `[webhook]` messages +- AccĂ©dant Ă  `/dashboard/profil` et vĂ©rifiant que le badge "Premium" apparaĂźt + +### Étape 5 : Testez les fonctionnalitĂ©s PREMIUM + +AprĂšs l'upgrade : +1. ✅ Le ChatBot devrait ĂȘtre visible en bas Ă  droite +2. ✅ Les stats 30/90 jours devraient ĂȘtre accessibles +3. ✅ L'analyse IA quotidienne devrait fonctionner + +## 🔧 Debugging + +### VĂ©rifier les variables d'environnement + +```bash +grep STRIPE .env.local +``` + +### VĂ©rifier les logs du serveur + +Cherchez : +- `[create-checkout]` : Logs de crĂ©ation de session +- `[webhook]` : Logs de webhooks Stripe + +### Simuler des webhooks en local + +Pour tester les webhooks sans vraiment payer, tĂ©lĂ©charge **Stripe CLI** : + +```bash +# Windows : TĂ©lĂ©charge depuis https://stripe.com/docs/stripe-cli +# macOS : brew install stripe/stripe-cli/stripe +# Linux : Voir https://stripe.com/docs/stripe-cli + +# Se connecter avec ton compte Stripe +stripe login + +# Rediriger les webhooks Ă  ton serveur local +stripe listen --forward-to localhost:3000/api/stripe/webhook + +# Dans un autre terminal, simuler un paiement +stripe trigger customer.subscription.created +``` + +### VĂ©rifier la base de donnĂ©es + +```bash +sqlite3 prisma/dev.db +.tables +SELECT * FROM "Subscription" LIMIT 1; +SELECT plan FROM "User" WHERE email = 'test@example.com'; +``` + +## 🐛 ProblĂšmes courants + +### Le bouton "Passer Ă  Premium" ne fait rien +- VĂ©rifiez que `NEXT_PUBLIC_STRIPE_KEY` est correct (commence par `pk_`) +- VĂ©rifiez les erreurs dans DevTools → Console + +### Stripe Checkout ne se charge pas +- VĂ©rifiez que la clĂ© publique est valide +- VĂ©rifiez que la `STRIPE_PRICE_ID` existe dans votre compte Stripe + +### Le webhook ne met pas Ă  jour le plan +- VĂ©rifiez que `STRIPE_WEBHOOK_SECRET` est correct +- VĂ©rifiez les logs serveur pour les erreurs de signature + +### L'utilisateur reste FREE aprĂšs paiement +- VĂ©rifiez que le webhook a Ă©tĂ© traitĂ© (cherchez `[webhook]` dans les logs) +- VĂ©rifiez que l'utilisateur a un `userId` correct +- VĂ©rifiez les logs de base de donnĂ©es + +## 📊 État aprĂšs un paiement rĂ©ussi + +La base de donnĂ©es devrait contenir : + +``` +User: + id: "cuid-123" + plan: "PREMIUM" + stripeId: "cus_abc123" + +Subscription: + id: "sub-123" + userId: "cuid-123" + stripeId: "sub_abc123" + status: "active" + currentPeriodEnd: 2026-05-26 (30 jours Ă  partir d'aujourd'hui) +``` + +## 🚀 Prochaines Ă©tapes + +- [ ] Ajouter une page de confirmation de paiement +- [ ] Envoyer un email de bienvenue Premium +- [ ] Afficher l'historique des factures +- [ ] Permettre l'annulation d'abonnement +- [ ] Tester en production avec les clĂ©s Live diff --git a/add_readings.js b/add_readings.js new file mode 100644 index 0000000..566045c --- /dev/null +++ b/add_readings.js @@ -0,0 +1,43 @@ +const { PrismaClient } = require("@prisma/client"); +const prisma = new PrismaClient(); + +async function main() { + const user = await prisma.user.findUnique({ + where: { email: "testuser@example.com" } + }); + + if (!user) { + console.log("User not found"); + process.exit(1); + } + + // Generate sample readings for the past 12 months + const readings = []; + const today = new Date(); + + for (let monthsAgo = 0; monthsAgo < 12; monthsAgo++) { + const monthDate = new Date(today.getFullYear(), today.getMonth() - monthsAgo, 1); + const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate(); + + for (let day = 1; day <= daysInMonth; day += 2) { // Every 2 days + for (let time = 0; time < 3; time++) { // 3 times per day + readings.push({ + userId: user.id, + value: Math.floor(Math.random() * 150) + 60, // 60-210 mg/dL + measuredAt: new Date(monthDate.getFullYear(), monthDate.getMonth(), day, 6 + time * 8, 0), + notes: time === 0 ? "Avant petit-dĂ©jeuner" : time === 1 ? "Avant dĂ©jeuner" : "Avant dĂźner" + }); + } + } + } + + await prisma.reading.createMany({ + data: readings, + skipDuplicates: true + }); + + console.log(`Created ${readings.length} readings`); + await prisma.$disconnect(); +} + +main().catch(console.error); diff --git a/dev.db b/dev.db new file mode 100644 index 0000000..4a4e226 Binary files /dev/null and b/dev.db differ diff --git a/package-lock.json b/package-lock.json index 23252b8..a5d75ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,40 @@ "name": "diabetix", "version": "0.1.0", "dependencies": { + "@auth/prisma-adapter": "^2.11.2", + "@google/generative-ai": "^0.24.1", + "@prisma/adapter-better-sqlite3": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.9.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^1.11.0", "next": "16.2.4", + "next-auth": "^5.0.0-beta.31", + "pdf-lib": "^1.17.1", + "pdfkit": "^0.18.0", + "prisma": "^7.8.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "recharts": "^3.8.1", + "resend": "^6.12.2", + "stripe": "^22.1.0", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", + "@types/pdfkit": "^0.17.6", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.4.2", "eslint": "^9", "eslint-config-next": "16.2.4", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } }, @@ -36,6 +58,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.2.tgz", + "integrity": "sha512-GyNEUNtrPgDPs0M4xX6F5i7jTsCKwU6BXV9zutctcoo6K1Ud+juckrmQS11uyNgeWsw6sliextHbU/e+8lsizQ==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -276,6 +339,33 @@ "node": ">=6.9.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -309,6 +399,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "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", @@ -453,6 +985,27 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1035,6 +1588,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1192,6 +1751,30 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1240,6 +1823,426 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@prisma/adapter-better-sqlite3": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-better-sqlite3/-/adapter-better-sqlite3-7.8.0.tgz", + "integrity": "sha512-9p0HvQNaRoOaUForDcp1MPIjylmCamYQjQpEogpdesLr14g3NwAsYR3bHL9wFi8tn21TPDVQPzcZi+SxXEokSA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "better-sqlite3": "^12.6.0" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1247,6 +2250,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1538,6 +2559,76 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1563,17 +2654,26 @@ "version": "20.19.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/pdfkit": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", + "integrity": "sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "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" @@ -1583,12 +2683,18 @@ "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, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", @@ -2419,6 +3525,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz", @@ -2446,6 +3561,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.22", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz", @@ -2458,6 +3593,55 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.2.tgz", + "integrity": "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -2482,6 +3666,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -2516,6 +3718,58 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2613,12 +3867,75 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2646,6 +3963,12 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "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", @@ -2657,7 +3980,6 @@ "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", @@ -2672,9 +3994,129 @@ "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/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2736,6 +4178,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2754,6 +4206,36 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2761,6 +4243,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2797,16 +4288,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "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==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2820,6 +4337,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2835,6 +4364,16 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.344", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", @@ -2849,6 +4388,24 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -2863,6 +4420,18 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3040,6 +4609,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz", + "integrity": "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3469,11 +5090,53 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.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-glob": { @@ -3520,6 +5183,28 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3543,6 +5228,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3594,6 +5285,23 @@ "dev": true, "license": "ISC" }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3610,6 +5318,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3651,6 +5396,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -3696,6 +5450,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3741,6 +5501,21 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3801,9 +5576,20 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3915,6 +5701,57 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hono": { + "version": "4.12.15", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", + "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3925,6 +5762,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3952,6 +5799,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3967,6 +5826,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4237,6 +6105,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4393,7 +6267,6 @@ "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/iterator.prototype": { @@ -4418,12 +6291,26 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4812,6 +6699,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4835,6 +6741,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4858,6 +6770,30 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", + "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4902,6 +6838,18 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4919,12 +6867,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4932,6 +6885,38 @@ "dev": true, "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4950,6 +6935,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5026,6 +7017,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz", + "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5054,6 +7072,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -5080,6 +7122,15 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5203,6 +7254,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5271,6 +7337,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5298,7 +7370,6 @@ "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" @@ -5311,6 +7382,50 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/pdfkit": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.18.0.tgz", + "integrity": "sha512-NvUwSDZ0eYEzqAiWwVQkRkjYUkZ48kcsHuCO31ykqPPIVkwoSDjDGiwIgHHNtsiwls3z3P/zy4q00hl2chg2Ug==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "fontkit": "^2.0.4", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5330,6 +7445,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/png-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz", + "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5340,6 +7474,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", @@ -5369,6 +7509,65 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5379,6 +7578,39 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5391,6 +7623,33 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5401,6 +7660,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5422,6 +7697,40 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5447,9 +7756,103 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5494,6 +7897,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resend": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.2.tgz", + "integrity": "sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "svix": "1.90.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -5538,6 +7986,21 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5593,6 +8056,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5628,6 +8111,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5644,6 +8133,11 @@ "semver": "bin/semver.js" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5755,7 +8249,6 @@ "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" @@ -5768,7 +8261,6 @@ "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" @@ -5850,6 +8342,63 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5859,6 +8408,15 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5866,6 +8424,22 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5880,6 +8454,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6016,6 +8599,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.1.0.tgz", + "integrity": "sha512-w/xHyJGxXWnLPbNHG13sz/fae0MrFGC80Oz7YbICQymbfpqfEcsoG+6yG+9BWb81PWc4rrkeSO4wmTcmefmbLw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -6065,6 +8665,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svix": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.90.0.tgz", + "integrity": "sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -6086,6 +8706,46 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6192,6 +8852,38 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6287,7 +8979,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6344,7 +9036,33 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6423,11 +9141,74 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "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" @@ -6538,6 +9319,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6558,6 +9345,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 96b3ba8..4844b46 100644 --- a/package.json +++ b/package.json @@ -6,21 +6,45 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "db:seed": "tsx prisma/seed.ts", + "db:clear": "tsx prisma/clear.ts" }, "dependencies": { + "@auth/prisma-adapter": "^2.11.2", + "@google/generative-ai": "^0.24.1", + "@prisma/adapter-better-sqlite3": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.9.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^1.11.0", "next": "16.2.4", + "next-auth": "^5.0.0-beta.31", + "pdf-lib": "^1.17.1", + "pdfkit": "^0.18.0", + "prisma": "^7.8.0", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "recharts": "^3.8.1", + "resend": "^6.12.2", + "stripe": "^22.1.0", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", + "@types/pdfkit": "^0.17.6", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.4.2", "eslint": "^9", "eslint-config-next": "16.2.4", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } } diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..5c632b7 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,16 @@ +import "dotenv/config"; +import path from "node:path"; +import { defineConfig } from "prisma/config"; + +const databaseUrl = + process.env["DATABASE_URL"] ?? `file:${path.resolve("prisma/dev.db")}`; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: databaseUrl, + }, +}); diff --git a/prisma/clear.ts b/prisma/clear.ts new file mode 100644 index 0000000..9d98e99 --- /dev/null +++ b/prisma/clear.ts @@ -0,0 +1,18 @@ +import path from "node:path"; +import { PrismaClient } from "../src/generated/prisma/client"; +import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; +import "dotenv/config"; + +const databaseUrl = + process.env.DATABASE_URL ?? `file:${path.resolve("prisma/dev.db")}`; + +const prisma = new PrismaClient({ + adapter: new PrismaBetterSqlite3({ url: databaseUrl }), +}); + +async function main() { + const { count } = await prisma.reading.deleteMany(); + console.log(`✓ ${count} relevĂ©${count > 1 ? "s" : ""} supprimĂ©${count > 1 ? "s" : ""}.`); +} + +main().finally(() => prisma.$disconnect()); diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000..cdec42c Binary files /dev/null and b/prisma/dev.db differ diff --git a/prisma/migrations/20260426084121_init/migration.sql b/prisma/migrations/20260426084121_init/migration.sql new file mode 100644 index 0000000..6b7900b --- /dev/null +++ b/prisma/migrations/20260426084121_init/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "Reading" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "measuredAt" DATETIME NOT NULL, + "moment" TEXT NOT NULL, + "value" REAL NOT NULL, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE INDEX "Reading_measuredAt_idx" ON "Reading"("measuredAt"); diff --git a/prisma/migrations/20260426095223_add_patient/migration.sql b/prisma/migrations/20260426095223_add_patient/migration.sql new file mode 100644 index 0000000..7f217ef --- /dev/null +++ b/prisma/migrations/20260426095223_add_patient/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "Patient" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT, + "birthDate" DATETIME, + "heightCm" INTEGER, + "weightKg" REAL, + "updatedAt" DATETIME NOT NULL +); diff --git a/prisma/migrations/20260426095923_add_patient_medical_fields/migration.sql b/prisma/migrations/20260426095923_add_patient_medical_fields/migration.sql new file mode 100644 index 0000000..506fecb --- /dev/null +++ b/prisma/migrations/20260426095923_add_patient_medical_fields/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Patient" ADD COLUMN "diabetesType" TEXT; +ALTER TABLE "Patient" ADD COLUMN "sex" TEXT; +ALTER TABLE "Patient" ADD COLUMN "treatment" TEXT; diff --git a/prisma/migrations/20260426103214_add_daily_analysis/migration.sql b/prisma/migrations/20260426103214_add_daily_analysis/migration.sql new file mode 100644 index 0000000..91620da --- /dev/null +++ b/prisma/migrations/20260426103214_add_daily_analysis/migration.sql @@ -0,0 +1,6 @@ +-- CreateTable +CREATE TABLE "DailyAnalysis" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1, + "content" TEXT NOT NULL, + "generatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/prisma/migrations/20260426142759_saas_multi_tenant/migration.sql b/prisma/migrations/20260426142759_saas_multi_tenant/migration.sql new file mode 100644 index 0000000..e50b48a --- /dev/null +++ b/prisma/migrations/20260426142759_saas_multi_tenant/migration.sql @@ -0,0 +1,87 @@ +/* + Warnings: + + - The primary key for the `DailyAnalysis` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `Patient` table will be changed. If it partially fails, the table could be left without primary key constraint. + - Added the required column `userId` to the `DailyAnalysis` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `Patient` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `Reading` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "emailVerified" DATETIME, + "name" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "plan" TEXT NOT NULL DEFAULT 'FREE', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "VerifyToken" ( + "id" TEXT NOT NULL PRIMARY KEY, + "token" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "VerifyToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_DailyAnalysis" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "generatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DailyAnalysis_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_DailyAnalysis" ("content", "generatedAt", "id") SELECT "content", "generatedAt", "id" FROM "DailyAnalysis"; +DROP TABLE "DailyAnalysis"; +ALTER TABLE "new_DailyAnalysis" RENAME TO "DailyAnalysis"; +CREATE UNIQUE INDEX "DailyAnalysis_userId_key" ON "DailyAnalysis"("userId"); +CREATE TABLE "new_Patient" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT, + "birthDate" DATETIME, + "heightCm" INTEGER, + "weightKg" REAL, + "sex" TEXT, + "diabetesType" TEXT, + "treatment" TEXT, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Patient_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Patient" ("birthDate", "diabetesType", "email", "firstName", "heightCm", "id", "lastName", "sex", "treatment", "updatedAt", "weightKg") SELECT "birthDate", "diabetesType", "email", "firstName", "heightCm", "id", "lastName", "sex", "treatment", "updatedAt", "weightKg" FROM "Patient"; +DROP TABLE "Patient"; +ALTER TABLE "new_Patient" RENAME TO "Patient"; +CREATE UNIQUE INDEX "Patient_userId_key" ON "Patient"("userId"); +CREATE TABLE "new_Reading" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "userId" TEXT NOT NULL, + "measuredAt" DATETIME NOT NULL, + "moment" TEXT NOT NULL, + "value" REAL NOT NULL, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Reading_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Reading" ("createdAt", "id", "measuredAt", "moment", "notes", "value") SELECT "createdAt", "id", "measuredAt", "moment", "notes", "value" FROM "Reading"; +DROP TABLE "Reading"; +ALTER TABLE "new_Reading" RENAME TO "Reading"; +CREATE INDEX "Reading_userId_measuredAt_idx" ON "Reading"("userId", "measuredAt"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerifyToken_token_key" ON "VerifyToken"("token"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..f9779ca --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,90 @@ +generator client { + provider = "prisma-client-js" + output = "../src/generated/prisma" +} + +datasource db { + provider = "sqlite" +} + +// ─── Auth ──────────────────────────────────────────────────────────────────── + +model User { + id String @id @default(cuid()) + email String @unique + emailVerified DateTime? + name String + passwordHash String + plan String @default("FREE") // FREE | PREMIUM + stripeId String? @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + readings Reading[] + patient Patient? + dailyAnalysis DailyAnalysis? + verifyTokens VerifyToken[] + subscription Subscription? +} + +model VerifyToken { + id String @id @default(cuid()) + token String @unique + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + expiresAt DateTime + createdAt DateTime @default(now()) +} + +// ─── Domain ────────────────────────────────────────────────────────────────── + +model Reading { + id Int @id @default(autoincrement()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + measuredAt DateTime + moment String + value Float + notes String? + createdAt DateTime @default(now()) + + @@index([userId, measuredAt]) +} + +model Patient { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + firstName String + lastName String + email String? + birthDate DateTime? + heightCm Int? + weightKg Float? + sex String? + diabetesType String? + treatment String? + updatedAt DateTime @updatedAt +} + +model DailyAnalysis { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + content String + generatedAt DateTime @default(now()) +} + +model Subscription { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + stripeId String @unique + stripePriceId String + stripeCustomerId String + status String // active | past_due | canceled | unpaid + currentPeriodEnd DateTime + canceledAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..99bcd8a --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,51 @@ +import path from "node:path"; +import { PrismaClient } from "../src/generated/prisma/client"; +import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; +import "dotenv/config"; + +const databaseUrl = + process.env.DATABASE_URL ?? `file:${path.resolve("prisma/dev.db")}`; + +const prisma = new PrismaClient({ + adapter: new PrismaBetterSqlite3({ url: databaseUrl }), +}); + +const moments = [ + { key: "FASTING", hour: 7, baseMin: 0.85, baseMax: 1.25 }, + { key: "LUNCH", hour: 14, baseMin: 1.05, baseMax: 1.75 }, + { key: "DINNER", hour: 21, baseMin: 1.0, baseMax: 1.7 }, +] as const; + +function rand(min: number, max: number) { + return min + Math.random() * (max - min); +} + +async function main() { + await prisma.reading.deleteMany(); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const data: { measuredAt: Date; moment: string; value: number; notes: string | null }[] = []; + + for (let day = 29; day >= 0; day--) { + for (const m of moments) { + const d = new Date(today.getTime() - day * 86_400_000); + d.setHours(m.hour, Math.floor(Math.random() * 30), 0, 0); + let value = rand(m.baseMin, m.baseMax); + // Quelques valeurs aberrantes pour montrer hypo / hyper + if (Math.random() < 0.05) value = rand(0.5, 0.68); + if (Math.random() < 0.05) value = rand(1.85, 2.4); + data.push({ + measuredAt: d, + moment: m.key, + value: Math.round(value * 100) / 100, + notes: Math.random() < 0.15 ? "Bonne forme" : null, + }); + } + } + + await prisma.reading.createMany({ data }); + console.log(`✓ ${data.length} relevĂ©s gĂ©nĂ©rĂ©s.`); +} + +main().finally(() => prisma.$disconnect()); diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..27b0f7c --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..c9f1379 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Diabetix — Suivi de glycĂ©mie", + "short_name": "Diabetix", + "description": "Suivi quotidien du diabĂšte avec tableau de bord et tendances.", + "start_url": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#f8fafc", + "theme_color": "#0f766e", + "lang": "fr", + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..6014028 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,52 @@ +const CACHE = "diabetix-v1"; +const APP_SHELL = ["/", "/saisie", "/historique", "/manifest.webmanifest", "/icon.svg"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE).then((cache) => cache.addAll(APP_SHELL)).catch(() => {}) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + if (request.method !== "GET") return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + // Network-first for HTML and API, cache fallback + if (request.mode === "navigate" || url.pathname.startsWith("/api/")) { + event.respondWith( + fetch(request) + .then((res) => { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put(request, copy)).catch(() => {}); + return res; + }) + .catch(() => caches.match(request).then((m) => m || caches.match("/"))) + ); + return; + } + + // Cache-first for static assets + event.respondWith( + caches.match(request).then((cached) => { + if (cached) return cached; + return fetch(request).then((res) => { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put(request, copy)).catch(() => {}); + return res; + }); + }) + ); +}); diff --git a/reset-db.mjs b/reset-db.mjs new file mode 100644 index 0000000..04ece7e --- /dev/null +++ b/reset-db.mjs @@ -0,0 +1,28 @@ +import { PrismaClient } from "@prisma/client"; +import fs from "fs"; +import path from "path"; + +const dbPath = "./prisma/dev.db"; +if (fs.existsSync(dbPath)) { + fs.unlinkSync(dbPath); + console.log("Database deleted"); +} + +const prisma = new PrismaClient(); + +await prisma.$executeRawUnsafe(` + CREATE TABLE "_prisma_migrations" ( + "id" TEXT NOT NULL PRIMARY KEY, + "checksum" TEXT NOT NULL, + "finished_at" DATETIME, + "execution_time" INTEGER NOT NULL, + "migration_name" TEXT NOT NULL, + "logs" TEXT, + "rolled_back_at" DATETIME, + "started_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "applied_steps_count" INTEGER NOT NULL DEFAULT 0 + ) +`); + +await prisma.$disconnect(); +console.log("Database reset complete"); diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..73228a0 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/lib/auth"; +export const { GET, POST } = handlers; diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..db982cb --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import crypto from "node:crypto"; +import { prisma } from "@/lib/prisma"; +import { sendVerificationEmail } from "@/lib/email"; + +export async function POST(request: Request) { + const body = await request.json().catch(() => null); + if (!body) return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + + const name = typeof body.name === "string" ? body.name.trim() : ""; + const email = typeof body.email === "string" ? body.email.toLowerCase().trim() : ""; + const password = typeof body.password === "string" ? body.password : ""; + + if (!name || name.length < 2) { + return NextResponse.json({ error: "Nom trop court (2 car. min)" }, { status: 400 }); + } + if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + return NextResponse.json({ error: "Email invalide" }, { status: 400 }); + } + if (password.length < 8) { + return NextResponse.json({ error: "Mot de passe trop court (8 car. min)" }, { status: 400 }); + } + + const existing = await prisma.user.findUnique({ where: { email } }); + if (existing) { + return NextResponse.json({ error: "Cet email est dĂ©jĂ  utilisĂ©" }, { status: 409 }); + } + + const passwordHash = await bcrypt.hash(password, 12); + + // Split "PrĂ©nom Nom" — first word = prĂ©nom, rest = nom + const parts = name.split(" ").filter(Boolean); + const firstName = parts[0] ?? name; + const lastName = parts.slice(1).join(" ") || firstName; + + const user = await prisma.user.create({ + data: { + name, + email, + passwordHash, + plan: "FREE", + patient: { + create: { firstName, lastName, email }, + }, + }, + }); + + // Create verification token (24h) + const token = crypto.randomBytes(32).toString("hex"); + await prisma.verifyToken.create({ + data: { + token, + userId: user.id, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + }); + + try { + await sendVerificationEmail(email, name, token); + } catch (e) { + console.error("[register] email send failed:", e); + return NextResponse.json( + { ok: true, emailError: e instanceof Error ? e.message : "Erreur envoi email" }, + { status: 201 } + ); + } + + return NextResponse.json({ ok: true }, { status: 201 }); +} diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts new file mode 100644 index 0000000..712dd63 --- /dev/null +++ b/src/app/api/auth/verify/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const token = searchParams.get("token"); + + if (!token) { + return NextResponse.redirect(new URL("/auth/login?error=token_missing", request.url)); + } + + const record = await prisma.verifyToken.findUnique({ where: { token } }); + + if (!record || record.expiresAt < new Date()) { + await prisma.verifyToken.deleteMany({ where: { token } }).catch(() => {}); + return NextResponse.redirect(new URL("/auth/login?error=token_expired", request.url)); + } + + await prisma.user.update({ + where: { id: record.userId }, + data: { emailVerified: new Date() }, + }); + await prisma.verifyToken.delete({ where: { token } }); + + return NextResponse.redirect(new URL("/auth/login?verified=1", request.url)); +} diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts new file mode 100644 index 0000000..51a28cc --- /dev/null +++ b/src/app/api/chat/route.ts @@ -0,0 +1,207 @@ +import { NextResponse } from "next/server"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { estimateHbA1c, statusFor, type Moment } from "@/lib/glycemia"; +import { DIABETES_TYPE_LABELS, SEX_LABELS } from "@/lib/patient"; +import { differenceInYears, format } from "date-fns"; +import { fr } from "date-fns/locale"; + +const MAX_MESSAGES = 20; + +type ChatMessage = { role: "user" | "model"; content: string }; + +function buildSystemPrompt(ctx: { + patient: { + firstName: string; + lastName: string; + sex: string | null; + birthDate: Date | null; + heightCm: number | null; + weightKg: number | null; + diabetesType: string | null; + treatment: string | null; + } | null; + stats: { + count7: number; + avg7: number | null; + count30: number; + avg30: number | null; + inRangePct30: number | null; + hypo30: number; + hyper30: number; + hba1c: number | null; + lastValue: number | null; + lastMoment: string | null; + lastDate: Date | null; + }; +}): string { + const { patient, stats } = ctx; + + const patientSection = patient + ? (() => { + const age = patient.birthDate + ? differenceInYears(new Date(), patient.birthDate) + : null; + const bmi = + patient.heightCm && patient.weightKg + ? patient.weightKg / Math.pow(patient.heightCm / 100, 2) + : null; + return ` +## Fiche patient +- Nom : ${patient.firstName} ${patient.lastName} +- Sexe : ${patient.sex ? (SEX_LABELS[patient.sex] ?? patient.sex) : "non renseignĂ©"} +- Âge : ${age !== null ? `${age} ans` : "non renseignĂ©"} +- Taille : ${patient.heightCm ? `${patient.heightCm} cm` : "non renseignĂ©e"} +- Poids : ${patient.weightKg ? `${patient.weightKg} kg` : "non renseignĂ©"} +- IMC : ${bmi ? `${bmi.toFixed(1)}` : "non calculable"} +- Type de diabĂšte : ${patient.diabetesType ? (DIABETES_TYPE_LABELS[patient.diabetesType] ?? patient.diabetesType) : "non renseignĂ©"} +- Traitement en cours : ${patient.treatment ?? "non renseignĂ©"}`; + })() + : "## Fiche patient\nAucune fiche patient renseignĂ©e."; + + const statsSection = ` +## DonnĂ©es glycĂ©miques rĂ©centes +- DerniĂšre mesure : ${stats.lastValue !== null ? `${stats.lastValue.toFixed(2).replace(".", ",")} g/L (${stats.lastMoment ?? ""})${stats.lastDate ? ` le ${format(stats.lastDate, "d MMMM yyyy", { locale: fr })}` : ""}` : "aucune"} +- Moyenne sur 7 jours : ${stats.avg7 !== null ? `${stats.avg7.toFixed(2).replace(".", ",")} g/L (${stats.count7} relevĂ©s)` : "aucune donnĂ©e"} +- Moyenne sur 30 jours : ${stats.avg30 !== null ? `${stats.avg30.toFixed(2).replace(".", ",")} g/L (${stats.count30} relevĂ©s)` : "aucune donnĂ©e"} +- % dans la cible sur 30 jours : ${stats.inRangePct30 !== null ? `${stats.inRangePct30.toFixed(0)} %` : "aucune donnĂ©e"} +- Épisodes d'hypoglycĂ©mie (30j) : ${stats.hypo30} +- Épisodes d'hyperglycĂ©mie (30j) : ${stats.hyper30} +- HbA1c estimĂ©e (indicatif) : ${stats.hba1c !== null ? `${stats.hba1c.toFixed(1)} %` : "aucune donnĂ©e"}`; + + return `Tu es Diablo, un coach diabĂšte bienveillant et pĂ©dagogue intĂ©grĂ© Ă  l'application Diabetix. Tu aides les patients Ă  comprendre leurs donnĂ©es glycĂ©miques et Ă  adopter de bonnes habitudes. + +## Ton rĂŽle +- Analyser les relevĂ©s glycĂ©miques et donner des explications claires et encourageantes +- Donner des conseils pratiques sur l'alimentation, l'activitĂ© physique, le rythme des prises de mesures +- Expliquer les tendances : pourquoi une glycĂ©mie est Ă©levĂ©e aprĂšs un repas, l'impact du stress ou de l'exercice +- Encourager le patient sans le culpabiliser +- Poser des questions pour mieux comprendre le contexte (repas, activitĂ©, stress, sommeil) + +## Tes limites (IMPORTANT) +- Tu n'es PAS un mĂ©decin et tu ne te substitues pas Ă  l'Ă©quipe soignante +- Tu ne modifies JAMAIS un traitement mĂ©dical, ne recommandes jamais d'ajuster les doses d'insuline ou de mĂ©dicaments +- En cas d'urgence (hypoglycĂ©mie sĂ©vĂšre, malaise), tu diriges immĂ©diatement vers les secours (15, 112) ou le mĂ©decin +- Tu rappelles toujours de consulter le mĂ©decin pour toute dĂ©cision mĂ©dicale +- Tu gardes un ton positif, jamais alarmiste sauf urgence rĂ©elle + +## Format de tes rĂ©ponses +- RĂ©ponds en français, de façon concise et chaleureuse +- Utilise des emojis avec modĂ©ration pour rendre la lecture agrĂ©able +- Structure avec des listes Ă  points si la rĂ©ponse est longue +- Commence souvent par reconnaĂźtre ce que le patient vit/ressent + +${patientSection} +${statsSection} + +Utilise ces donnĂ©es pour personnaliser tes conseils. Si une information manque, pose la question.`; +} + +export async function POST(request: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + const userId = session.user.id; + + const plan = (session.user as { plan?: string }).plan ?? "FREE"; + if (plan !== "PREMIUM") { + return NextResponse.json( + { error: "Le chatbot Diablo est rĂ©servĂ© aux membres Premium. Passez Ă  Premium pour en bĂ©nĂ©ficier." }, + { status: 403 } + ); + } + + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey || apiKey === "your_gemini_api_key_here") { + return NextResponse.json( + { error: "ClĂ© API Gemini non configurĂ©e. Ajoutez GEMINI_API_KEY dans .env.local." }, + { status: 503 } + ); + } + + const body = await request.json().catch(() => null); + if (!body?.message || typeof body.message !== "string") { + return NextResponse.json({ error: "Message requis" }, { status: 400 }); + } + const userMessage = body.message.trim().slice(0, 2000); + const history: ChatMessage[] = Array.isArray(body.history) ? body.history.slice(-MAX_MESSAGES) : []; + + const now = new Date(); + const ago7 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const ago30 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + const [patient, last, readings7, readings30] = await Promise.all([ + prisma.patient.findUnique({ where: { userId } }), + prisma.reading.findFirst({ where: { userId }, orderBy: { measuredAt: "desc" } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago7 } } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago30 } } }), + ]); + + const avg7 = readings7.length ? readings7.reduce((s, r) => s + r.value, 0) / readings7.length : null; + const avg30 = readings30.length ? readings30.reduce((s, r) => s + r.value, 0) / readings30.length : null; + + let inRange30 = 0, hypo30 = 0, hyper30 = 0; + for (const r of readings30) { + const s = statusFor(r.value, r.moment as Moment); + if (s === "in-range") inRange30++; + else if (s === "hypo") hypo30++; + else hyper30++; + } + + const systemPrompt = buildSystemPrompt({ + patient, + stats: { + count7: readings7.length, + avg7, + count30: readings30.length, + avg30, + inRangePct30: readings30.length ? (inRange30 / readings30.length) * 100 : null, + hypo30, + hyper30, + hba1c: avg30 ? estimateHbA1c(avg30) : null, + lastValue: last?.value ?? null, + lastMoment: last?.moment ?? null, + lastDate: last ? new Date(last.measuredAt) : null, + }, + }); + + const genAI = new GoogleGenerativeAI(apiKey); + const modelName = process.env.GEMINI_MODEL ?? "gemini-2.5-flash"; + const model = genAI.getGenerativeModel({ model: modelName, systemInstruction: systemPrompt }); + + const geminiHistory = history.map((m) => ({ role: m.role, parts: [{ text: m.content }] })); + const chat = model.startChat({ history: geminiHistory }); + + const stream = new ReadableStream({ + async start(controller) { + try { + const result = await chat.sendMessageStream(userMessage); + for await (const chunk of result.stream) { + const text = chunk.text(); + if (text) controller.enqueue(new TextEncoder().encode(text)); + } + } catch (err) { + const raw = err instanceof Error ? err.message : "Erreur Gemini"; + let friendly = raw; + if (raw.includes("429") || raw.includes("quota") || raw.includes("Quota")) { + friendly = "Quota Gemini dĂ©passĂ©. VĂ©rifiez votre plan sur https://ai.dev/rate-limit ou attendez quelques minutes."; + } else if (raw.includes("403") || raw.includes("API_KEY")) { + friendly = "ClĂ© API Gemini invalide. VĂ©rifiez GEMINI_API_KEY dans .env.local."; + } else if (raw.includes("404")) { + friendly = "ModĂšle Gemini introuvable. VĂ©rifiez GEMINI_MODEL dans .env.local."; + } + controller.enqueue(new TextEncoder().encode(`⚠ ${friendly}`)); + } finally { + controller.close(); + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "X-Content-Type-Options": "nosniff", + "Cache-Control": "no-cache", + }, + }); +} diff --git a/src/app/api/daily-analysis/route.ts b/src/app/api/daily-analysis/route.ts new file mode 100644 index 0000000..1121009 --- /dev/null +++ b/src/app/api/daily-analysis/route.ts @@ -0,0 +1,130 @@ +import { NextResponse } from "next/server"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { estimateHbA1c, statusFor, type Moment } from "@/lib/glycemia"; +import { DIABETES_TYPE_LABELS, SEX_LABELS } from "@/lib/patient"; +import { differenceInYears, isSameDay } from "date-fns"; + +async function generateAnalysis(userId: string): Promise { + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey || apiKey === "your_gemini_api_key_here") { + throw new Error("ClĂ© API Gemini non configurĂ©e."); + } + + const now = new Date(); + const ago7 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const ago30 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + const [patient, readings7, readings30, lastReading] = await Promise.all([ + prisma.patient.findUnique({ where: { userId } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago7 } } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago30 } } }), + prisma.reading.findFirst({ where: { userId }, orderBy: { measuredAt: "desc" } }), + ]); + + const avg7 = readings7.length + ? readings7.reduce((s, r) => s + r.value, 0) / readings7.length + : null; + const avg30 = readings30.length + ? readings30.reduce((s, r) => s + r.value, 0) / readings30.length + : null; + + let inRange30 = 0, hypo30 = 0, hyper30 = 0; + for (const r of readings30) { + const s = statusFor(r.value, r.moment as Moment); + if (s === "in-range") inRange30++; + else if (s === "hypo") hypo30++; + else hyper30++; + } + const inRangePct = readings30.length ? (inRange30 / readings30.length) * 100 : null; + const hba1c = avg30 ? estimateHbA1c(avg30) : null; + + const age = patient?.birthDate ? differenceInYears(now, patient.birthDate) : null; + const bmi = + patient?.heightCm && patient?.weightKg + ? patient.weightKg / Math.pow(patient.heightCm / 100, 2) + : null; + + const prompt = `Tu es Diablo, coach diabĂšte bienveillant. RĂ©dige un court paragraphe d'analyse quotidienne (4 Ă  6 phrases max) pour ce patient. + +DonnĂ©es patient : +- PrĂ©nom : ${patient?.firstName ?? "le patient"} +- Âge : ${age ? `${age} ans` : "non renseignĂ©"} +- Type diabĂšte : ${patient?.diabetesType ? (DIABETES_TYPE_LABELS[patient.diabetesType] ?? patient.diabetesType) : "non renseignĂ©"} +- Traitement : ${patient?.treatment ?? "non renseignĂ©"} +- IMC : ${bmi ? bmi.toFixed(1) : "non calculable"} +- Sexe : ${patient?.sex ? (SEX_LABELS[patient.sex] ?? patient.sex) : "non renseignĂ©"} + +DonnĂ©es glycĂ©miques : +- DerniĂšre mesure : ${lastReading ? `${lastReading.value.toFixed(2)} g/L (${lastReading.moment})` : "aucune"} +- Moyenne 7 jours : ${avg7 ? `${avg7.toFixed(2)} g/L (${readings7.length} relevĂ©s)` : "aucune donnĂ©e"} +- Moyenne 30 jours : ${avg30 ? `${avg30.toFixed(2)} g/L (${readings30.length} relevĂ©s)` : "aucune donnĂ©e"} +- % dans la cible (30j) : ${inRangePct ? `${inRangePct.toFixed(0)} %` : "aucune donnĂ©e"} +- HypoglycĂ©mies (30j) : ${hypo30} +- HyperglycĂ©mies (30j) : ${hyper30} +- HbA1c estimĂ©e : ${hba1c ? `${hba1c.toFixed(1)} %` : "aucune donnĂ©e"} + +Consignes : +- Commence par une observation concrĂšte sur les chiffres de la semaine +- Identifie une tendance positive et une piste d'amĂ©lioration si pertinent +- Termine par une phrase d'encouragement personnalisĂ©e +- Ton chaleureux, positif, jamais alarmiste +- Ne propose jamais de modifier un traitement mĂ©dical +- RĂ©ponds en français, sans titre, sans liste — juste un paragraphe fluide +- 4 Ă  6 phrases maximum`; + + const genAI = new GoogleGenerativeAI(apiKey); + const model = genAI.getGenerativeModel({ model: process.env.GEMINI_MODEL ?? "gemini-2.5-flash" }); + + const result = await model.generateContent(prompt); + return result.response.text().trim(); +} + +export async function GET(request: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + const userId = session.user.id; + + const plan = (session.user as { plan?: string }).plan ?? "FREE"; + if (plan !== "PREMIUM") { + return NextResponse.json({ error: "L'analyse IA est rĂ©servĂ©e aux membres Premium." }, { status: 403 }); + } + + const url = new URL(request.url); + const forceRefresh = url.searchParams.get("refresh") === "1"; + + const existing = await prisma.dailyAnalysis.findUnique({ where: { userId } }); + + const isStale = + !existing || + forceRefresh || + !isSameDay(new Date(existing.generatedAt), new Date()); + + if (!isStale && existing) { + return NextResponse.json({ content: existing.content, generatedAt: existing.generatedAt, fresh: false }); + } + + try { + const content = await generateAnalysis(userId); + const record = await prisma.dailyAnalysis.upsert({ + where: { userId }, + create: { userId, content, generatedAt: new Date() }, + update: { content, generatedAt: new Date() }, + }); + return NextResponse.json({ content: record.content, generatedAt: record.generatedAt, fresh: true }); + } catch (err) { + if (existing) { + return NextResponse.json({ + content: existing.content, + generatedAt: existing.generatedAt, + fresh: false, + warning: err instanceof Error ? err.message : "Erreur de gĂ©nĂ©ration", + }); + } + return NextResponse.json( + { error: err instanceof Error ? err.message : "Erreur inconnue" }, + { status: 503 } + ); + } +} diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts new file mode 100644 index 0000000..a6c96ec --- /dev/null +++ b/src/app/api/export/route.ts @@ -0,0 +1,39 @@ +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { MOMENT_LABELS, type Moment } from "@/lib/glycemia"; +import { format } from "date-fns"; + +export async function GET() { + const session = await auth(); + if (!session?.user?.id) { + return new Response("Non authentifiĂ©", { status: 401 }); + } + const userId = session.user.id; + + const readings = await prisma.reading.findMany({ + where: { userId }, + orderBy: { measuredAt: "desc" }, + }); + + const header = ["Date", "Heure", "Moment", "GlycĂ©mie (g/L)", "Notes"].join(";"); + const rows = readings.map((r) => { + const d = new Date(r.measuredAt); + return [ + format(d, "yyyy-MM-dd"), + format(d, "HH:mm"), + MOMENT_LABELS[r.moment as Moment] ?? r.moment, + r.value.toFixed(2).replace(".", ","), + (r.notes ?? "").replace(/[\r\n;]/g, " "), + ].join(";"); + }); + + const csv = "ï»ż" + [header, ...rows].join("\r\n"); + const filename = `diabetix-${format(new Date(), "yyyyMMdd-HHmm")}.csv`; + + return new Response(csv, { + headers: { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": `attachment; filename="${filename}"`, + }, + }); +} diff --git a/src/app/api/patient/route.ts b/src/app/api/patient/route.ts new file mode 100644 index 0000000..3bee4f1 --- /dev/null +++ b/src/app/api/patient/route.ts @@ -0,0 +1,68 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { DIABETES_TYPE_VALUES, SEX_VALUES } from "@/lib/patient"; + +export async function GET() { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + + const patient = await prisma.patient.findUnique({ where: { userId: session.user.id } }); + return NextResponse.json(patient); +} + +export async function PUT(request: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + const userId = session.user.id; + + const body = await request.json().catch(() => null); + if (!body) return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + + const firstName = typeof body.firstName === "string" ? body.firstName.trim() : ""; + const lastName = typeof body.lastName === "string" ? body.lastName.trim() : ""; + if (!firstName || !lastName) { + return NextResponse.json({ error: "Nom et prĂ©nom requis" }, { status: 400 }); + } + + const email = + typeof body.email === "string" && body.email.trim() ? body.email.trim() : null; + const birthDate = + typeof body.birthDate === "string" && body.birthDate ? new Date(body.birthDate) : null; + + const heightCm = + body.heightCm === "" || body.heightCm == null ? null : Math.round(Number(body.heightCm)); + if (heightCm !== null && (!Number.isFinite(heightCm) || heightCm < 50 || heightCm > 260)) { + return NextResponse.json({ error: "Taille invalide (50–260 cm)" }, { status: 400 }); + } + + const weightKg = + body.weightKg === "" || body.weightKg == null + ? null + : Math.round(Number(body.weightKg) * 10) / 10; + if (weightKg !== null && (!Number.isFinite(weightKg) || weightKg < 20 || weightKg > 400)) { + return NextResponse.json({ error: "Poids invalide (20–400 kg)" }, { status: 400 }); + } + + const sex = + typeof body.sex === "string" && SEX_VALUES.includes(body.sex) ? body.sex : null; + const diabetesType = + typeof body.diabetesType === "string" && DIABETES_TYPE_VALUES.includes(body.diabetesType) + ? body.diabetesType + : null; + const treatment = + typeof body.treatment === "string" && body.treatment.trim() ? body.treatment.trim() : null; + if (treatment !== null && treatment.length > 1000) { + return NextResponse.json({ error: "Traitement trop long (1000 car. max)" }, { status: 400 }); + } + + const data = { firstName, lastName, email, birthDate, heightCm, weightKg, sex, diabetesType, treatment }; + + const patient = await prisma.patient.upsert({ + where: { userId }, + create: { userId, ...data }, + update: data, + }); + + return NextResponse.json(patient); +} diff --git a/src/app/api/readings/[id]/route.ts b/src/app/api/readings/[id]/route.ts new file mode 100644 index 0000000..865c41f --- /dev/null +++ b/src/app/api/readings/[id]/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function DELETE( + _request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + + const { id } = await params; + const numericId = Number(id); + if (!Number.isInteger(numericId)) { + return NextResponse.json({ error: "ID invalide" }, { status: 400 }); + } + + const reading = await prisma.reading.findUnique({ where: { id: numericId }, select: { userId: true } }); + if (!reading || reading.userId !== session.user.id) { + return NextResponse.json({ error: "RelevĂ© introuvable" }, { status: 404 }); + } + + await prisma.reading.delete({ where: { id: numericId } }); + return NextResponse.json({ ok: true }); +} + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + + const { id } = await params; + const numericId = Number(id); + if (!Number.isInteger(numericId)) { + return NextResponse.json({ error: "ID invalide" }, { status: 400 }); + } + + const reading = await prisma.reading.findUnique({ where: { id: numericId }, select: { userId: true } }); + if (!reading || reading.userId !== session.user.id) { + return NextResponse.json({ error: "RelevĂ© introuvable" }, { status: 404 }); + } + + const body = await request.json().catch(() => null); + if (!body) return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + + const data: Record = {}; + if (typeof body.value === "number" || typeof body.value === "string") { + const v = typeof body.value === "string" ? parseFloat(body.value.replace(",", ".")) : body.value; + if (Number.isFinite(v) && v > 0 && v <= 6) data.value = v; + } + if (typeof body.notes === "string" || body.notes === null) { + data.notes = body.notes?.trim() || null; + } + if (typeof body.measuredAt === "string") { + data.measuredAt = new Date(body.measuredAt); + } + if (typeof body.moment === "string" && ["FASTING", "LUNCH", "DINNER"].includes(body.moment)) { + data.moment = body.moment; + } + + const updated = await prisma.reading.update({ where: { id: numericId }, data }); + return NextResponse.json(updated); +} diff --git a/src/app/api/readings/route.ts b/src/app/api/readings/route.ts new file mode 100644 index 0000000..9bd4827 --- /dev/null +++ b/src/app/api/readings/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import type { Moment } from "@/lib/glycemia"; + +const VALID_MOMENTS: Moment[] = ["FASTING", "LUNCH", "DINNER"]; + +export async function GET(request: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + const userId = session.user.id; + + const url = new URL(request.url); + const limit = Number(url.searchParams.get("limit") ?? 200); + const from = url.searchParams.get("from"); + const to = url.searchParams.get("to"); + + const where: Record = { userId }; + if (from || to) { + where.measuredAt = { + ...(from ? { gte: new Date(from) } : {}), + ...(to ? { lte: new Date(to) } : {}), + }; + } + + const readings = await prisma.reading.findMany({ + where, + orderBy: { measuredAt: "desc" }, + take: Math.min(Math.max(limit, 1), 1000), + }); + + return NextResponse.json(readings); +} + +export async function POST(request: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + const userId = session.user.id; + + const body = await request.json().catch(() => null); + if (!body) return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + + const { measuredAt, moment, value, notes } = body as { + measuredAt?: string; + moment?: string; + value?: number | string; + notes?: string | null; + }; + + if (!moment || !VALID_MOMENTS.includes(moment as Moment)) { + return NextResponse.json({ error: "Moment invalide" }, { status: 400 }); + } + const numericValue = typeof value === "string" ? parseFloat(value.replace(",", ".")) : value; + if (typeof numericValue !== "number" || Number.isNaN(numericValue) || numericValue <= 0) { + return NextResponse.json({ error: "Valeur invalide" }, { status: 400 }); + } + if (numericValue > 6) { + return NextResponse.json({ error: "Valeur trop Ă©levĂ©e (>6 g/L)" }, { status: 400 }); + } + + const reading = await prisma.reading.create({ + data: { + userId, + measuredAt: measuredAt ? new Date(measuredAt) : new Date(), + moment, + value: numericValue, + notes: notes?.trim() || null, + }, + }); + + return NextResponse.json(reading, { status: 201 }); +} diff --git a/src/app/api/reports/generate-pdf/route.ts b/src/app/api/reports/generate-pdf/route.ts new file mode 100644 index 0000000..1e8b7fd --- /dev/null +++ b/src/app/api/reports/generate-pdf/route.ts @@ -0,0 +1,276 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { PDFDocument, PDFPage, rgb } from "pdf-lib"; +import { startOfMonth, endOfMonth, format } from "date-fns"; +import { fr } from "date-fns/locale"; + +const TEAL = rgb(0.08, 0.64, 0.64); // #14a4a4 - Diabetix teal +const TEAL_LIGHT = rgb(0.80, 0.95, 0.95); // Light teal background +const GRAY_DARK = rgb(0.15, 0.15, 0.15); // Dark gray text +const GRAY_LIGHT = rgb(0.62, 0.62, 0.62); // Light gray +const WHITE = rgb(1, 1, 1); +const RED = rgb(0.95, 0.26, 0.21); // For hypoglycemia +const GREEN = rgb(0.30, 0.77, 0.46); // For normal +const ORANGE = rgb(1, 0.59, 0.16); // For hyperglycemia + +export async function GET(request: NextRequest) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userId = session.user.id; + + try { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { patient: true }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const searchParams = request.nextUrl.searchParams; + const month = searchParams.get("month") ? new Date(searchParams.get("month")!) : new Date(); + + const startDate = startOfMonth(month); + const endDate = endOfMonth(month); + + const readings = await prisma.reading.findMany({ + where: { + userId, + measuredAt: { + gte: startDate, + lte: endDate, + }, + }, + orderBy: { measuredAt: "asc" }, + }); + + if (readings.length === 0) { + return NextResponse.json( + { error: "No readings found for this period" }, + { status: 404 } + ); + } + + const doc = await PDFDocument.create(); + let page = doc.addPage([595, 842]); // A4 + let y = 750; + + const monthFormatted = format(month, "MMMM yyyy", { locale: fr }); + const todayFormatted = format(new Date(), "dd MMMM yyyy", { locale: fr }); + + // Header with Diabetix branding + page.drawRectangle({ + x: 0, + y: 770, + width: 595, + height: 72, + color: TEAL, + }); + + page.drawText("Diabetix", { x: 50, y: 810, size: 32, color: WHITE }); + page.drawText("Rapport GlycĂ©mie Mensuel", { x: 50, y: 785, size: 14, color: TEAL_LIGHT }); + + y = 740; + + // Patient Information Section + page.drawRectangle({ + x: 50, + y: y - 120, + width: 495, + height: 120, + color: TEAL_LIGHT, + borderColor: TEAL, + borderWidth: 1.5, + }); + + page.drawText("INFORMATIONS PATIENT", { x: 60, y: y - 15, size: 10, color: TEAL }); + y -= 35; + + if (user.patient) { + page.drawText(`${user.patient.firstName} ${user.patient.lastName}`, { x: 60, y: y, size: 12, color: GRAY_DARK }); + y -= 18; + page.drawText(`Email: ${user.email}`, { x: 60, y: y, size: 9, color: GRAY_LIGHT }); + y -= 15; + page.drawText(`Type de diabĂšte: ${user.patient.diabetesType || "Non spĂ©cifiĂ©"}`, { x: 60, y: y, size: 9, color: GRAY_LIGHT }); + y -= 15; + page.drawText(`Traitement: ${user.patient.treatment || "Non spĂ©cifiĂ©"}`, { x: 60, y: y, size: 9, color: GRAY_LIGHT }); + } + + y = 610; + + // Report Period + page.drawText(`PĂ©riode: ${monthFormatted}`, { x: 50, y: y, size: 11, color: GRAY_DARK }); + page.drawText(`GĂ©nĂ©rĂ© le: ${todayFormatted}`, { x: 300, y: y, size: 11, color: GRAY_DARK }); + y -= 30; + + // Statistics Section + page.drawText("STATISTIQUES", { x: 50, y: y, size: 11, color: TEAL }); + y -= 20; + + const values = readings.map((r) => r.value); + const avg = values.reduce((a, b) => a + b, 0) / values.length; + const min = Math.min(...values); + const max = Math.max(...values); + const std = Math.sqrt(values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length); + + // Stats in a grid + const statBoxHeight = 60; + const statBoxWidth = 110; + const statBoxX = [50, 170, 290, 410]; + + const stats = [ + { label: "Mesures", value: readings.length.toString() }, + { label: "Moyenne", value: `${avg.toFixed(2)}` }, + { label: "Min", value: `${min.toFixed(2)}` }, + { label: "Max", value: `${max.toFixed(2)}` }, + ]; + + stats.forEach((stat, idx) => { + page.drawRectangle({ + x: statBoxX[idx], + y: y - statBoxHeight, + width: statBoxWidth, + height: statBoxHeight, + color: TEAL_LIGHT, + borderColor: TEAL, + borderWidth: 1, + }); + + page.drawText(stat.value, { x: statBoxX[idx] + 10, y: y - 30, size: 14, color: TEAL }); + page.drawText(stat.label, { x: statBoxX[idx] + 10, y: y - 50, size: 9, color: GRAY_LIGHT }); + }); + + y -= 85; + page.drawText(`Écart-type: ${std.toFixed(2)} g/L`, { x: 50, y: y, size: 9, color: GRAY_LIGHT }); + y -= 25; + + // Distribution Section + page.drawText("DISTRIBUTION DES MESURES", { x: 50, y: y, size: 11, color: TEAL }); + y -= 20; + + const hypoCount = readings.filter((r) => r.value < 0.7).length; + const normalCount = readings.filter((r) => r.value >= 0.7 && r.value <= 1.8).length; + const hyperCount = readings.filter((r) => r.value > 1.8).length; + + const distributions = [ + { label: "HypoglycĂ©mies (<0.7)", value: hypoCount, percent: ((hypoCount / readings.length) * 100).toFixed(1), color: ORANGE }, + { label: "Normales (0.7-1.8)", value: normalCount, percent: ((normalCount / readings.length) * 100).toFixed(1), color: GREEN }, + { label: "HyperglycĂ©mies (>1.8)", value: hyperCount, percent: ((hyperCount / readings.length) * 100).toFixed(1), color: ORANGE }, + ]; + + distributions.forEach((dist) => { + page.drawRectangle({ + x: 50, + y: y - 20, + width: 8, + height: 8, + color: dist.color, + }); + page.drawText(`${dist.label}: ${dist.value} (${dist.percent}%)`, { x: 65, y: y - 18, size: 9, color: GRAY_DARK }); + y -= 18; + }); + + y -= 15; + + // Detailed Readings Table + page.drawText("DÉTAIL DES MESURES", { x: 50, y: y, size: 11, color: TEAL }); + y -= 20; + + // Table header + const headerY = y; + page.drawRectangle({ + x: 50, + y: headerY - 18, + width: 495, + height: 18, + color: TEAL, + }); + + page.drawText("Date", { x: 60, y: headerY - 14, size: 8, color: WHITE }); + page.drawText("Heure", { x: 130, y: headerY - 14, size: 8, color: WHITE }); + page.drawText("Valeur", { x: 210, y: headerY - 14, size: 8, color: WHITE }); + page.drawText("Notes", { x: 280, y: headerY - 14, size: 8, color: WHITE }); + + y = headerY - 25; + + // Table rows + readings.forEach((reading, idx) => { + if (y < 60) { + page = doc.addPage([595, 842]); + y = 800; + + // Repeat header on new page + page.drawRectangle({ + x: 50, + y: y - 18, + width: 495, + height: 18, + color: TEAL, + }); + page.drawText("Date", { x: 60, y: y - 14, size: 8, color: WHITE }); + page.drawText("Heure", { x: 130, y: y - 14, size: 8, color: WHITE }); + page.drawText("Valeur", { x: 210, y: y - 14, size: 8, color: WHITE }); + page.drawText("Notes", { x: 280, y: y - 14, size: 8, color: WHITE }); + y -= 25; + } + + const date = format(new Date(reading.measuredAt), "dd/MM/yyyy", { locale: fr }); + const time = format(new Date(reading.measuredAt), "HH:mm"); + const value = reading.value.toFixed(2); + const notes = reading.notes?.substring(0, 30) || ""; + + // Color based on glycemia level + let rowColor = TEAL_LIGHT; + let textColor = GRAY_DARK; + + if (reading.value < 0.7) { + // Hypo - orange background + rowColor = rgb(1, 0.92, 0.80); // light orange + textColor = rgb(1, 0.59, 0.16); // darker orange text + } else if (reading.value > 1.8) { + // Hyper - orange background + rowColor = rgb(1, 0.92, 0.80); // light orange + textColor = rgb(1, 0.59, 0.16); // darker orange text + } else if (idx % 2 === 0) { + // Normal - alternate colors + rowColor = TEAL_LIGHT; + } else { + rowColor = WHITE; + } + + page.drawRectangle({ + x: 50, + y: y - 16, + width: 495, + height: 16, + color: rowColor, + }); + + page.drawText(date, { x: 60, y: y - 12, size: 8, color: textColor }); + page.drawText(time, { x: 130, y: y - 12, size: 8, color: textColor }); + page.drawText(value, { x: 210, y: y - 12, size: 8, color: textColor }); + page.drawText(notes, { x: 280, y: y - 12, size: 8, color: textColor }); + y -= 16; + }); + + const pdfBuffer = await doc.save(); + + return new NextResponse(pdfBuffer, { + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="rapport_glycemie_${format(month, "yyyy-MM")}.pdf"`, + }, + }); + } catch (error) { + console.error("[generate-pdf]", error); + return NextResponse.json( + { error: "Failed to generate PDF" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/stats/route.ts b/src/app/api/stats/route.ts new file mode 100644 index 0000000..3f82c11 --- /dev/null +++ b/src/app/api/stats/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { estimateHbA1c, statusFor, type Moment } from "@/lib/glycemia"; + +export async function GET() { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non authentifiĂ©" }, { status: 401 }); + const userId = session.user.id; + + const now = new Date(); + const ago7 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const ago30 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + const ago90 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + + const [last, last7, last30, last90] = await Promise.all([ + prisma.reading.findFirst({ where: { userId }, orderBy: { measuredAt: "desc" } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago7 } } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago30 } } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago90 } } }), + ]); + + const summarize = (rows: typeof last7) => { + if (rows.length === 0) { + return { count: 0, avg: null, inRangePct: null, hypoCount: 0, hyperCount: 0 }; + } + const sum = rows.reduce((acc, r) => acc + r.value, 0); + const avg = sum / rows.length; + let inRange = 0, hypo = 0, hyper = 0; + for (const r of rows) { + const s = statusFor(r.value, r.moment as Moment); + if (s === "in-range") inRange++; + else if (s === "hypo") hypo++; + else hyper++; + } + return { count: rows.length, avg, inRangePct: (inRange / rows.length) * 100, hypoCount: hypo, hyperCount: hyper }; + }; + + const s7 = summarize(last7); + const s30 = summarize(last30); + const s90 = summarize(last90); + + return NextResponse.json({ + last, + week: s7, + month: s30, + quarter: s90, + estimatedHbA1c: s90.avg ? estimateHbA1c(s90.avg) : null, + }); +} diff --git a/src/app/api/stripe/cancel-subscription/route.ts b/src/app/api/stripe/cancel-subscription/route.ts new file mode 100644 index 0000000..f4f2216 --- /dev/null +++ b/src/app/api/stripe/cancel-subscription/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { stripe } from "@/lib/stripe"; +import { prisma } from "@/lib/prisma"; + +export async function POST() { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userId = session.user.id; + + try { + const subscription = await prisma.subscription.findUnique({ + where: { userId }, + }); + + if (!subscription) { + return NextResponse.json( + { error: "No active subscription" }, + { status: 404 } + ); + } + + if (!subscription.stripeId.startsWith("sub_test_")) { + try { + await stripe.subscriptions.update(subscription.stripeId, { + cancel_at_period_end: true, + }); + } catch (error) { + console.warn("[cancel-subscription] Stripe cancellation failed, continuing with DB update", error); + } + } + + await prisma.subscription.update({ + where: { userId }, + data: { + status: "canceled", + canceledAt: new Date(), + }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("[cancel-subscription]", error); + return NextResponse.json( + { error: "Failed to cancel subscription" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/stripe/create-checkout/route.ts b/src/app/api/stripe/create-checkout/route.ts new file mode 100644 index 0000000..a5b34d2 --- /dev/null +++ b/src/app/api/stripe/create-checkout/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { stripe, STRIPE_PRICE_ID } from "@/lib/stripe"; +import { prisma } from "@/lib/prisma"; + +export async function POST() { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userId = session.user.id; + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + try { + let stripeCustomerId = user.stripeId; + + if (!stripeCustomerId) { + const customer = await stripe.customers.create({ + email: user.email, + metadata: { userId }, + }); + stripeCustomerId = customer.id; + await prisma.user.update({ + where: { id: userId }, + data: { stripeId: stripeCustomerId }, + }); + } + + const checkoutSession = await stripe.checkout.sessions.create({ + customer: stripeCustomerId, + mode: "subscription", + payment_method_types: ["card"], + line_items: [ + { + price: STRIPE_PRICE_ID, + quantity: 1, + }, + ], + success_url: `${process.env.NEXTAUTH_URL}/dashboard?checkout=success`, + cancel_url: `${process.env.NEXTAUTH_URL}/pricing`, + metadata: { userId }, + subscription_data: { + metadata: { userId }, + }, + }); + + return NextResponse.json({ sessionId: checkoutSession.id }); + } catch (error) { + console.error("[create-checkout]", error); + return NextResponse.json( + { error: "Failed to create checkout session" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/stripe/webhook/route.ts b/src/app/api/stripe/webhook/route.ts new file mode 100644 index 0000000..0bd0053 --- /dev/null +++ b/src/app/api/stripe/webhook/route.ts @@ -0,0 +1,116 @@ +import { NextResponse } from "next/server"; +import { stripe } from "@/lib/stripe"; +import { prisma } from "@/lib/prisma"; +import Stripe from "stripe"; + +const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || ""; + +async function handler(request: Request) { + if (request.method !== "POST") { + return NextResponse.json({ error: "Method not allowed" }, { status: 405 }); + } + + const body = await request.text(); + const signature = request.headers.get("stripe-signature") || ""; + + let event: Stripe.Event; + try { + event = stripe.webhooks.constructEvent(body, signature, webhookSecret); + } catch (error) { + console.error("[webhook] signature verification failed", error); + return NextResponse.json({ error: "Invalid signature" }, { status: 400 }); + } + + try { + console.log(`[webhook] received event: ${event.type}`); + + switch (event.type) { + case "customer.subscription.created": + case "customer.subscription.updated": { + const subscription = event.data.object as Stripe.Subscription; + const userId = subscription.metadata?.userId; + console.log(`[webhook] subscription event - userId: ${userId}, status: ${subscription.status}`); + if (!userId) { + console.warn("[webhook] missing userId in metadata"); + break; + } + + const active = subscription.status === "active"; + const currentPeriodEnd = new Date((subscription as any).current_period_end * 1000); + const cancelAtPeriodEnd = (subscription as any).cancel_at_period_end || false; + + const existing = await prisma.subscription.findUnique({ + where: { userId }, + }); + + if (existing) { + await prisma.subscription.update({ + where: { userId }, + data: { + status: subscription.status, + currentPeriodEnd, + canceledAt: (subscription as any).canceled_at + ? new Date((subscription as any).canceled_at * 1000) + : null, + }, + }); + } else { + await prisma.subscription.create({ + data: { + userId, + stripeId: subscription.id, + stripePriceId: subscription.items.data[0]?.price.id || "", + stripeCustomerId: subscription.customer as string, + status: subscription.status, + currentPeriodEnd, + }, + }); + } + + const keepPremium = active && !cancelAtPeriodEnd; + await prisma.user.update({ + where: { id: userId }, + data: { plan: keepPremium ? "PREMIUM" : "FREE" }, + }); + console.log(`[webhook] user ${userId} plan updated to: ${active ? "PREMIUM" : "FREE"}`); + break; + } + + case "customer.subscription.deleted": { + const subscription = event.data.object as Stripe.Subscription; + const userId = subscription.metadata?.userId; + if (!userId) break; + + await prisma.subscription.deleteMany({ where: { userId } }); + await prisma.user.update({ + where: { id: userId }, + data: { plan: "FREE" }, + }); + break; + } + + case "invoice.payment_failed": { + const invoice = event.data.object as Stripe.Invoice; + const userId = invoice.metadata?.userId; + if (!userId) break; + + await prisma.subscription.update({ + where: { userId }, + data: { status: "unpaid" }, + }); + break; + } + } + } catch (error) { + console.error("[webhook] processing error", error); + return NextResponse.json( + { error: "Failed to process webhook" }, + { status: 500 } + ); + } + + console.log(`[webhook] event ${event.type} processed successfully`); + return NextResponse.json({ received: true }); +} + +export { handler as POST }; diff --git a/src/app/api/test/create-premium-user/route.ts b/src/app/api/test/create-premium-user/route.ts new file mode 100644 index 0000000..7ddc434 --- /dev/null +++ b/src/app/api/test/create-premium-user/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + try { + const testEmail = "test.premium2@example.com"; + const testPassword = "TestPassword123!"; + + let user = await prisma.user.findUnique({ + where: { email: testEmail }, + }); + + if (!user) { + const passwordHash = await bcrypt.hash(testPassword, 10); + user = await prisma.user.create({ + data: { + email: testEmail, + name: "Test Premium User 2", + passwordHash, + plan: "PREMIUM", + emailVerified: new Date(), + stripeId: "cus_test_" + Date.now(), + subscription: { + create: { + stripeId: "sub_test_" + Date.now(), + stripePriceId: "price_test", + stripeCustomerId: "cus_test_" + Date.now(), + status: "active", + currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + }, + }, + }, + include: { subscription: true }, + }); + } else { + // Reset the existing user to premium status + await prisma.user.update({ + where: { id: user.id }, + data: { + plan: "PREMIUM", + emailVerified: new Date(), + }, + }); + + const sub = await prisma.subscription.findUnique({ where: { userId: user.id } }); + if (!sub) { + await prisma.subscription.create({ + data: { + userId: user.id, + stripeId: "sub_test_" + Date.now(), + stripePriceId: "price_test", + stripeCustomerId: "cus_test_" + Date.now(), + status: "active", + currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + }, + }); + } else { + await prisma.subscription.update({ + where: { userId: user.id }, + data: { + status: "active", + canceledAt: null, + currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + }, + }); + } + } + + return NextResponse.json({ + email: testEmail, + password: testPassword, + plan: user.plan, + hasSubscription: !!user.stripeId, + }); + } catch (error) { + console.error("[test] error", error); + return NextResponse.json( + { error: "Failed to create test user" }, + { status: 500 } + ); + } +} diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx new file mode 100644 index 0000000..a5aabfc --- /dev/null +++ b/src/app/auth/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Activity } from "lucide-react"; + +export const metadata: Metadata = { + title: "Diabetix", +}; + +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + + Diabetix + + {children} +
+ ); +} diff --git a/src/app/auth/login/LoginForm.tsx b/src/app/auth/login/LoginForm.tsx new file mode 100644 index 0000000..9299c93 --- /dev/null +++ b/src/app/auth/login/LoginForm.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useState } from "react"; +import { signIn } from "next-auth/react"; +import { useRouter, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { Loader2, Eye, EyeOff } from "lucide-react"; + +export function LoginForm() { + const router = useRouter(); + const params = useSearchParams(); + const verified = params.get("verified") === "1"; + const errorParam = params.get("error"); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPwd, setShowPwd] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + + const result = await signIn("credentials", { + email: email.trim().toLowerCase(), + password, + redirect: false, + }); + + if (result?.error) { + setError("Email ou mot de passe incorrect."); + setLoading(false); + return; + } + + router.push("/dashboard"); + router.refresh(); + } + + return ( +
+ {verified && ( +
+ ✅ Email confirmĂ© ! Vous pouvez vous connecter. +
+ )} + {errorParam === "token_expired" && ( +
+ ⚠ Lien expirĂ©. Inscrivez-vous Ă  nouveau. +
+ )} + +
+
+ + setEmail(e.target.value)} + required + className="w-full rounded-lg border border-slate-300 px-3 py-2.5 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200" + placeholder="vous@exemple.fr" + /> +
+ +
+ +
+ setPassword(e.target.value)} + required + className="w-full rounded-lg border border-slate-300 px-3 py-2.5 pr-10 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200" + placeholder="‱‱‱‱‱‱‱‱" + /> + +
+
+ + {error && ( +
+ {error} +
+ )} + + +
+ +

+ Pas encore de compte ?{" "} + + S'inscrire gratuitement + +

+
+ ); +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..0df9324 --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,18 @@ +import { LoginForm } from "./LoginForm"; +import { Suspense } from "react"; + +export const metadata = { title: "Connexion — Diabetix" }; + +export default function LoginPage() { + return ( +
+
+

Connexion

+

Accédez à votre espace de suivi

+
+ + + +
+ ); +} diff --git a/src/app/auth/register/RegisterForm.tsx b/src/app/auth/register/RegisterForm.tsx new file mode 100644 index 0000000..a37c12d --- /dev/null +++ b/src/app/auth/register/RegisterForm.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { CheckCircle2, Eye, EyeOff, Loader2 } from "lucide-react"; + +export function RegisterForm() { + const router = useRouter(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPwd, setShowPwd] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [done, setDone] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + + try { + const res = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "Erreur lors de l'inscription"); + if (data.emailError) { + console.warn("[register] email error:", data.emailError); + throw new Error(`Compte créé mais l'email de confirmation n'a pas pu ĂȘtre envoyĂ© : ${data.emailError}`); + } + setDone(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Erreur inconnue"); + } finally { + setLoading(false); + } + } + + if (done) { + return ( +
+ +

Vérifiez vos emails !

+

+ Un email de confirmation a été envoyé à {email}.
+ Cliquez sur le lien pour activer votre compte. +

+

+ Pas reçu ?{" "} + +

+
+ ); + } + + return ( +
+
+
+ + setName(e.target.value)} + required + minLength={2} + className="w-full rounded-lg border border-slate-300 px-3 py-2.5 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200" + placeholder="Jean Dupont" + /> +
+ +
+ + setEmail(e.target.value)} + required + className="w-full rounded-lg border border-slate-300 px-3 py-2.5 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200" + placeholder="vous@exemple.fr" + /> +
+ +
+ +
+ setPassword(e.target.value)} + required + minLength={8} + className="w-full rounded-lg border border-slate-300 px-3 py-2.5 pr-10 outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-200" + placeholder="‱‱‱‱‱‱‱‱" + /> + +
+
+ + {error && ( +
+ {error} +
+ )} + + + +

+ En vous inscrivant, vous acceptez nos{" "} + CGU + {" "}et notre{" "} + politique de confidentialité. +

+
+ +

+ Déjà inscrit ?{" "} + + Se connecter + +

+
+ ); +} diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx new file mode 100644 index 0000000..4eb2add --- /dev/null +++ b/src/app/auth/register/page.tsx @@ -0,0 +1,17 @@ +import { RegisterForm } from "./RegisterForm"; + +export const metadata = { title: "Inscription — Diabetix" }; + +export default function RegisterPage() { + return ( +
+
+

Créer un compte

+

+ Commencez gratuitement, sans carte bancaire +

+
+ +
+ ); +} diff --git a/src/app/auth/verify-pending/page.tsx b/src/app/auth/verify-pending/page.tsx new file mode 100644 index 0000000..e6f1e76 --- /dev/null +++ b/src/app/auth/verify-pending/page.tsx @@ -0,0 +1,25 @@ +import Link from "next/link"; +import { MailCheck } from "lucide-react"; + +export const metadata = { title: "VĂ©rification email — Diabetix" }; + +export default function VerifyPendingPage() { + return ( +
+
+ +

Vérifiez votre email

+

+ Votre compte est créé ! Cliquez sur le lien que vous avez reçu par email pour accéder à votre espace. +

+

+ Email non reçu ? Vérifiez vos spams ou{" "} + + réinscrivez-vous + + . +

+
+
+ ); +} diff --git a/src/app/dashboard/historique/page.tsx b/src/app/dashboard/historique/page.tsx new file mode 100644 index 0000000..c5d17a7 --- /dev/null +++ b/src/app/dashboard/historique/page.tsx @@ -0,0 +1,36 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { HistoryClient } from "@/app/historique/HistoryClient"; + +export const dynamic = "force-dynamic"; +export const metadata = { title: "Historique — Diabetix" }; + +export default async function HistoriquePage() { + const session = await auth(); + if (!session?.user?.id) redirect("/auth/login"); + const userId = session.user.id; + + const plan = (session.user as { plan?: string }).plan ?? "FREE"; + const cutoff = plan === "PREMIUM" + ? undefined + : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + + const readings = await prisma.reading.findMany({ + where: { userId, ...(cutoff ? { measuredAt: { gte: cutoff } } : {}) }, + orderBy: { measuredAt: "desc" }, + }); + + return ( + ({ + id: r.id, + measuredAt: r.measuredAt.toISOString(), + moment: r.moment, + value: r.value, + notes: r.notes, + }))} + isPremium={plan === "PREMIUM"} + /> + ); +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx new file mode 100644 index 0000000..75f38cb --- /dev/null +++ b/src/app/dashboard/layout.tsx @@ -0,0 +1,41 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { DIABETES_TYPE_LABELS } from "@/lib/patient"; +import { AppShell } from "@/components/AppShell"; + +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + if (!session?.user) redirect("/auth/login"); + if (!(session.user as { emailVerified?: Date | null }).emailVerified) redirect("/auth/verify-pending"); + + const patient = await prisma.patient.findUnique({ + where: { userId: session.user.id }, + select: { firstName: true, lastName: true, diabetesType: true }, + }); + + const fullName = patient + ? `${patient.firstName} ${patient.lastName}`.trim() + : session.user.name ?? null; + const initials = fullName + ? fullName.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2) + : null; + const diabetesLabel = patient?.diabetesType + ? DIABETES_TYPE_LABELS[patient.diabetesType] ?? null + : null; + + return ( + + {children} + + ); +} diff --git a/src/app/dashboard/mobile/page.tsx b/src/app/dashboard/mobile/page.tsx new file mode 100644 index 0000000..1aa665f --- /dev/null +++ b/src/app/dashboard/mobile/page.tsx @@ -0,0 +1,29 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { MobileEntry } from "@/app/mobile/MobileEntry"; + +export const dynamic = "force-dynamic"; + +export default async function MobilePage() { + const session = await auth(); + if (!session?.user?.id) redirect("/auth/login"); + const userId = session.user.id; + + const [patient, last3] = await Promise.all([ + prisma.patient.findUnique({ where: { userId }, select: { firstName: true } }), + prisma.reading.findMany({ + where: { userId }, + orderBy: { measuredAt: "desc" }, + take: 3, + select: { measuredAt: true, moment: true, value: true }, + }), + ]); + + return ( + ({ measuredAt: r.measuredAt.toISOString(), moment: r.moment, value: r.value }))} + /> + ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..ca303a7 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,250 @@ +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { format, formatDistanceToNow } from "date-fns"; +import { fr } from "date-fns/locale"; +import { ArrowRight, Droplet, History, PlusCircle, Smartphone, TrendingUp } from "lucide-react"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { + estimateHbA1c, + formatValue, + MOMENT_LABELS, + STATUS_STYLE, + statusFor, + type Moment, +} from "@/lib/glycemia"; +import { GlycemiaChart } from "@/components/GlycemiaChart"; +import { DailyInsight } from "@/components/DailyInsight"; + +export const dynamic = "force-dynamic"; + +function summarize(rows: { value: number; moment: string }[]) { + if (rows.length === 0) { + return { count: 0, avg: null as number | null, inRangePct: null as number | null, hypo: 0, hyper: 0 }; + } + let inRange = 0, hypo = 0, hyper = 0, sum = 0; + for (const r of rows) { + sum += r.value; + const s = statusFor(r.value, r.moment as Moment); + if (s === "in-range") inRange++; + else if (s === "hypo") hypo++; + else hyper++; + } + return { count: rows.length, avg: sum / rows.length, inRangePct: (inRange / rows.length) * 100, hypo, hyper }; +} + +export default async function DashboardPage() { + const session = await auth(); + if (!session?.user?.id) redirect("/auth/login"); + const userId = session.user.id; + const plan = (session.user as { plan?: string }).plan ?? "FREE"; + + const now = new Date(); + const ago7 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const ago30 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + const ago90 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + + // FREE plan: only last 7 days + const readingCutoff = plan === "PREMIUM" ? ago90 : ago7; + + const [last, last7, last30, last90, chartRows, patient] = await Promise.all([ + prisma.reading.findFirst({ where: { userId }, orderBy: { measuredAt: "desc" } }), + prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago7 } } }), + plan === "PREMIUM" + ? prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago30 } } }) + : Promise.resolve([]), + plan === "PREMIUM" + ? prisma.reading.findMany({ where: { userId, measuredAt: { gte: ago90 } } }) + : Promise.resolve([]), + prisma.reading.findMany({ + where: { userId, measuredAt: { gte: readingCutoff } }, + orderBy: { measuredAt: "asc" }, + select: { measuredAt: true, value: true, moment: true }, + }), + prisma.patient.findUnique({ where: { userId }, select: { firstName: true } }), + ]); + + const week = summarize(last7); + const month = summarize(last30); + const quarter = summarize(last90); + const hba1c = quarter.avg ? estimateHbA1c(quarter.avg) : null; + + const lastStatus = last ? statusFor(last.value, last.moment as Moment) : null; + const isPremium = plan === "PREMIUM"; + + return ( +
+
+
+

+ {patient ? `Bonjour ${patient.firstName}` : "Tableau de bord"} +

+

+ Suivi de votre glycĂ©mie — 3 relevĂ©s par jour (matin, midi, soir). +

+
+
+ + + Saisie rapide + + + + Nouveau relevé + +
+
+ + {/* DerniĂšre mesure */} +
+
+

DerniĂšre mesure

+ +
+ {last && lastStatus ? ( +
+
+
+ {formatValue(last.value)} +
+
+ {MOMENT_LABELS[last.moment as Moment]} · il y a{" "} + {formatDistanceToNow(new Date(last.measuredAt), { locale: fr })} +
+
+ {format(new Date(last.measuredAt), "EEEE d MMMM yyyy 'Ă ' HH:mm", { locale: fr })} +
+
+ + {STATUS_STYLE[lastStatus].label} + + {last.notes && ( +
« {last.notes} »
+ )} +
+ ) : ( +
+ Aucun relevé pour le moment.{" "} + + Saisir le premier + + . +
+ )} +
+ + {isPremium ? ( + + ) : ( +
+

+ 💡 L'analyse IA quotidienne et le coach Diablo sont disponibles avec le plan{" "} + + Premium + + . +

+
+ )} + + {/* Stats cards */} +
+ 1 ? "s" : ""}`} + /> + {isPremium ? ( + <> + 1 ? "s" : ""}`} + /> + 0 ? `${month.hypo} hypo · ${month.hyper} hyper` : "Aucune donnée"} + accent={ + month.inRangePct === null + ? undefined + : month.inRangePct >= 70 + ? "text-emerald-700" + : month.inRangePct >= 50 + ? "text-amber-700" + : "text-rose-700" + } + /> + + + ) : ( +
+

+ Statistiques 30/90 jours et HbA1c estimée disponibles avec le plan{" "} + Premium. +

+
+ )} +
+ + {/* Chart */} +
+
+

+ + {isPremium ? "Évolution sur 30 jours" : "Évolution sur 7 jours"} +

+ + + Voir l'historique + + +
+ ({ + measuredAt: r.measuredAt.toISOString(), + value: r.value, + moment: r.moment, + }))} + /> +
+ + + +
+
+
+ ); +} + +function StatCard({ title, value, sub, accent }: { title: string; value: string; sub?: string; accent?: string }) { + return ( +
+
{title}
+
{value}
+ {sub &&
{sub}
} +
+ ); +} + +function LegendDot({ color, label }: { color: string; label: string }) { + return ( + + + {label} + + ); +} diff --git a/src/app/dashboard/profil/page.tsx b/src/app/dashboard/profil/page.tsx new file mode 100644 index 0000000..457a445 --- /dev/null +++ b/src/app/dashboard/profil/page.tsx @@ -0,0 +1,51 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { ProfileForm } from "@/app/profil/ProfileForm"; +import { CancelSubscriptionButton } from "@/app/profil/CancelSubscriptionButton"; + +export const dynamic = "force-dynamic"; +export const metadata = { title: "Mon profil — Diabetix" }; + +export default async function ProfilPage() { + const session = await auth(); + if (!session?.user?.id) redirect("/auth/login"); + const userId = session.user.id; + + const patient = await prisma.patient.findUnique({ where: { userId } }); + const user = await prisma.user.findUnique({ where: { id: userId } }); + const subscription = await prisma.subscription.findUnique({ + where: { userId }, + }); + + return ( +
+
+

Mon profil

+

Vos informations personnelles et médicales.

+
+ + +
+ ); +} diff --git a/src/app/dashboard/rapports/page.tsx b/src/app/dashboard/rapports/page.tsx new file mode 100644 index 0000000..9a04dcf --- /dev/null +++ b/src/app/dashboard/rapports/page.tsx @@ -0,0 +1,110 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { format, subMonths, startOfMonth, endOfMonth } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Download, Lock } from "lucide-react"; + +export const dynamic = "force-dynamic"; +export const metadata = { title: "Mes Rapports — Diabetix" }; + +export default async function RapportsPage() { + const session = await auth(); + if (!session?.user?.id) redirect("/auth/login"); + + const userId = session.user.id; + const user = await prisma.user.findUnique({ where: { id: userId } }); + + const isPremium = user?.plan === "PREMIUM"; + + const months = Array.from({ length: 12 }, (_, i) => { + return subMonths(startOfMonth(new Date()), i); + }); + + const readingsByMonth = await Promise.all( + months.map(async (month) => { + const count = await prisma.reading.count({ + where: { + userId, + measuredAt: { + gte: month, + lte: endOfMonth(month), + }, + }, + }); + return { month, count }; + }) + ); + + // Filter to only show months with readings + const monthsWithReadings = readingsByMonth.filter(({ count }) => count > 0); + + return ( +
+
+

Mes Rapports

+

+ Téléchargez vos rapports glycémie mensuels en PDF +

+
+ + {!isPremium && ( +
+
+ +
+

Feature Premium

+

+ Les rapports PDF sont exclusifs au plan Premium. Passer à Premium pour y accéder. +

+
+
+
+ )} + +
+ {monthsWithReadings.length > 0 ? ( + monthsWithReadings.map(({ month, count }) => ( +
+
+

+ {format(month, "MMMM yyyy", { locale: fr })} +

+

{count} mesure(s)

+
+ + {count > 0 && isPremium && ( + + + Télécharger + PDF + + )} + + {count > 0 && !isPremium && ( +
+ +
+ )} + + {count === 0 && ( +

Aucune donnée

+ )} +
+ )) + ) : ( +
+

Aucun mois avec des données.

+
+ )} +
+
+ ); +} diff --git a/src/app/dashboard/saisie/page.tsx b/src/app/dashboard/saisie/page.tsx new file mode 100644 index 0000000..8b620dc --- /dev/null +++ b/src/app/dashboard/saisie/page.tsx @@ -0,0 +1,15 @@ +import { ReadingForm } from "@/app/saisie/ReadingForm"; + +export const metadata = { title: "Nouveau relevĂ© — Diabetix" }; + +export default function SaisiePage() { + return ( +
+
+

Nouveau relevé

+

Saisissez votre glycémie du moment.

+
+ +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..10be18d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,11 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - +html, body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-geist-sans), system-ui, -apple-system, sans-serif; } diff --git a/src/app/historique/HistoryClient.tsx b/src/app/historique/HistoryClient.tsx new file mode 100644 index 0000000..0246bbb --- /dev/null +++ b/src/app/historique/HistoryClient.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { useMemo, useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Download, Trash2 } from "lucide-react"; +import { + formatValue, + MOMENT_LABELS, + STATUS_STYLE, + statusFor, + type Moment, +} from "@/lib/glycemia"; + +type ReadingRow = { + id: number; + measuredAt: string; + moment: string; + value: number; + notes: string | null; +}; + +type Filter = "all" | "FASTING" | "LUNCH" | "DINNER" | "out-of-range"; + +export function HistoryClient({ readings, isPremium }: { readings: ReadingRow[]; isPremium?: boolean }) { + const router = useRouter(); + const [filter, setFilter] = useState("all"); + const [pending, startTransition] = useTransition(); + const [deletingId, setDeletingId] = useState(null); + + const filtered = useMemo(() => { + return readings.filter((r) => { + if (filter === "all") return true; + if (filter === "out-of-range") { + return statusFor(r.value, r.moment as Moment) !== "in-range"; + } + return r.moment === filter; + }); + }, [readings, filter]); + + async function handleDelete(id: number) { + if (!confirm("Supprimer ce relevé ?")) return; + setDeletingId(id); + try { + const res = await fetch(`/api/readings/${id}`, { method: "DELETE" }); + if (!res.ok) throw new Error("Suppression impossible"); + startTransition(() => router.refresh()); + } catch (e) { + alert(e instanceof Error ? e.message : "Erreur"); + } finally { + setDeletingId(null); + } + } + + return ( +
+ {!isPremium && ( +
+ Seuls les 7 derniers jours sont affichés sur le plan gratuit.{" "} + Passer à Premium pour l'historique complet. +
+ )} +
+
+

Historique

+

+ {readings.length} relevé{readings.length > 1 ? "s" : ""} enregistré + {readings.length > 1 ? "s" : ""}. +

+
+ + + Exporter en CSV + +
+ +
+ setFilter("all")}> + Tous + + setFilter("FASTING")}> + Matin + + setFilter("LUNCH")}> + Midi + + setFilter("DINNER")}> + Soir + + setFilter("out-of-range")}> + Hors cible + +
+ +
+ {filtered.length === 0 ? ( +
+ Aucun relevé pour ce filtre. +
+ ) : ( +
    + {filtered.map((r) => { + const status = statusFor(r.value, r.moment as Moment); + const date = new Date(r.measuredAt); + return ( +
  • +
    +
    + + {formatValue(r.value)} + + + {STATUS_STYLE[status].label} + + + {MOMENT_LABELS[r.moment as Moment]} + +
    +
    + {format(date, "EEEE d MMM yyyy 'Ă ' HH:mm", { locale: fr })} +
    + {r.notes && ( +
    + « {r.notes} » +
    + )} +
    + +
  • + ); + })} +
+ )} +
+
+ ); +} + +function FilterPill({ + active, + onClick, + children, +}: { + active: boolean; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} diff --git a/src/app/historique/page.tsx b/src/app/historique/page.tsx new file mode 100644 index 0000000..4a4176d --- /dev/null +++ b/src/app/historique/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function HistoriquePage() { + redirect("/dashboard/historique"); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..6823f29 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,33 +1,32 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { ServiceWorkerRegister } from "@/components/ServiceWorkerRegister"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); +const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Diabetix — Suivi de glycĂ©mie", + description: "Application de suivi du diabĂšte : trois relevĂ©s quotidiens, statistiques et tendances.", + manifest: "/manifest.webmanifest", + appleWebApp: { capable: true, statusBarStyle: "default", title: "Diabetix" }, }; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export const viewport: Viewport = { + themeColor: "#0f766e", + width: "device-width", + initialScale: 1, + maximumScale: 1, +}; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - - {children} + + + + {children} + ); } diff --git a/src/app/mobile/MobileEntry.tsx b/src/app/mobile/MobileEntry.tsx new file mode 100644 index 0000000..cc292cf --- /dev/null +++ b/src/app/mobile/MobileEntry.tsx @@ -0,0 +1,297 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { + CheckCircle2, + ChevronLeft, + ChevronRight, + Delete, + Droplets, + Loader2, +} from "lucide-react"; +import { + MOMENT_LABELS, + STATUS_STYLE, + statusFor, + type Moment, +} from "@/lib/glycemia"; +import { cn } from "@/lib/utils"; + +type LastReading = { measuredAt: string; moment: string; value: number }; + +const MOMENTS: Moment[] = ["FASTING", "LUNCH", "DINNER"]; +const MOMENT_SHORT: Record = { + FASTING: "Matin", + LUNCH: "Midi", + DINNER: "Soir", +}; +const MOMENT_ICON: Record = { + FASTING: "🌅", + LUNCH: "☀", + DINNER: "🌙", +}; + +function suggestMoment(): Moment { + const h = new Date().getHours(); + if (h < 11) return "FASTING"; + if (h < 17) return "LUNCH"; + return "DINNER"; +} + +// Simple numpad keys +const PAD_KEYS = [ + ["1", "2", "3"], + ["4", "5", "6"], + ["7", "8", "9"], + [".", "0", "⌫"], +]; + +export function MobileEntry({ + firstName, + last3, +}: { + firstName: string | null; + last3: LastReading[]; +}) { + const router = useRouter(); + const [moment, setMoment] = useState(suggestMoment()); + const [digits, setDigits] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [success, setSuccess] = useState<{ value: number; moment: Moment } | null>(null); + const [error, setError] = useState(null); + + const momentIdx = MOMENTS.indexOf(moment); + + function prevMoment() { + setMoment(MOMENTS[(momentIdx + MOMENTS.length - 1) % MOMENTS.length]); + } + function nextMoment() { + setMoment(MOMENTS[(momentIdx + 1) % MOMENTS.length]); + } + + function handleKey(key: string) { + setError(null); + if (key === "⌫") { + setDigits((d) => d.slice(0, -1)); + return; + } + // Only one dot allowed + if (key === "." && digits.includes(".")) return; + // Max 4 chars total (e.g. "1.35") + if (digits.replace(".", "").length >= 4) return; + // Don't start with two zeros + if (digits === "0" && key === "0") return; + setDigits((d) => d + key); + } + + const numericValue = useMemo(() => { + const v = parseFloat(digits.replace(",", ".")); + return Number.isFinite(v) && v > 0 ? v : null; + }, [digits]); + + const liveStatus = numericValue !== null ? statusFor(numericValue, moment) : null; + + async function handleSubmit() { + if (!numericValue) { + setError("Saisissez une valeur."); + return; + } + if (numericValue > 6) { + setError("Valeur trop Ă©levĂ©e (> 6 g/L)."); + return; + } + + setSubmitting(true); + setError(null); + try { + const res = await fetch("/api/readings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ moment, value: numericValue }), + }); + if (!res.ok) { + const d = await res.json().catch(() => ({})); + throw new Error(d.error ?? "Erreur serveur"); + } + setSuccess({ value: numericValue, moment }); + setDigits(""); + // Auto-advance to next moment + setMoment(MOMENTS[(momentIdx + 1) % MOMENTS.length]); + router.refresh(); + setTimeout(() => setSuccess(null), 3000); + } catch (e) { + setError(e instanceof Error ? e.message : "Erreur inconnue"); + } finally { + setSubmitting(false); + } + } + + // Format display value nicely + const displayValue = digits === "" ? "—" : digits + (digits.endsWith(".") ? "" : ""); + + return ( +
+ {/* Top bar */} +
+
+ + + {firstName ? `Bonjour ${firstName}` : "Saisie rapide"} + +
+ + {format(new Date(), "EEE d MMM", { locale: fr })} + +
+ +
+ {/* Last 3 readings */} + {last3.length > 0 && ( +
+ {last3.map((r, i) => { + const s = statusFor(r.value, r.moment as Moment); + return ( +
+ + {r.value.toFixed(2).replace(".", ",")} + + + {MOMENT_ICON[r.moment as Moment]} {MOMENT_SHORT[r.moment as Moment]} + + + {format(new Date(r.measuredAt), "HH:mm")} + +
+ ); + })} +
+ )} + + {/* Moment selector */} +
+ +
+
{MOMENT_ICON[moment]}
+
{MOMENT_SHORT[moment]}
+
{MOMENT_LABELS[moment]}
+
+ +
+ + {/* Value display */} +
+
+ {displayValue} +
+
g/L
+ {liveStatus && ( + + {STATUS_STYLE[liveStatus].label} + + )} +
+ + {/* Numpad */} +
+ {PAD_KEYS.flat().map((key) => ( + + ))} +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Success */} + {success && ( +
+ + {success.value.toFixed(2).replace(".", ",")} g/L enregistrĂ© —{" "} + {MOMENT_SHORT[success.moment]} +
+ )} + + {/* Submit */} + +
+
+ ); +} diff --git a/src/app/mobile/layout.tsx b/src/app/mobile/layout.tsx new file mode 100644 index 0000000..b76d606 --- /dev/null +++ b/src/app/mobile/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata, Viewport } from "next"; + +export const metadata: Metadata = { + title: "Saisie rapide — Diabetix", + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: "Diabetix", + }, +}; + +export const viewport: Viewport = { + themeColor: "#0f766e", + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + +export default function MobileLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/mobile/page.tsx b/src/app/mobile/page.tsx new file mode 100644 index 0000000..bfecf85 --- /dev/null +++ b/src/app/mobile/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function MobilePage() { + redirect("/dashboard/mobile"); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..609ebf7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,255 @@ -import Image from "next/image"; +import Link from "next/link"; +import { Activity, BarChart3, Bot, CheckCircle2, Crown, Shield, Smartphone } from "lucide-react"; -export default function Home() { +export const metadata = { + title: "Diabetix — Suivi de glycĂ©mie intelligent", + description: "Application de suivi de glycĂ©mie avec analyse IA, coach personnel et tableau de bord complet. Disponible sur web et mobile.", +}; + +export default function LandingPage() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. +
+ {/* Header */} +
+
+
+ + Diabetix +
+ +
+ + Connexion + + + Commencer gratuitement + +
+
+
+ + {/* Hero */} +
+
+
+ + Coach IA personnalisé inclus en Premium +
+

+ Prenez le contrÎle de votre{" "} + glycémie

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. +

+ Diabetix vous aide à suivre vos relevés quotidiens, visualiser vos tendances et recevoir des conseils personnalisés grùce à l'intelligence artificielle.

+
+ + Commencer gratuitement + + + Voir les tarifs + +
+

Sans carte bancaire · AccÚs immédiat · Données sécurisées

-
+ + {/* Features */} +
+
+

Tout ce dont vous avez besoin

+

+ Une application complÚte, pensée pour les patients diabétiques au quotidien. +

+
+ {[ + { + icon: BarChart3, + title: "Tableau de bord complet", + desc: "Visualisez vos relevĂ©s, moyennes, % dans la cible et estimation HbA1c sur 7, 30 et 90 jours.", + }, + { + icon: Smartphone, + title: "Saisie mobile ultra-rapide", + desc: "Interface dĂ©diĂ©e avec pavĂ© numĂ©rique, optimisĂ©e pour une saisie en quelques secondes sur smartphone.", + }, + { + icon: Bot, + title: "Coach IA Diablo", + desc: "Un coach bienveillant qui analyse vos donnĂ©es et vous donne des conseils personnalisĂ©s en tenant compte de votre profil.", + }, + { + icon: Activity, + title: "Analyse quotidienne IA", + desc: "Chaque matin, recevez une analyse fraĂźche de vos tendances glycĂ©miques avec un message d'encouragement.", + }, + { + icon: Shield, + title: "DonnĂ©es sĂ©curisĂ©es", + desc: "Vos donnĂ©es de santĂ© restent les vĂŽtres. Stockage sĂ©curisĂ©, accĂšs chiffrĂ©, export CSV Ă  tout moment.", + }, + { + icon: CheckCircle2, + title: "Profil mĂ©dical complet", + desc: "Renseignez votre type de diabĂšte, traitement, taille, poids — le coach s'adapte Ă  votre situation.", + }, + ].map((f) => ( +
+
+ +
+

{f.title}

+

{f.desc}

+
+ ))} +
-

+ + + {/* Pricing */} +
+
+

Tarifs simples et transparents

+

Commencez gratuitement, passez Premium quand vous ĂȘtes prĂȘt.

+
+ {/* Free */} +
+
Gratuit
+
0 €
+
pour toujours
+ + Commencer gratuitement + +
    + {[ + "Saisie illimitée de relevés", + "Tableau de bord 7 jours", + "Statistiques hebdomadaires", + "Saisie mobile rapide", + "Export CSV", + ].map((f) => ( +
  • + + {f} +
  • + ))} +
+
+ {/* Premium */} +
+
+ + + Recommandé + +
+
Premium
+
+ 4,99 € + /mois +
+
sans engagement
+ + Essayer Premium + +
    + {[ + "Tout le plan Gratuit", + "Historique illimité", + "Statistiques 30 et 90 jours", + "HbA1c estimée (90j)", + "Analyse IA quotidienne", + "Coach Diablo en temps réel", + ].map((f) => ( +
  • + + {f} +
  • + ))} +
+
+
+
+
+ + {/* FAQ */} +
+
+

Questions fréquentes

+
+ {[ + { + q: "Diabetix remplace-t-il mon médecin ?", + a: "Non. Diabetix est un outil de suivi et d'aide à la compréhension de vos données. Il ne remplace pas votre équipe soignante et ne donne jamais de conseils médicaux. Consultez toujours votre médecin pour toute décision de santé.", + }, + { + q: "Mes données sont-elles sécurisées ?", + a: "Oui. Vos données sont stockées de façon sécurisée et ne sont jamais partagées avec des tiers. Vous pouvez les exporter ou les supprimer à tout moment.", + }, + { + q: "Comment fonctionne le coach IA Diablo ?", + a: "Diablo utilise l'IA Gemini de Google pour analyser vos relevés, votre profil médical et vous donner des conseils personnalisés. Il ne peut pas modifier votre traitement médical et vous renverra toujours vers votre médecin pour les décisions importantes.", + }, + { + q: "Puis-je annuler mon abonnement Premium à tout moment ?", + a: "Oui, sans engagement. Vous pouvez revenir au plan gratuit à tout moment.", + }, + { + q: "L'application fonctionne-t-elle sur mobile ?", + a: "Oui. Diabetix est une Progressive Web App (PWA) optimisée pour mobile. Une interface de saisie dédiée est disponible pour entrer vos relevés en quelques secondes.", + }, + ].map(({ q, a }) => ( +
+

{q}

+

{a}

+
+ ))} +
+
+
+ + {/* CTA */} +
+

PrĂȘt Ă  mieux gĂ©rer votre diabĂšte ?

+

Rejoignez Diabetix gratuitement et commencez votre suivi aujourd'hui.

+ + Créer mon compte gratuit + +
+ + {/* Footer */} +
+
+ + Diabetix +
+

© {new Date().getFullYear()} Diabetix — Application de suivi de glycĂ©mie

+

+ Cet outil ne remplace pas un avis médical. Consultez votre médecin pour toute décision de santé. +

+
+ CGU & Confidentialité +
+
); } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx new file mode 100644 index 0000000..9c4d283 --- /dev/null +++ b/src/app/pricing/page.tsx @@ -0,0 +1,89 @@ +import Link from "next/link"; +import { CheckCircle2, Crown } from "lucide-react"; +import { auth } from "@/lib/auth"; +import { UpgradeButton } from "@/components/UpgradeButton"; + +export const metadata = { title: "Tarifs — Diabetix" }; + +export default async function PricingPage() { + const session = await auth(); + const isPremium = (session?.user as { plan?: string })?.plan === "PREMIUM"; + + return ( +
+

Choisissez votre plan

+

Commencez gratuitement, passez Premium quand vous ĂȘtes prĂȘt.

+ +
+ {/* Free */} +
+
Gratuit
+
0 €
+ {!session ? ( + + Commencer + + ) : ( +
+ Votre plan actuel +
+ )} +
    + {["Saisie illimitée", "Tableau de bord 7 jours", "Statistiques semaine", "Export CSV"].map((f) => ( +
  • + + {f} +
  • + ))} +
+
+ + {/* Premium */} +
+
+ + + Recommandé + +
+
Premium
+
+ 4,99 € + /mois +
+
+ {!session ? ( + + Essayer Premium + + ) : ( + + )} +
+
    + {["Tout le plan Gratuit", "Historique illimité", "Stats 30 et 90 jours", "HbA1c estimée", "Analyse IA quotidienne", "Coach Diablo"].map((f) => ( +
  • + + {f} +
  • + ))} +
+
+
+ +

+ {!session ? ( + <> + Déjà inscrit ?{" "} + Se connecter + + ) : ( + <> + Besoin d'aide ?{" "} + Contactez-nous + + )} +

+
+ ); +} diff --git a/src/app/profil/CancelSubscriptionButton.tsx b/src/app/profil/CancelSubscriptionButton.tsx new file mode 100644 index 0000000..e749dee --- /dev/null +++ b/src/app/profil/CancelSubscriptionButton.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AlertCircle, Loader2, CheckCircle2 } from "lucide-react"; + +export function CancelSubscriptionButton({ + isPremium, + subscriptionEndDate, + isScheduledForCancellation, +}: { + isPremium: boolean; + subscriptionEndDate?: Date; + isScheduledForCancellation?: boolean; +}) { + const router = useRouter(); + const [canceling, setCanceling] = useState(false); + const [error, setError] = useState(null); + const [showConfirm, setShowConfirm] = useState(false); + const [isCanceled, setIsCanceled] = useState(!!isScheduledForCancellation); + + if (!isPremium) return null; + + async function handleCancel() { + setCanceling(true); + setError(null); + + try { + const res = await fetch("/api/stripe/cancel-subscription", { + method: "POST", + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error ?? "Erreur lors de la résiliation."); + } + + setShowConfirm(false); + setIsCanceled(true); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "Erreur inconnue"); + } finally { + setCanceling(false); + } + } + + const endDateFormatted = subscriptionEndDate + ? new Intl.DateTimeFormat("fr-FR", { + year: "numeric", + month: "long", + day: "numeric", + }).format(subscriptionEndDate) + : null; + + return ( +
+ {isCanceled ? ( +
+
+ +
+

Abonnement résilié

+

+ Votre accĂšs Premium reste actif jusqu'au {endDateFormatted}. +

+
+
+
+ ) : !showConfirm ? ( + + ) : ( +
+
+ +
+

Êtes-vous sĂ»r ?

+

+ Votre accĂšs Premium restera actif jusqu'au {endDateFormatted}. AprĂšs cette date, vous reviendrez au plan gratuit. +

+
+
+
+ + +
+
+ )} + {error && ( +
+ {error} +
+ )} +
+ ); +} diff --git a/src/app/profil/ProfileForm.tsx b/src/app/profil/ProfileForm.tsx new file mode 100644 index 0000000..f92b0fe --- /dev/null +++ b/src/app/profil/ProfileForm.tsx @@ -0,0 +1,311 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { signOut } from "next-auth/react"; +import { CheckCircle2, Loader2, LogOut } from "lucide-react"; +import { DIABETES_TYPE_OPTIONS, SEX_OPTIONS } from "@/lib/patient"; + +type PatientInput = { + firstName: string; + lastName: string; + email: string | null; + birthDate: string | null; + heightCm: number | null; + weightKg: number | null; + sex: string | null; + diabetesType: string | null; + treatment: string | null; +}; + +export function ProfileForm({ initial }: { initial: PatientInput | null }) { + const router = useRouter(); + const [form, setForm] = useState(() => ({ + firstName: initial?.firstName ?? "", + lastName: initial?.lastName ?? "", + email: initial?.email ?? "", + birthDate: initial?.birthDate ?? "", + heightCm: initial?.heightCm ?? null, + weightKg: initial?.weightKg ?? null, + sex: initial?.sex ?? null, + diabetesType: initial?.diabetesType ?? null, + treatment: initial?.treatment ?? "", + })); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const bmi = useMemo(() => { + if (!form.heightCm || !form.weightKg) return null; + const m = form.heightCm / 100; + return form.weightKg / (m * m); + }, [form.heightCm, form.weightKg]); + + const age = useMemo(() => { + if (!form.birthDate) return null; + const d = new Date(form.birthDate); + if (Number.isNaN(d.getTime())) return null; + const now = new Date(); + let a = now.getFullYear() - d.getFullYear(); + const m = now.getMonth() - d.getMonth(); + if (m < 0 || (m === 0 && now.getDate() < d.getDate())) a--; + return a; + }, [form.birthDate]); + + function update(key: K, value: PatientInput[K]) { + setForm((f) => ({ ...f, [key]: value })); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setSuccess(false); + + if (!form.firstName.trim() || !form.lastName.trim()) { + setError("Le nom et le prénom sont obligatoires."); + return; + } + + setSubmitting(true); + try { + const res = await fetch("/api/patient", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + firstName: form.firstName.trim(), + lastName: form.lastName.trim(), + email: form.email?.trim() || null, + birthDate: form.birthDate || null, + heightCm: form.heightCm, + weightKg: form.weightKg, + sex: form.sex || null, + diabetesType: form.diabetesType || null, + treatment: form.treatment?.trim() || null, + }), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error ?? "Erreur lors de l'enregistrement."); + } + setSuccess(true); + router.refresh(); + setTimeout(() => setSuccess(false), 2500); + } catch (err) { + setError(err instanceof Error ? err.message : "Erreur inconnue"); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+ + update("firstName", e.target.value)} + className={inputClass} + autoComplete="given-name" + /> + + + update("lastName", e.target.value)} + className={inputClass} + autoComplete="family-name" + /> + +
+ + + update("email", e.target.value)} + className={inputClass} + autoComplete="email" + placeholder="prenom.nom@exemple.fr" + /> + + + + update("birthDate", e.target.value)} + className={inputClass} + /> + + +
+ + + update("heightCm", e.target.value === "" ? null : Number(e.target.value)) + } + className={inputClass} + placeholder="175" + /> + + + + update("weightKg", e.target.value === "" ? null : Number(e.target.value)) + } + className={inputClass} + placeholder="72,5" + /> + +
+ +
+

+ Profil médical +

+ +
+ + + + + + +
+ +
+ +