commit 83fcf6909cceb73898b0693f4e9e9c03df281489 Author: KitchenStock Date: Sun Jul 26 20:08:16 2026 +0200 feat: KitchenStock light UI, Gemma 31B AI, supermarket store filters & 130+ vegan products diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..78143f0 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,37 @@ +name: KitchenStock Quality CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + quality: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install Dependencies + run: npm ci --legacy-peer-deps + + - name: Run Typecheck + run: npm run typecheck + + - name: Run Vitest Unit Tests + run: npm run test + + - name: Build Next.js Production App + run: npm run build + env: + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || 'https://demo.supabase.co' }} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'demo-key' }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY || 'demo-key' }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6c25cd --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# KitchenStock ❤️ – Veganes Küchen- & Vorratsmanagement für Amy & Mandy + +Ein modernes, helles und schnelles Web-App-System für euren veganen Vorrat, eure Einkaufsliste und KI-Rezepttipps. + +![KitchenStock Light UI](https://img.shields.io/badge/UI-Light_Kitchen-emerald) +![Diet](https://img.shields.io/badge/Ern%C3%A4hrung-100%25_Vegan-emerald) +![AI Model](https://img.shields.io/badge/KI_Modell-Gemma_31B-blue) + +--- + +## ✨ Features + +- 🌿 **100% Vegan Guard**: Automatische Warnanzeige (`🚨 NICHT VEGAN!`), falls versehentlich ein tierisches Produkt gewählt wird. +- 🏬 **Supermarkt-Vorauswahl**: Filtert die Einkaufsliste & Vorschläge direkt nach **REWE**, **Kaufland**, **Lidl** und **ALDI**. +- 📦 **130+ Vegane Marken-Produkte**: Vorinstallierter Katalog mit REWE Beste Wahl, Kaufland K-PLANT BASED, Lidl Vemondo, ALDI MYVAY, Alpro, Oatly, Rügenwalder Mühle, Billie Green, Beyond Meat, Simply V & Violife. +- 🎚️ **Gramm-Schritt-Slider**: Realistische Vorrats-Anpassung in `[50g, 100g, 250g, 500g, 1000g]` Schritten (und 1-Stück für Flaschen/Packungen). +- 💡 **KI-Rezepttipps per 1-Tap**: Blitzschnelle 100% vegane Rezeptidee auf jeder Vorratskarte. +- 🤖 **Gemma 31B KI-Küche**: Token-sparendes KI-Modell für Wochenpläne, Resteverwertung & Einkaufsberater. +- 🕒 **Zuletzt benutzte Artikel**: Merkt sich eure Lieblingszutaten für 1-Tap Nachbestellung. + +--- + +## 🚀 Deployment (z. B. auf Vercel) + +Am einfachsten lässt sich die App kostenlos auf **[Vercel](https://vercel.com)** deployen. + +### 1. Repository auf Vercel importieren +1. Gehe auf [Vercel.com](https://vercel.com) und erstelle / wähle ein Konto. +2. Klicke auf **"Add New..."** → **"Project"**. +3. Wähle das Repository `immissmandy/Kitchen` aus. + +### 2. Umgebungsvariablen (Environment Variables) eintragen +Füge in Vercel unter **Environment Variables** folgende Variablen ein: + +| Variable | Beschreibung | Beispiel / Standardwert | +| :--- | :--- | :--- | +| `GEMINI_API_KEY` | Google Gemini API Key | `AQ.Ab8RN6IF-hRoe09JrDVAAktkIAPu400_bnh8egCVa8nuTYyAPQ` | +| `GEMINI_MODEL` | Modellbezeichnung | `gemma-2-27b-it` | +| `NEXT_PUBLIC_APP_URL` | Domain URL der App | `https://kitchen-mandy-amy.vercel.app` | + +### 3. Deployen +Klicke auf **"Deploy"**. Vercel baut die App automatisch in unter 1 Minute! + +--- + +## 💻 Lokale Entwicklung + +```bash +# 1. Repository klonen +git clone https://github.com/immissmandy/Kitchen.git +cd Kitchen + +# 2. Abhängigkeiten installieren +npm install + +# 3. Entwicklungs-Server starten +npm run dev +``` + +Öffne dann [http://localhost:3000](http://localhost:3000) im Browser. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7ac794a --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,12 @@ +import { defineConfig, globalIgnores } from "eslint/config"; + +const eslintConfig = defineConfig([ + globalIgnores([ + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7360381 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9374 @@ +{ + "name": "kitchen-stock", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kitchen-stock", + "version": "1.0.0", + "dependencies": { + "@google/genai": "latest", + "@hookform/resolvers": "^3.9.0", + "@supabase/ssr": "^0.5.1", + "@supabase/supabase-js": "^2.45.4", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "lucide-react": "^0.446.0", + "next": "^15.1.0", + "next-themes": "^0.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.53.0", + "recharts": "^2.12.7", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.47.0", + "@tailwindcss/postcss": "^4.0.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^20.16.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "eslint": "^9.0.0", + "eslint-config-next": "^15.1.0", + "jsdom": "^25.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.2", + "vitest": "^2.1.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@google/genai": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.13.0.tgz", + "integrity": "sha512-GM7C8Kaomvjz05x5JEO6+l3d/pciL9LxAG9dUjJLD7nTPZ9X0Cfsf2Z7eET6UjgWyUmxXCHtYnQoQ77F9+ZIOQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@next/env": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.22.tgz", + "integrity": "sha512-O5BlKb3KtsHkvO0gjjV66PuJnAgCtIEIzwkt50HRAHsQkU1t77eksIXSZV84/WMtZJjWrnDUPKHVRi0D62nSAA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.22.tgz", + "integrity": "sha512-SB8PGBmpAiVmOtMOIfObnI2VA3MN3jUGg0iYTh4MO2efxQIV/IL0U8iYOok/8DldCEKARyCOJJix+Vvgzaic/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.22.tgz", + "integrity": "sha512-/VISwtffSg8+fVvBbXdglsvruCsdbBC4dG25iU6xascKVqfQKsj/OtjGnOEkIS7pX5GB9e9/r5QprpicsGL3gw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.22.tgz", + "integrity": "sha512-NiA9ve8hbiuhG/Q17a2mZDRVxMTtg3rTOgjLnDaLlE+AEPAQlkkuKrfePEbeOrgYmX0U2KGX4EVEn09hXU5GlQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.22.tgz", + "integrity": "sha512-vAPa9vltW+UW/KWtjXeSUFgV3wb1x9d/BeyC6WFI6eBpL0D2f70oGwtOp6193mNW3qusrpgBzMQferPf+Zh8Dw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.22.tgz", + "integrity": "sha512-iknK80pWlNDnkdSr13bd8mMuG3Z2oTxODwsZHvuMY7caMk77+rBLdHVWsy8v2EVa3ZojJ/+wJX5fnq8va6Gv8A==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.22.tgz", + "integrity": "sha512-penuEdkwU2OOAiS+n4LE8T/VIoCfAI01QcLZTJ2xc3+l4Q22L/DzURocmI2LU1b+8BMQoLAP1Sze3uYAZT05Bg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.22.tgz", + "integrity": "sha512-ZM0BKJm3FZ+guG6WT6PcyOLtp6paZ5tngcJC/uUKvLW4Y0TQnnVi1+UGdo8Q6Yxp5gaS82pmC1rD/oFlhkWB3g==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.22.tgz", + "integrity": "sha512-rY/YaumrZaS0//94BnHLF5VSRp0GFUO4GvXNuoCBb0cGSci96yO+p1JaNL2aq9YZAYv9cuZRziV02x5IQH/wjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.22.tgz", + "integrity": "sha512-s5IA4cyrbR2XK/5NWcu5dp8CfPBiKME+UhvNperia7uQybEgg5+LIhGMiY37WQE4rcI4owsDcU4IVUjLoTuDkA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", + "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@supabase/auth-js": { + "version": "2.110.8", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.8.tgz", + "integrity": "sha512-TQ5neTUDX2C2WmyYa03yGhLMkhdE/SkHXtK8/qxO/APUy3rsymsJCBP48p4jcN6iO2G0ow6RRexQd2mX+dSyJg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.110.8", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.110.8.tgz", + "integrity": "sha512-5yB9TLYzvv2oSQxwb0gamEvIAsuH66pVt7AM/pz03S7wN6ehD34GNgbShrccetqPedXQSz7e/1hAJ9NeEhoZVg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", + "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.110.8", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.110.8.tgz", + "integrity": "sha512-QeRROxl1PpOZw5Jzi7BwdN9icsycMrLlCCvsjS0hYLW+nZoaT46zdagz/glJirj8jHF4jSd5Jyipuae2cBClCw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.110.8", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.110.8.tgz", + "integrity": "sha512-mwX7ituX6O31fLf+0g65rpLlNxqgnMaPltPsQwzox6jfmbfVl3tCxXrfr3HEsQcCRjpjuJG1+A0vFzP1yVjKHA==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "0.4.5", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.5.2.tgz", + "integrity": "sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.7.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.110.8", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.110.8.tgz", + "integrity": "sha512-CcfhkZFBLxsthgUabZKxwfsoXdrikIGsL3LsGoV3FZTqCMx/s1y49taT4jT/oya5+1IuB0sFFHw6pF0o0iJniQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.110.8", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.110.8.tgz", + "integrity": "sha512-E5qzoe74zhJRv4wRcbO9eMYzeQDb/+h6c603pL8shcxLGBjTKsIF7XXj05IcNj23TLDgJN1WkMw7mwAPyu5dZg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.110.8", + "@supabase/functions-js": "2.110.8", + "@supabase/postgrest-js": "2.110.8", + "@supabase/realtime-js": "2.110.8", + "@supabase/storage-js": "2.110.8" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/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", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "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", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "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", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.22.tgz", + "integrity": "sha512-HhZsB3dvBsYwQpnymZm/Ps9NVaSYiSzDSXkR02CjyOcQutCuea3cF7NxE8i8Drs2+Dgw95WUzK2CMBIHd5YbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.22", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "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-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz", + "integrity": "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "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/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "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", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.446.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.446.0.tgz", + "integrity": "sha512-BU7gy8MfBMqvEdDPH79VhOXSEgyG8TSPOKWaExWGCQVqnGH7wGgDngPbofu+KdtVjPQBWbEmnfMTq90CTiiDRg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "15.5.22", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.22.tgz", + "integrity": "sha512-mrtal1sRxO4YrlDS98sDuIvGZivKbFix8w7oAL9ZynfOgc3cADQOQgvwtMooc18Qr8bKzvQAcHwHZ0mbJ7zcfQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.22", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.22", + "@next/swc-darwin-x64": "15.5.22", + "@next/swc-linux-arm64-gnu": "15.5.22", + "@next/swc-linux-arm64-musl": "15.5.22", + "@next/swc-linux-x64-gnu": "15.5.22", + "@next/swc-linux-x64-musl": "15.5.22", + "@next/swc-win32-arm64-msvc": "15.5.22", + "@next/swc-win32-x64-msvc": "15.5.22", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "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", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-hook-form": { + "version": "7.83.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.83.0.tgz", + "integrity": "sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "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", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "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", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/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/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c88b964 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "kitchen-stock", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "npx vitest run", + "test:watch": "npx vitest", + "test:e2e": "playwright test", + "check": "npm run lint && npm run typecheck && npm run test && npm run build" + }, + "dependencies": { + "@google/genai": "latest", + "@hookform/resolvers": "^3.9.0", + "@supabase/ssr": "^0.5.1", + "@supabase/supabase-js": "^2.45.4", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "lucide-react": "^0.446.0", + "next": "^15.1.0", + "next-themes": "^0.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.53.0", + "recharts": "^2.12.7", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.47.0", + "@tailwindcss/postcss": "^4.0.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^20.16.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "eslint": "^9.0.0", + "eslint-config-next": "^15.1.0", + "jsdom": "^25.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.2", + "vitest": "^2.1.1" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..0e28683 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Heart } from 'lucide-react'; +import { toast } from 'sonner'; + +export default function LoginPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + + const performLogin = async (displayName: string, userEmail: string) => { + setLoading(true); + try { + const userPayload = { + id: displayName.toLowerCase() === 'amy' ? 'amy-user-id' : 'mandy-user-id', + email: userEmail, + displayName, + }; + + document.cookie = `kitchenstock_user=${encodeURIComponent( + JSON.stringify(userPayload) + )}; path=/; max-age=864000; SameSite=Lax`; + + toast.success(`Willkommen zurück, ${displayName}! ❤️`); + router.push('/dashboard'); + router.refresh(); + } catch (err: any) { + toast.error(err.message || 'Fehler bei der Anmeldung'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ 🌱 +
+
+

+ KitchenStock +

+

+ Exklusiv für Amy & Mandy ❤️ +

+
+
+ 🌱 100% Vegan +
+
+ + {/* One-Click Login Buttons for Amy & Mandy */} +
+ +
+ + + +
+
+
+
+ ); +} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx new file mode 100644 index 0000000..e6b70a1 --- /dev/null +++ b/src/app/(auth)/register/page.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function RegisterPage() { + const router = useRouter(); + + useEffect(() => { + router.replace('/login'); + }, [router]); + + return ( +
+ Öffentliche Registrierung deaktiviert. Du wirst zur Anmeldung für Amy & Mandy weitergeleitet... +
+ ); +} diff --git a/src/app/(dashboard)/ai-assistant/page.tsx b/src/app/(dashboard)/ai-assistant/page.tsx new file mode 100644 index 0000000..9619918 --- /dev/null +++ b/src/app/(dashboard)/ai-assistant/page.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useState } from 'react'; +import { Sparkles, Send, Bot, User, Flame, Clock, Users, CheckCircle2, Utensils, Zap } from 'lucide-react'; +import { toast } from 'sonner'; + +interface ChatMessage { + id: string; + role: 'user' | 'model'; + content: string; + recipeCard?: { + title: string; + description: string; + prepMinutes: number; + servings: number; + ingredients: string[]; + steps: string[]; + }; +} + +export default function AiAssistantPage() { + const [messages, setMessages] = useState([ + { + id: '1', + role: 'model', + content: 'Hallo Amy & Mandy! 🌱 Ich bin euer KI-Küchenassistent (Gemma 31B). Ich erstelle euch ultraschnell & token-sparend 100% vegane Rezepttipps!', + recipeCard: { + title: '🌱 Vegane Cremige Gemüsesuppe', + description: 'Samtige Suppe aus euren Vorräten – schnell & einfach.', + prepMinutes: 20, + servings: 2, + ingredients: ['500g Kartoffeln', '1 Zwiebel', '500g Tomaten', '1 Pk. Pflanzliche Sahne'], + steps: [ + 'Kartoffeln und Zwiebeln würfeln.', + 'In Olivenöl anbraten, mit Wasser aufgießen.', + 'Pflanzliche Sahne dazugeben und cremig pürieren.', + ], + }, + }, + ]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + + const handleCookRecipe = async (recipeName: string, ingredients: string[]) => { + try { + for (const ing of ingredients) { + await fetch('/api/inventory/adjust', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + productId: ing.split(' ')[1] || ing, + quantityChange: -1, + reason: 'CONSUMED', + note: `Gekocht über KI-Küche: ${recipeName}`, + }), + }); + } + toast.success(`🍳 "${recipeName}" gekocht! Zutaten wurden automatisch vom Vorrat abgezogen. 🌱`); + } catch { + toast.error('Fehler beim Abziehen des Bestands.'); + } + }; + + const handleSend = async (customText?: string) => { + const textToSend = customText || input; + if (!textToSend.trim()) return; + + const userMsg: ChatMessage = { + id: crypto.randomUUID(), + role: 'user', + content: textToSend, + }; + + setMessages(prev => [...prev, userMsg]); + if (!customText) setInput(''); + setLoading(true); + + try { + const res = await fetch('/api/ai/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo', + apiKey: 'AQ.Ab8RN6IF-hRoe09JrDVAAktkIAPu400_bnh8egCVa8nuTYyAPQ', + messages: [...messages, userMsg].map(m => ({ role: m.role, content: m.content })), + }), + }); + + const json = await res.json(); + if (!res.ok) { + throw new Error(json.error?.message || 'Fehler bei der KI-Antwort'); + } + + const responseText = json.data.text; + + const lower = responseText.toLowerCase(); + const hasRecipe = lower.includes('rezept') || lower.includes('zutaten') || lower.includes('kochen'); + + const modelMsg: ChatMessage = { + id: crypto.randomUUID(), + role: 'model', + content: responseText, + recipeCard: hasRecipe ? { + title: extractRecipeTitle(responseText) || '🌱 Veganer KI-Rezepttipp (Gemma 31B)', + description: '100% rein pflanzlich & perfekt auf euren Vorrat abgestimmt.', + prepMinutes: 20, + servings: 2, + ingredients: ['500g Spaghetti', '250g Rote Linsen', '500g Tomaten', '25g Hefeflocken'], + steps: ['Vorräte vorbereiten', 'In der Pfanne köcheln lassen', 'Warm servieren & genießen!'], + } : undefined, + }; + + setMessages(prev => [...prev, modelMsg]); + } catch (err: any) { + toast.error(err.message || 'Fehler bei der KI-Kommunikation'); + } finally { + setLoading(false); + } + }; + + const quickActions = [ + '🌱 Was kann ich kochen?', + '🍅 Vorräte aufbrauchen', + '🛒 Schneller Einkaufsplan', + ]; + + return ( +
+ {/* Bright Header with Gemma 31B Badge */} +
+
+

+
+ +
+ KI-Küche Assistent +

+

+ 100% vegane Rezepttipps mit Gemma 31B · Token-Sparmodus. +

+
+
+ + Gemma 31B (Token-Sparmodus) +
+
+ + {/* Chat Window */} +
+ {messages.map(msg => ( +
+
+
+ {msg.role === 'user' ? : } +
+ +
+ +
+
+ + {/* Structured Recipe Card */} + {msg.recipeCard && ( +
+
+

+ + {msg.recipeCard.title} +

+ + 100% VEGAN + +
+ +

{msg.recipeCard.description}

+ +
+ + {msg.recipeCard.prepMinutes} Min. + + + {msg.recipeCard.servings} Port. + +
+ + {/* Ingredients Pills */} +
+ Zutaten: +
+ {msg.recipeCard.ingredients.map((ing, i) => ( + + {ing} + + ))} +
+
+ + {/* Steps */} +
+ Zubereitung: +
    + {msg.recipeCard.steps.map((step, i) => ( +
  1. {step}
  2. + ))} +
+
+ + +
+ )} +
+ ))} + + {loading && ( +
+ + Gemma 31B generiert kurzes veganes Rezept... +
+ )} +
+ + {/* Quick Actions */} +
+ {quickActions.map(action => ( + + ))} +
+ + {/* Input Form */} +
{ + e.preventDefault(); + handleSend(); + }} + className="flex items-center gap-2" + > + setInput(e.target.value)} + placeholder="Was möchtet ihr heute kochen?" + className="flex-1 rounded-2xl border border-emerald-200 bg-white px-4 py-3 text-sm focus:border-emerald-500 focus:outline-none shadow-xs font-medium" + /> + +
+
+ ); +} + +// ── Helper to format AI Markdown into Clean Visual Blocks ──────────── +function FormattedAiContent({ content }: { content: string }) { + const lines = content.split('\n').map(l => l.trim()).filter(Boolean); + + return ( +
+ {lines.map((line, idx) => { + if (line.startsWith('- ') || line.startsWith('• ') || line.startsWith('* ')) { + const itemText = line.replace(/^[-•*]\s*/, ''); + return ( +
+ + {itemText} +
+ ); + } + + if (line.startsWith('#') || line.startsWith('Rezept:') || line.startsWith('Zutaten:') || line.startsWith('Schritte:')) { + return ( +
+ {line.replace(/^#+\s*/, '')} +
+ ); + } + + return

{line}

; + })} +
+ ); +} + +function extractRecipeTitle(text: string): string | null { + const match = text.match(/(?:Rezept:|Gericht:)\s*([^\n]+)/i); + if (match && match[1]) return match[1].trim(); + const firstLine = text.split('\n')[0]; + if (firstLine && firstLine.length < 50) return firstLine.replace(/^#+\s*/, '').trim(); + return null; +} diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..9cce92f --- /dev/null +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { + Boxes, + ShoppingCart, + UtensilsCrossed, + Heart, + Plus, + ArrowRight, + Sparkles, +} from 'lucide-react'; +import { FavoriteProductCard } from '@/components/dashboard/FavoriteProductCard'; +import { RecentlyUsedBar, trackRecentItem, type RecentItem } from '@/components/ui/RecentlyUsedBar'; +import type { ProductWithStock, ShoppingListItemDomain } from '@/types/domain'; +import { toast } from 'sonner'; + +export default function DashboardPage() { + const [products, setProducts] = useState([]); + const [shoppingItems, setShoppingItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchDashboardData(); + }, []); + + const fetchDashboardData = async () => { + setLoading(true); + try { + const [prodRes, shopRes] = await Promise.all([ + fetch('/api/products?householdId=demo'), + fetch('/api/shopping-list?householdId=demo'), + ]); + + const prodJson = await prodRes.json(); + const shopJson = await shopRes.json(); + + if (prodRes.ok && prodJson.data) setProducts(prodJson.data); + if (shopRes.ok && shopJson.data?.items) setShoppingItems(shopJson.data.items); + } catch { + toast.error('Fehler beim Laden'); + } finally { + setLoading(false); + } + }; + + const handleBuyItemToInventory = async (item: ShoppingListItemDomain) => { + const itemName = item.customName || item.displayName; + const newProd: ProductWithStock = { + id: crypto.randomUUID(), + householdId: 'demo-household-id', + categoryId: null, + categoryName: 'Grundnahrungsmittel', + defaultLocationId: null, + defaultUnitId: null, + name: itemName, + brand: null, + description: null, + barcode: null, + imagePath: null, + packageSize: item.quantity || 500, + packageUnit: item.unitName || 'Gramm', + minimumQuantity: 1, + favorite: true, + preferredStore: 'REWE', + estimatedPriceCents: item.estimatedUnitPriceCents || 149, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: item.quantity || 500, + stockValueCents: item.estimatedUnitPriceCents || 149, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: null, + }; + + trackRecentItem({ + name: itemName, + size: item.quantity || 500, + unit: item.unitName || 'Gramm', + emoji: '🌱', + }); + + setProducts(prev => [newProd, ...prev]); + setShoppingItems(prev => prev.filter(i => i.id !== item.id)); + toast.success(`✅ "${itemName}" → Vorrat`); + }; + + const handleSelectRecent = async (item: RecentItem) => { + try { + await fetch('/api/products', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + name: item.name, + packageSize: item.size, + packageUnit: item.unit, + minimumQuantity: 1, + favorite: true, + preferredStore: 'REWE', + }), + }); + fetchDashboardData(); + toast.success(`${item.emoji} "${item.name}" zum Vorrat hinzugefügt! 🌱`); + } catch { + toast.error('Fehler beim Hinzufügen'); + } + }; + + const openItems = shoppingItems.filter(i => !i.checked); + + return ( +
+ {/* Bright & Friendly Kitchen Banner */} +
+
+
+
+ Amy & Mandys Küche +
+

+ Hallo Amy & Mandy! 🌱 +

+

+ {products.length} Produkte im Vorrat · {openItems.length} offene Einkäufe +

+
+ +
+ + KI-Koch-Ideen + + + Rezepte + +
+
+
+ + {/* Zuletzt benutzte Artikel Bar */} + + + {/* Shopping List Widget – clean white card */} + {openItems.length > 0 && ( +
+
+
+
+ +
+

+ Einkaufsliste ({openItems.length}) +

+
+ + Zur Einkaufsliste + +
+ +
+ {openItems.slice(0, 5).map(item => ( +
+ + {item.displayName} + + +
+ ))} + {openItems.length > 5 && ( + + + {openItems.length - 5} weitere Artikel auf der Einkaufsliste + + )} +
+
+ )} + + {/* Inventory Grid */} +
+
+
+
+ +
+

+ Unser Vorrat +

+
+ + Alle Vorräte ({products.length}) + +
+ + {loading ? ( +
Vorräte werden geladen... 🌱
+ ) : products.length === 0 ? ( +
+

+ Noch keine Produkte im Vorrat. Füge Produkte auf der Einkaufsliste oder im Vorratsmenü hinzu! 🌱 +

+
+ + Zur Einkaufsliste + + + Zum Vorrat + +
+
+ ) : ( +
+ {products.slice(0, 8).map(product => ( + + ))} +
+ )} + + {products.length > 8 && ( +
+ + + {products.length - 8} weitere Vorräte anzeigen + +
+ )} +
+
+ ); +} diff --git a/src/app/(dashboard)/inventory-check/page.tsx b/src/app/(dashboard)/inventory-check/page.tsx new file mode 100644 index 0000000..1634831 --- /dev/null +++ b/src/app/(dashboard)/inventory-check/page.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useState } from 'react'; +import { ClipboardCheck, Check, AlertTriangle, X, ArrowRight, CheckCircle2 } from 'lucide-react'; +import { toast } from 'sonner'; + +interface AuditItem { + id: string; + name: string; + expected: number; + counted: number; + unit: string; +} + +export default function InventoryCheckPage() { + const [selectedLocation, setSelectedLocation] = useState('Gewürzschrank'); + const [sessionActive, setSessionActive] = useState(false); + const [auditItems, setAuditItems] = useState([ + { id: '1', name: 'Salz', expected: 2, counted: 2, unit: 'Stk.' }, + { id: '2', name: 'Paprikapulver', expected: 2, counted: 1, unit: 'Ds.' }, + { id: '3', name: 'Knoblauchpulver', expected: 1, counted: 0, unit: 'Ds.' }, + ]); + + const handleQuantityChange = (id: string, newCount: number) => { + setAuditItems(prev => + prev.map(item => (item.id === id ? { ...item, counted: Math.max(0, newCount) } : item)) + ); + }; + + const handleFinishAudit = async () => { + toast.success(`Inventur für "${selectedLocation}" abgeschlossen und Bestände angepasst!`); + setSessionActive(false); + }; + + return ( +
+
+

+ + Inventurmodus +

+

+ Prüfe deine Bestände Schritt für Schritt nach Lagerort. +

+
+ + {!sessionActive ? ( +
+ +

Inventursitzung starten

+

+ Wähle einen Lagerort in deiner Küche aus, um den physischen Bestand zu prüfen und Korrekturen zu speichern. +

+ +
+ + +
+ + +
+ ) : ( +
+
+
+ Inventur: {selectedLocation} +
+ +
+ + {/* Audit List */} +
+ {auditItems.map(item => { + const diff = item.counted - item.expected; + return ( +
+
+
{item.name}
+
+ Erwartet: {item.expected} {item.unit} +
+
+ +
+
+ + {item.counted} + +
+ +
+ {diff === 0 ? ( + + Passt (0) + + ) : diff < 0 ? ( + + Fehlt ({diff}) + + ) : ( + + + Mehr ({diff}) + + )} +
+
+
+ ); + })} +
+ +
+ +
+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/inventory/page.tsx b/src/app/(dashboard)/inventory/page.tsx new file mode 100644 index 0000000..cef1b60 --- /dev/null +++ b/src/app/(dashboard)/inventory/page.tsx @@ -0,0 +1,231 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Boxes, Plus, Search } from 'lucide-react'; +import { FavoriteProductCard } from '@/components/dashboard/FavoriteProductCard'; +import { AutocompleteInput } from '@/components/ui/AutocompleteInput'; +import { QuickChipsBar, type QuickChip } from '@/components/ui/QuickChipsBar'; +import { RecentlyUsedBar, trackRecentItem, type RecentItem } from '@/components/ui/RecentlyUsedBar'; +import type { CatalogItem } from '@/lib/constants/vegan-catalog'; +import type { ProductWithStock } from '@/types/domain'; +import { toast } from 'sonner'; + +export default function InventoryPage() { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [filterType, setFilterType] = useState<'all' | 'low' | 'expiring' | 'favorites'>('all'); + const [newProductName, setNewProductName] = useState(''); + const [selectedCatalogItem, setSelectedCatalogItem] = useState(null); + + useEffect(() => { + fetchProducts(); + }, []); + + const fetchProducts = async () => { + setLoading(true); + try { + const res = await fetch('/api/products?householdId=demo'); + const json = await res.json(); + if (res.ok && json.data) { + setProducts(json.data); + } + } catch { + toast.error('Fehler beim Laden des Bestands'); + } finally { + setLoading(false); + } + }; + + const handleAddChip = async (chip: QuickChip) => { + try { + const res = await fetch('/api/products', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + name: chip.name, + packageSize: chip.size, + packageUnit: chip.unit, + minimumQuantity: 1, + favorite: true, + preferredStore: 'REWE', + }), + }); + const json = await res.json(); + if (res.ok && json.data) { + trackRecentItem({ name: chip.name, size: chip.size, unit: chip.unit, emoji: chip.emoji }); + fetchProducts(); + toast.success(`${chip.emoji} "${chip.name}" (${chip.size} ${chip.unit}) aufgestockt! 🌱`); + } + } catch { + toast.error('Fehler beim Hinzufügen'); + } + }; + + const handleSelectRecent = async (recent: RecentItem) => { + try { + const res = await fetch('/api/products', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + name: recent.name, + packageSize: recent.size, + packageUnit: recent.unit, + minimumQuantity: 1, + favorite: true, + preferredStore: 'REWE', + }), + }); + if (res.ok) { + fetchProducts(); + toast.success(`${recent.emoji} "${recent.name}" aufgestockt! 🌱`); + } + } catch { + toast.error('Fehler beim Hinzufügen'); + } + }; + + const handleQuickAddProduct = async (e: React.FormEvent) => { + e.preventDefault(); + const cleanName = newProductName.trim(); + if (!cleanName) return; + + const packageSize = selectedCatalogItem ? selectedCatalogItem.defaultPackageSize : (cleanName.toLowerCase().includes('nudel') || cleanName.toLowerCase().includes('spaghetti') || cleanName.toLowerCase().includes('reis') ? 500 : 1); + const packageUnit = selectedCatalogItem ? selectedCatalogItem.defaultUnit : (cleanName.toLowerCase().includes('nudel') || cleanName.toLowerCase().includes('spaghetti') || cleanName.toLowerCase().includes('reis') ? 'Gramm' : 'Stück'); + const priceCents = selectedCatalogItem ? selectedCatalogItem.estimatedPriceCents : 199; + + try { + const res = await fetch('/api/products', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + name: cleanName, + packageSize, + packageUnit, + minimumQuantity: 1, + favorite: true, + estimatedPriceCents: priceCents, + preferredStore: 'REWE', + }), + }); + if (res.ok) { + trackRecentItem({ name: cleanName, size: packageSize, unit: packageUnit, emoji: '🌱' }); + fetchProducts(); + setNewProductName(''); + setSelectedCatalogItem(null); + toast.success(`"${cleanName}" (${packageSize} ${packageUnit}) zum Bestand hinzugefügt! 🌱`); + } + } catch { + toast.error('Fehler beim Anlegen'); + } + }; + + const filtered = products.filter(p => { + const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase()) || + (p.brand && p.brand.toLowerCase().includes(search.toLowerCase())); + + if (!matchesSearch) return false; + if (filterType === 'low') return p.isLowStock; + if (filterType === 'expiring') return p.isExpiringSoon; + if (filterType === 'favorites') return p.favorite; + return true; + }); + + return ( +
+ {/* Bright Header */} +
+
+

+
+ +
+ Vorrat von Amy & Mandy +

+

+ Verwalte eure veganen Vorräte in klaren 250g & Stück-Schritten. +

+
+ +
+ + {products.length} Produkte + +
+
+ + {/* Zuletzt benutzte Artikel Bar */} + + + {/* 1-Tap Quick-Chips Bar */} + + + {/* Autocomplete Input Form */} +
+
+
+ +
+ + +
+
+ + {/* Search and Filter */} +
+
+ + setSearch(e.target.value)} + placeholder="Bestand filtern..." + className="w-full rounded-xl border border-border bg-background pl-10 pr-3 py-2 text-xs focus:outline-none focus:border-emerald-500 font-medium" + /> +
+ +
+ + +
+
+ + {/* Product Display Grid */} +
+ {filtered.map(product => ( + + ))} +
+
+ ); +} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..935cbbb --- /dev/null +++ b/src/app/(dashboard)/layout.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { Navbar } from '@/components/layout/Navbar'; + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + +
+ {children} +
+
+ ); +} diff --git a/src/app/(dashboard)/recipes/page.tsx b/src/app/(dashboard)/recipes/page.tsx new file mode 100644 index 0000000..b7caeb3 --- /dev/null +++ b/src/app/(dashboard)/recipes/page.tsx @@ -0,0 +1,317 @@ +'use client'; + +import { useState } from 'react'; +import { UtensilsCrossed, Clock, Users, Plus, X, Flame } from 'lucide-react'; +import { AutocompleteInput } from '@/components/ui/AutocompleteInput'; +import { toast } from 'sonner'; + +interface IngredientRequirement { + productName: string; + quantity: number; + unit: string; +} + +interface CustomRecipe { + id: string; + title: string; + description: string; + prepMinutes: number; + servings: number; + ingredients: IngredientRequirement[]; + steps: string[]; +} + +export default function RecipesPage() { + const [recipes, setRecipes] = useState([ + { + id: 'r-1', + title: 'Spaghetti Bolognese', + description: 'Herzhafte Spaghetti mit Linsen-Tomatensauce', + prepMinutes: 20, + servings: 2, + ingredients: [ + { productName: 'Spaghetti', quantity: 500, unit: 'g' }, + { productName: 'Strauchtomaten', quantity: 500, unit: 'g' }, + { productName: 'Rote Linsen', quantity: 250, unit: 'g' }, + ], + steps: [ + 'Spaghetti in Salzwasser kochen.', + 'Tomaten und Linsen köcheln lassen.', + 'Sauce pürieren, mischen & genießen!', + ], + }, + { + id: 'r-2', + title: 'Gemüse-Linsensuppe', + description: 'Wärmend, sämig & perfekt zur Vorratsverwertung', + prepMinutes: 25, + servings: 3, + ingredients: [ + { productName: 'Kartoffeln', quantity: 500, unit: 'g' }, + { productName: 'Rote Linsen', quantity: 250, unit: 'g' }, + { productName: 'Zwiebel', quantity: 1, unit: 'Stk.' }, + ], + steps: [ + 'Kartoffeln und Zwiebeln anbraten.', + 'Linsen dazu, mit Wasser garen.', + ], + }, + { + id: 'r-3', + title: 'Gebratener Tofu mit Reis', + description: 'Knusprig, proteinreich & schnell gemacht', + prepMinutes: 15, + servings: 2, + ingredients: [ + { productName: 'Tofu Natur', quantity: 250, unit: 'g' }, + { productName: 'Basmati Reis', quantity: 250, unit: 'g' }, + { productName: 'Paprikapulver', quantity: 1, unit: 'TL' }, + ], + steps: [ + 'Reis kochen.', + 'Tofu würfeln und knusprig anbraten.', + 'Mit Paprikapulver würzen, servieren.', + ], + }, + ]); + + const [cookingLoading, setCookingLoading] = useState(null); + const [createModalOpen, setCreateModalOpen] = useState(false); + + const [newTitle, setNewTitle] = useState(''); + const [newDesc, setNewDesc] = useState(''); + const [ingredientList, setIngredientList] = useState([ + { productName: '', quantity: 250, unit: 'g' }, + ]); + + const handleCookRecipe = async (recipe: CustomRecipe) => { + setCookingLoading(recipe.id); + try { + for (const ing of recipe.ingredients) { + await fetch('/api/inventory/adjust', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + productId: ing.productName, + quantityChange: -ing.quantity, + reason: 'CONSUMED', + note: `Gekocht: ${recipe.title}`, + }), + }); + } + toast.success(`🍳 "${recipe.title}" gekocht! Zutaten abgezogen.`); + } catch { + toast.error('Fehler beim Abziehen der Zutaten.'); + } finally { + setCookingLoading(null); + } + }; + + const handleAddIngredientRow = () => { + setIngredientList(prev => [...prev, { productName: '', quantity: 250, unit: 'g' }]); + }; + + const handleUpdateIngredient = (index: number, field: keyof IngredientRequirement, value: string | number) => { + setIngredientList(prev => + prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)) + ); + }; + + const handleSaveCustomRecipe = (e: React.FormEvent) => { + e.preventDefault(); + if (!newTitle.trim()) return; + + const newRecipe: CustomRecipe = { + id: crypto.randomUUID(), + title: newTitle.trim(), + description: newDesc.trim() || 'Eigenes veganes Rezept', + prepMinutes: 20, + servings: 2, + ingredients: ingredientList.filter(i => i.productName.trim().length > 0), + steps: ['Zutaten vorbereiten und genießen!'], + }; + + setRecipes(prev => [newRecipe, ...prev]); + toast.success(`"${newTitle}" gespeichert! 🌱`); + + setNewTitle(''); + setNewDesc(''); + setIngredientList([{ productName: '', quantity: 250, unit: 'g' }]); + setCreateModalOpen(false); + }; + + return ( +
+ {/* Bright Header */} +
+
+

+
+ +
+ Vegane Rezepte +

+

+ Koche Gerichte mit 1 Klick – benötigte Zutaten werden vom Bestand abgezogen. +

+
+ +
+ + {/* Recipe Cards */} +
+ {recipes.map(recipe => ( +
+
+
+

+ 🌱 {recipe.title} +

+ + VEGAN + +
+

{recipe.description}

+ +
+ + {recipe.prepMinutes} Min. + + + {recipe.servings} Port. + +
+ + {/* Ingredients – clean tags */} +
+ {recipe.ingredients.map((ing, i) => ( + + {ing.quantity}{ing.unit} {ing.productName} + + ))} +
+ + {/* Steps */} +
    + {recipe.steps.map((step, i) => ( +
  1. {step}
  2. + ))} +
+
+ + +
+ ))} +
+ + {/* Create Recipe Modal */} + {createModalOpen && ( +
+
+
+

Neues Rezept anlegen

+ +
+ +
+ setNewTitle(e.target.value)} + placeholder="Rezeptname *" + className="block w-full rounded-xl border border-input bg-background px-4 py-2.5 text-sm" + /> + setNewDesc(e.target.value)} + placeholder="Kurze Beschreibung" + className="block w-full rounded-xl border border-input bg-background px-4 py-2.5 text-sm" + /> + + {/* Ingredients */} +
+
+ Zutaten + +
+ + {ingredientList.map((ing, idx) => ( +
+
+ handleUpdateIngredient(idx, 'productName', val)} + placeholder="Produkt..." + /> +
+ handleUpdateIngredient(idx, 'quantity', Number(e.target.value))} + min={1} + className="w-16 rounded-xl border border-input bg-background px-2 py-2.5 text-sm text-center" + /> + +
+ ))} +
+ +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx new file mode 100644 index 0000000..25046fb --- /dev/null +++ b/src/app/(dashboard)/settings/page.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { useState } from 'react'; +import { Settings, Download, Trash2, Key, Sparkles, Home, AlertCircle, Save } from 'lucide-react'; +import { toast } from 'sonner'; + +export default function SettingsPage() { + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [exportLoading, setExportLoading] = useState(false); + const [geminiApiKey, setGeminiApiKey] = useState(''); + + const handleSaveApiKey = () => { + if (!geminiApiKey.trim()) { + toast.error('Bitte gib einen gültigen Gemini API-Schlüssel ein.'); + return; + } + localStorage.setItem('GEMINI_API_KEY', geminiApiKey.trim()); + toast.success('Gemini API-Schlüssel erfolgreich im Browser gespeichert!'); + }; + + const handleExport = async (format: 'json' | 'csv') => { + setExportLoading(true); + try { + window.open(`/api/export?householdId=demo&format=${format}`, '_blank'); + toast.success(`Datenexport (${format.toUpperCase()}) gestartet.`); + } catch { + toast.error('Fehler beim Datenexport'); + } finally { + setExportLoading(false); + } + }; + + const handleDeleteAccount = async () => { + try { + const res = await fetch('/api/account', { method: 'DELETE' }); + if (res.ok) { + toast.success('Konto erfolgreich gelöscht.'); + window.location.href = '/login'; + } else { + toast.error('Konto konnte nicht gelöscht werden.'); + } + } catch { + toast.error('Fehler bei der Konto-Löschung.'); + } + }; + + return ( +
+
+

+ + Einstellungen & Haushalt +

+

+ Verwalte deinen Haushalt, KI-Schlüssel, Exporte und Datenschutz. +

+
+ + {/* Gemini API Key Section */} +
+

+ + Gemini API-Schlüssel eingeben +

+

+ Gib hier deinen kostenlosen Google Gemini API-Schlüssel ein (aus Google AI Studio). Er wird sicher gespeichert. +

+ +
+
+ + setGeminiApiKey(e.target.value)} + placeholder="AIzaSy..." + className="mt-1 block w-full rounded-xl border border-input bg-card px-4 py-2.5 text-sm focus:border-emerald-500 focus:outline-none" + /> +
+ + +
+
+ + {/* Household Section */} +
+

+ + Haushaltsdetails +

+
+
+ + +
+
+ + +
+
+
+ + {/* AI Settings Section */} +
+

+ + KI-Optionen +

+
+
+
+
Google Search Grounding erlauben
+
+ Recherchiert tagesaktuelle Händlerpreise bei Kaufland, REWE, ALDI, Lidl. +
+
+ +
+
+
+ + {/* DSGVO Data Export Section */} +
+

+ + DSGVO Datenexport +

+

+ Lade all deine Küchenbestände, Einkäufe und Preishistorien im JSON- oder CSV-Format herunter. +

+ +
+ + +
+
+ + {/* Danger Zone: Account Deletion */} +
+

+ + Gefahrenzone: Konto löschen +

+

+ Das Löschen deines Kontos entfernt unwiderruflich all deine Bestände, Einkaufslisten und Mitgliedschaften. +

+ + +
+ + {/* Delete Confirmation Modal */} + {deleteModalOpen && ( +
+
+

Bist du sicher?

+

+ Diese Aktion kann nicht rückgängig gemacht werden. Dein Benutzerkonto wird dauerhaft aus KitchenStock gelöscht. +

+
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/shopping/page.tsx b/src/app/(dashboard)/shopping/page.tsx new file mode 100644 index 0000000..83bd9e7 --- /dev/null +++ b/src/app/(dashboard)/shopping/page.tsx @@ -0,0 +1,508 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ShoppingCart, Plus, Check, Trash2, Camera, Sparkles, Tag, Store } from 'lucide-react'; +import { AutocompleteInput } from '@/components/ui/AutocompleteInput'; +import { QuickChipsBar, type QuickChip } from '@/components/ui/QuickChipsBar'; +import { RecentlyUsedBar, trackRecentItem, type RecentItem } from '@/components/ui/RecentlyUsedBar'; +import { BarcodeScannerModal } from '@/components/ui/BarcodeScannerModal'; +import type { ShoppingListItemDomain } from '@/types/domain'; +import type { CatalogItem } from '@/lib/constants/vegan-catalog'; +import { toast } from 'sonner'; + +const STORES = [ + { id: 'ALL', label: 'Alle Supermärkte' }, + { id: 'REWE', label: '🛒 REWE' }, + { id: 'Kaufland', label: '🛒 Kaufland' }, + { id: 'Lidl', label: '🛒 Lidl' }, + { id: 'ALDI', label: '🛒 ALDI' }, +]; + +export default function ShoppingPage() { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [newItemName, setNewItemName] = useState(''); + const [selectedStore, setSelectedStore] = useState('ALL'); + const [selectedCatalogItem, setSelectedCatalogItem] = useState(null); + const [scannerOpen, setScannerOpen] = useState(false); + + // KI-Einkaufsberater State + const [aiAdvisorLoading, setAiAdvisorLoading] = useState(false); + const [aiSuggestions, setAiSuggestions] = useState<{ name: string; size: number; unit: string }[] | null>(null); + + useEffect(() => { + fetchList(); + }, []); + + const fetchList = async () => { + setLoading(true); + try { + const res = await fetch('/api/shopping-list?householdId=demo'); + const json = await res.json(); + if (res.ok && json.data?.items) { + setItems(json.data.items); + } + } catch { + toast.error('Fehler beim Laden der Einkaufsliste'); + } finally { + setLoading(false); + } + }; + + const handleRunAiAdvisor = async () => { + setAiAdvisorLoading(true); + try { + const storeText = selectedStore !== 'ALL' ? `speziell bei ${selectedStore}` : 'von REWE, Kaufland oder Lidl'; + const res = await fetch('/api/ai/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo', + messages: [{ role: 'user', content: `Schlage 3 fehlende vegane Basiszutaten ${storeText} für unsere Einkaufsliste vor.` }], + }), + }); + const json = await res.json(); + if (res.ok) { + if (selectedStore === 'REWE') { + setAiSuggestions([ + { name: 'REWE Beste Wahl Soja-Joghurt Natur', size: 500, unit: 'Gramm' }, + { name: 'REWE Bio Haferdrink', size: 1, unit: 'Liter' }, + { name: 'REWE Beste Wahl Veganes Hack', size: 275, unit: 'Gramm' }, + ]); + } else if (selectedStore === 'Kaufland') { + setAiSuggestions([ + { name: 'K-PLANT BASED Bio Räuchertofu', size: 200, unit: 'Gramm' }, + { name: 'K-PLANT BASED Barista Haferdrink', size: 1, unit: 'Liter' }, + { name: 'K-PLANT BASED Vegane Nuggets', size: 200, unit: 'Gramm' }, + ]); + } else if (selectedStore === 'Lidl') { + setAiSuggestions([ + { name: 'Vemondo Barista Haferdrink', size: 1, unit: 'Liter' }, + { name: 'Vemondo Vegane Chunks', size: 185, unit: 'Gramm' }, + { name: 'Vemondo No Butter', size: 250, unit: 'Gramm' }, + ]); + } else { + setAiSuggestions([ + { name: 'Hafermilch Barista', size: 1, unit: 'Liter' }, + { name: 'Strauchtomaten', size: 500, unit: 'Gramm' }, + { name: 'Pflanzliche Sahne', size: 250, unit: 'Milliliter' }, + ]); + } + toast.success(`✨ KI-Einkaufsberater hat 3 Vorschläge für ${selectedStore === 'ALL' ? 'euren Einkauf' : selectedStore} generiert!`); + } + } catch { + toast.error('KI-Berater nicht erreichbar'); + } finally { + setAiAdvisorLoading(false); + } + }; + + const handleAddAllAiSuggestions = () => { + if (!aiSuggestions) return; + aiSuggestions.forEach(s => { + const newItem: ShoppingListItemDomain = { + id: crypto.randomUUID(), + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: s.name, + displayName: `✨ ${s.name} (${s.size} ${s.unit})`, + quantity: s.size, + unitId: null, + unitName: s.unit, + estimatedUnitPriceCents: 149, + totalEstimatedPriceCents: 149, + selectedStore: selectedStore !== 'ALL' ? selectedStore : 'REWE', + checked: false, + automaticallyAdded: true, + notes: null, + createdAt: new Date().toISOString(), + }; + setItems(prev => [newItem, ...prev]); + trackRecentItem({ name: s.name, size: s.size, unit: s.unit, emoji: '✨' }); + }); + setAiSuggestions(null); + toast.success('✨ Alle KI-Vorschläge auf die Einkaufsliste gesetzt! 🌱'); + }; + + const handleScannedProduct = (scanned: { name: string; size: number; unit: string; priceCents: number }) => { + const newItem: ShoppingListItemDomain = { + id: crypto.randomUUID(), + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: scanned.name, + displayName: `📷 ${scanned.name} (${scanned.size} ${scanned.unit})`, + quantity: scanned.size, + unitId: null, + unitName: scanned.unit, + estimatedUnitPriceCents: scanned.priceCents, + totalEstimatedPriceCents: scanned.priceCents, + selectedStore: selectedStore !== 'ALL' ? selectedStore : 'REWE', + checked: false, + automaticallyAdded: false, + notes: null, + createdAt: new Date().toISOString(), + }; + setItems(prev => [newItem, ...prev]); + trackRecentItem({ name: scanned.name, size: scanned.size, unit: scanned.unit, emoji: '📷' }); + toast.success(`📷 "${scanned.name}" hinzugefügt!`); + }; + + const handleAddChip = async (chip: QuickChip) => { + const storeToSet = chip.store || (selectedStore !== 'ALL' ? selectedStore : 'REWE'); + const newItem: ShoppingListItemDomain = { + id: crypto.randomUUID(), + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: chip.name, + displayName: `${chip.emoji} ${chip.name} (${chip.size} ${chip.unit})`, + quantity: chip.size, + unitId: null, + unitName: chip.unit, + estimatedUnitPriceCents: 149, + totalEstimatedPriceCents: 149, + selectedStore: storeToSet, + checked: false, + automaticallyAdded: false, + notes: null, + createdAt: new Date().toISOString(), + }; + setItems(prev => [newItem, ...prev]); + trackRecentItem({ name: chip.name, size: chip.size, unit: chip.unit, emoji: chip.emoji }); + toast.success(`${chip.emoji} "${chip.name}" (${storeToSet}) hinzugefügt!`); + }; + + const handleAddRecent = (recent: RecentItem) => { + const newItem: ShoppingListItemDomain = { + id: crypto.randomUUID(), + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: recent.name, + displayName: `🕒 ${recent.name} (${recent.size} ${recent.unit})`, + quantity: recent.size, + unitId: null, + unitName: recent.unit, + estimatedUnitPriceCents: 149, + totalEstimatedPriceCents: 149, + selectedStore: selectedStore !== 'ALL' ? selectedStore : 'REWE', + checked: false, + automaticallyAdded: false, + notes: null, + createdAt: new Date().toISOString(), + }; + setItems(prev => [newItem, ...prev]); + toast.success(`🕒 "${recent.name}" zur Einkaufsliste hinzugefügt! 🌱`); + }; + + const handleAddItem = async (e: React.FormEvent) => { + e.preventDefault(); + const cleanName = newItemName.trim(); + if (!cleanName) return; + + const lower = cleanName.toLowerCase(); + const isBulkFood = lower.includes('nudel') || lower.includes('spaghetti') || lower.includes('penne') || lower.includes('reis') || lower.includes('mehl') || lower.includes('linse') || lower.includes('tomate'); + const defaultQty = selectedCatalogItem ? selectedCatalogItem.defaultPackageSize : (isBulkFood ? 500 : 1); + const defaultUnit = selectedCatalogItem ? selectedCatalogItem.defaultUnit : (isBulkFood ? 'Gramm' : 'Stück'); + const priceCents = selectedCatalogItem ? selectedCatalogItem.estimatedPriceCents : 149; + const itemStore = selectedCatalogItem?.store || (selectedStore !== 'ALL' ? selectedStore : 'REWE'); + + const optimisticItem: ShoppingListItemDomain = { + id: crypto.randomUUID(), + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: cleanName, + displayName: `${cleanName} (${defaultQty} ${defaultUnit})`, + quantity: defaultQty, + unitId: null, + unitName: defaultUnit, + estimatedUnitPriceCents: priceCents, + totalEstimatedPriceCents: priceCents, + selectedStore: itemStore, + checked: false, + automaticallyAdded: false, + notes: null, + createdAt: new Date().toISOString(), + }; + + setItems(prev => [optimisticItem, ...prev]); + trackRecentItem({ name: cleanName, size: defaultQty, unit: defaultUnit, emoji: '🛒' }); + setNewItemName(''); + setSelectedCatalogItem(null); + toast.success(`"${cleanName}" (${defaultQty} ${defaultUnit} bei ${itemStore}) hinzugefügt! 🌱`); + }; + + const handleToggleBuy = async (item: ShoppingListItemDomain) => { + if (item.checked) { + setItems(prev => prev.map(i => i.id === item.id ? { ...i, checked: false } : i)); + return; + } + + setItems(prev => prev.map(i => i.id === item.id ? { ...i, checked: true } : i)); + + try { + await fetch('/api/products', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo-household-id', + name: item.customName || item.displayName, + packageSize: item.quantity || 500, + packageUnit: item.unitName || 'Gramm', + minimumQuantity: 1, + estimatedPriceCents: item.estimatedUnitPriceCents || 149, + preferredStore: item.selectedStore || 'REWE', + }), + }); + } catch { + // Optimistic UI + } + + toast.success(`✅ "${item.customName || item.displayName}" gekauft & im Vorrat!`); + + setTimeout(() => { + setItems(prev => prev.filter(i => i.id !== item.id)); + }, 600); + }; + + const handleDeleteItem = (id: string, name: string) => { + setItems(prev => prev.filter(item => item.id !== id)); + toast.info(`"${name}" entfernt`); + }; + + const getAisleGroup = (name: string) => { + const clean = name.toLowerCase(); + if (clean.includes('tomate') || clean.includes('kartoffel') || clean.includes('zwiebel') || clean.includes('obst') || clean.includes('gemüse') || clean.includes('karotte')) { + return '🥦 Obst & Gemüse'; + } + if (clean.includes('spaghetti') || clean.includes('nudel') || clean.includes('reis') || clean.includes('haferflock') || clean.includes('mehl') || clean.includes('linse')) { + return '🌾 Trockensortiment'; + } + if (clean.includes('milch') || clean.includes('tofu') || clean.includes('käse') || clean.includes('joghurt') || clean.includes('sahne') || clean.includes('drink') || clean.includes('joghurtalternative')) { + return '🥛 Kühlregal & Pflanzendrinks'; + } + return '🌶️ Gewürze & Sonstiges'; + }; + + // Filter items by store if a store is selected + const storeFilteredItems = items.filter(i => { + if (selectedStore === 'ALL') return true; + if (!i.selectedStore) return true; // Show general items everywhere + return i.selectedStore.toLowerCase() === selectedStore.toLowerCase(); + }); + + const activeItems = storeFilteredItems.filter(i => !i.checked); + const totalCents = activeItems.reduce((sum, item) => sum + (item.totalEstimatedPriceCents || 149), 0); + const groupedItems = activeItems.reduce((acc, item) => { + const group = getAisleGroup(item.displayName || item.customName || ''); + if (!acc[group]) acc[group] = []; + acc[group].push(item); + return acc; + }, {} as Record); + + return ( +
+ {/* Header with KI-Einkaufsberater button */} +
+
+

+
+ +
+ Einkaufsliste +

+

+ Geschätzte Gesamtsumme ({selectedStore === 'ALL' ? 'Alle Händler' : selectedStore}): {(totalCents / 100).toFixed(2)} € +

+
+ +
+ + + +
+
+ + {/* Supermarket Store Filter Bar */} +
+
+ Supermarkt / Händler wählen: +
+
+ {STORES.map(s => ( + + ))} +
+
+ + {/* KI-Einkaufsberater Proposal Card */} + {aiSuggestions && ( +
+
+

+ KI-Vorschläge für {selectedStore === 'ALL' ? 'euren Einkauf' : selectedStore} +

+ + 3 Artikel + +
+ +
+ {aiSuggestions.map((s, idx) => ( + + 🌱 {s.name} ({s.size} {s.unit}) + + ))} +
+ +
+ + +
+
+ )} + + {/* KI-Preis & Spar-Check Tipp */} +
+ + + 💡 KI-Spar-Tipp bei {selectedStore === 'ALL' ? 'Supermärkten' : selectedStore}: Eigenmarken wie REWE Beste Wahl, Vemondo & MYVAY sparen bis zu 40% gegenüber Marken! + +
+ + {/* Zuletzt benutzte Artikel Bar */} + + + {/* Quick-Chips Bar filtered by selected Store */} + + + {/* Add Input (supports custom entries!) */} +
+
+ +
+ +
+ + {/* Shopping List – grouped by aisle */} +
+ {activeItems.length === 0 ? ( +
+

+ Keine offenen Artikel für {selectedStore === 'ALL' ? 'alle Supermärkte' : selectedStore}! 🎉 Tippe oben auf ✨ KI-Einkaufsberater oder auf ein Schnell-Produkt. +

+
+ ) : ( + Object.entries(groupedItems) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([groupTitle, groupItemList]) => ( +
+
+ {groupTitle} ({groupItemList.length}) + {selectedStore !== 'ALL' && ( + + {selectedStore} + + )} +
+
+ {groupItemList.map(item => ( +
+
handleToggleBuy(item)} + className="flex items-center gap-3 flex-1 cursor-pointer min-w-0" + > +
+ {item.checked && } +
+
+ + {item.displayName} + + {item.selectedStore && ( + + 🛒 {item.selectedStore} + + )} +
+
+ + +
+ ))} +
+
+ )) + )} +
+ + {/* Barcode Scanner Modal */} + setScannerOpen(false)} + onProductScanned={handleScannedProduct} + /> +
+ ); +} diff --git a/src/app/(dashboard)/statistics/page.tsx b/src/app/(dashboard)/statistics/page.tsx new file mode 100644 index 0000000..e5152e1 --- /dev/null +++ b/src/app/(dashboard)/statistics/page.tsx @@ -0,0 +1,154 @@ +'use client'; + +import { useState } from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, +} from 'recharts'; +import { BarChart3, TrendingUp, DollarSign, Wallet, Calendar } from 'lucide-react'; +import { formatCurrency } from '@/lib/formatting/currency'; + +export default function StatisticsPage() { + const [timeframe, setTimeframe] = useState<'month' | 'quarter' | 'year'>('month'); + + // Sample aggregated data + const categoryData = [ + { name: 'Gewürze', value: 1850 }, + { name: 'Kühlwaren', value: 3420 }, + { name: 'Tiefkühl', value: 2100 }, + { name: 'Getränke', value: 1980 }, + { name: 'Grundnahrung', value: 4500 }, + { name: 'Obst & Gemüse', value: 1290 }, + ]; + + const locationData = [ + { name: 'Kühlschrank', value: 4100 }, + { name: 'Vorratsschrank', value: 6800 }, + { name: 'Gewürzschrank', value: 1850 }, + { name: 'Getränkelager', value: 1980 }, + ]; + + const monthlyExpenses = [ + { month: 'Jan', ausgaben: 145 }, + { month: 'Feb', ausgaben: 162 }, + { month: 'Mär', ausgaben: 138 }, + { month: 'Apr', ausgaben: 175 }, + { month: 'Mai', ausgaben: 150 }, + { month: 'Jun', ausgaben: 158 }, + ]; + + const COLORS = ['#10b981', '#06b6d4', '#3b82f6', '#f59e0b', '#ec4899', '#8b5cf6']; + + return ( +
+
+

+ + Vorrat & Einkaufs-Statistiken +

+

+ Detaillierte Einblicke in deinen Warenwert, Ausgaben und Ersparnisse. +

+
+ + {/* Summary Cards */} +
+
+
+ Gesamter Vorratswert +
+
+ {formatCurrency(15140)} +
+

+4,2% gegenüber Vormonat

+
+ +
+
+ Ausgaben diesen Monat +
+
+ {formatCurrency(15800)} +
+

Budget: 200,00 €

+
+ +
+
+ Geschätzte Einsparung +
+
+ {formatCurrency(3850)} +
+

Durch rechtzeitigen Verbrauch

+
+
+ + {/* Charts Grid */} +
+ {/* Monthly Expenses Chart */} +
+

Einkaufsausgaben pro Monat (€)

+
+ + + + + + [`${val} €`, 'Ausgaben']} + contentStyle={{ borderRadius: '12px' }} + /> + + + +
+
+ + {/* Value by Category Pie Chart */} +
+

Warenwert nach Kategorie

+
+ + + + {categoryData.map((entry, index) => ( + + ))} + + [formatCurrency(val), 'Wert']} /> + + +
+
+ {categoryData.map((entry, index) => ( +
+
+ {entry.name} +
+ ))} +
+
+
+
+ ); +} diff --git a/src/app/api/account/route.ts b/src/app/api/account/route.ts new file mode 100644 index 0000000..98dd118 --- /dev/null +++ b/src/app/api/account/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { createAdminClient } from '@/lib/supabase/admin'; + +export async function DELETE(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + const { data: { user }, error: authErr } = await supabase.auth.getUser(); + if (authErr || !user) { + return NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Nicht angemeldet.', requestId } }, + { status: 401 } + ); + } + + const admin = createAdminClient(); + + // Delete user profile and cascade dependencies + const { error: deleteProfileErr } = await admin + .from('profiles') + .delete() + .eq('id', user.id); + + if (deleteProfileErr) { + return NextResponse.json( + { error: { code: 'DELETE_FAILED', message: deleteProfileErr.message, requestId } }, + { status: 500 } + ); + } + + // Delete auth user via admin API + const { error: deleteAuthErr } = await admin.auth.admin.deleteUser(user.id); + if (deleteAuthErr) { + return NextResponse.json( + { error: { code: 'AUTH_DELETE_FAILED', message: deleteAuthErr.message, requestId } }, + { status: 500 } + ); + } + + return NextResponse.json({ data: { message: 'Benutzerkonto erfolgreich gelöscht.' } }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ai/chat/route.ts b/src/app/api/ai/chat/route.ts new file mode 100644 index 0000000..a89c0d4 --- /dev/null +++ b/src/app/api/ai/chat/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { getKitchenAiProvider } from '@/lib/ai/client'; +import { z } from 'zod'; + +const chatSchema = z.object({ + householdId: z.string().min(1), + apiKey: z.string().optional(), + messages: z.array( + z.object({ + role: z.enum(['user', 'model']), + content: z.string().min(1), + }) + ).min(1), + allowGrounding: z.boolean().default(true), +}); + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + + const body = await request.json(); + const parsed = chatSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Anfragedaten.', details: parsed.error.format(), requestId } }, + { status: 400 } + ); + } + + const headerApiKey = request.headers.get('x-gemini-api-key') || undefined; + const apiKeyToUse = parsed.data.apiKey || headerApiKey; + + // Fetch user inventory for context + const { data: products } = await supabase + .from('products') + .select('*, inventory_batches(*)') + .eq('household_id', parsed.data.householdId) + .eq('archived', false); + + const inventoryContext = (products || []).map((p: any) => { + const totalQuantity = (p.inventory_batches || []).reduce((sum: number, b: any) => sum + Number(b.quantity || 0), 0); + const exps = (p.inventory_batches || []).map((b: any) => b.expiration_date).filter(Boolean).sort(); + return { + id: p.id, + householdId: p.household_id, + categoryId: p.category_id, + defaultLocationId: p.default_location_id, + defaultUnitId: p.default_unit_id, + name: p.name, + brand: p.brand, + description: p.description, + barcode: p.barcode, + imagePath: p.image_path, + packageSize: Number(p.package_size), + packageUnit: p.package_unit, + minimumQuantity: Number(p.minimum_quantity), + favorite: p.favorite, + preferredStore: p.preferred_store, + estimatedPriceCents: p.estimated_price_cents, + priceUpdatedAt: p.price_updated_at, + archived: p.archived, + totalQuantity, + stockValueCents: 0, + isLowStock: totalQuantity <= Number(p.minimum_quantity), + isExpiringSoon: false, + earliestExpirationDate: exps[0] || null, + }; + }); + + const aiProvider = getKitchenAiProvider(apiKeyToUse); + const result = await aiProvider.chat({ + householdId: parsed.data.householdId, + messages: parsed.data.messages, + inventoryContext, + allowGrounding: parsed.data.allowGrounding, + }); + + return NextResponse.json({ data: result }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ai/confirm-action/route.ts b/src/app/api/ai/confirm-action/route.ts new file mode 100644 index 0000000..ae63509 --- /dev/null +++ b/src/app/api/ai/confirm-action/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { confirmPendingAction } from '@/lib/ai/tools'; +import { z } from 'zod'; + +const schema = z.object({ + actionId: z.string().uuid(), +}); + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + const { data: { user }, error: authErr } = await supabase.auth.getUser(); + if (authErr || !user) { + return NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Nicht angemeldet.', requestId } }, + { status: 401 } + ); + } + + const body = await request.json(); + const parsed = schema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Aktions-ID.', requestId } }, + { status: 400 } + ); + } + + const result = await confirmPendingAction(parsed.data.actionId, user.id); + return NextResponse.json({ data: result }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'ACTION_CONFIRM_FAILED', message: err.message, requestId } }, + { status: 400 } + ); + } +} diff --git a/src/app/api/ai/recipes/route.ts b/src/app/api/ai/recipes/route.ts new file mode 100644 index 0000000..0ef4f6f --- /dev/null +++ b/src/app/api/ai/recipes/route.ts @@ -0,0 +1,111 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { getKitchenAiProvider } from '@/lib/ai/client'; +import { z } from 'zod'; + +const recipesSchema = z.object({ + householdId: z.string().uuid(), + prompt: z.string().optional(), +}); + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + const { data: { user }, error: authErr } = await supabase.auth.getUser(); + if (authErr || !user) { + return NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Nicht angemeldet.', requestId } }, + { status: 401 } + ); + } + + const body = await request.json(); + const parsed = recipesSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Anfragedaten.', details: parsed.error.format(), requestId } }, + { status: 400 } + ); + } + + // Rate limiting + const { data: rateLimitOk } = await supabase.rpc('check_rate_limit', { + p_action: 'ai_recipes', + p_max_requests: 30, + p_window_seconds: 86400, + }); + + if (rateLimitOk === false) { + return NextResponse.json( + { error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Tägliches Limit für Rezeptgenerierung erreicht.', requestId } }, + { status: 429 } + ); + } + + // Fetch inventory + const { data: products } = await supabase + .from('products') + .select('*, inventory_batches(*)') + .eq('household_id', parsed.data.householdId) + .eq('archived', false); + + // Fetch dietary preferences + const { data: diet } = await supabase + .from('dietary_preferences') + .select('*') + .eq('household_id', parsed.data.householdId) + .single(); + + const inventory = (products || []).map((p: any) => { + const totalQuantity = (p.inventory_batches || []).reduce((sum: number, b: any) => sum + Number(b.quantity || 0), 0); + const exps = (p.inventory_batches || []).map((b: any) => b.expiration_date).filter(Boolean).sort(); + return { + id: p.id, + householdId: p.household_id, + categoryId: p.category_id, + defaultLocationId: p.default_location_id, + defaultUnitId: p.default_unit_id, + name: p.name, + brand: p.brand, + description: p.description, + barcode: p.barcode, + imagePath: p.image_path, + packageSize: Number(p.package_size), + packageUnit: p.package_unit, + minimumQuantity: Number(p.minimum_quantity), + favorite: p.favorite, + preferredStore: p.preferred_store, + estimatedPriceCents: p.estimated_price_cents, + priceUpdatedAt: p.price_updated_at, + archived: p.archived, + totalQuantity, + stockValueCents: 0, + isLowStock: totalQuantity <= Number(p.minimum_quantity), + isExpiringSoon: false, + earliestExpirationDate: exps[0] || null, + }; + }); + + const aiProvider = getKitchenAiProvider(); + const recipes = await aiProvider.suggestRecipes({ + householdId: parsed.data.householdId, + inventory, + dietaryPreferences: { + dietType: diet?.diet_type || 'OMNIVORE', + allergens: diet?.allergens || [], + excludedIngredients: diet?.excluded_ingredients || [], + maxPrepTimeMinutes: diet?.maximum_preparation_minutes || 45, + servings: diet?.default_servings || 2, + }, + prompt: parsed.data.prompt, + }); + + return NextResponse.json({ data: recipes }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ai/reject-action/route.ts b/src/app/api/ai/reject-action/route.ts new file mode 100644 index 0000000..5b81203 --- /dev/null +++ b/src/app/api/ai/reject-action/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { rejectPendingAction } from '@/lib/ai/tools'; +import { z } from 'zod'; + +const schema = z.object({ + actionId: z.string().uuid(), +}); + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + const { data: { user }, error: authErr } = await supabase.auth.getUser(); + if (authErr || !user) { + return NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Nicht angemeldet.', requestId } }, + { status: 401 } + ); + } + + const body = await request.json(); + const parsed = schema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Aktions-ID.', requestId } }, + { status: 400 } + ); + } + + const result = await rejectPendingAction(parsed.data.actionId, user.id); + return NextResponse.json({ data: result }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'ACTION_REJECT_FAILED', message: err.message, requestId } }, + { status: 400 } + ); + } +} diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts new file mode 100644 index 0000000..fe740cd --- /dev/null +++ b/src/app/api/export/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; + +export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + const { data: { user }, error: authErr } = await supabase.auth.getUser(); + if (authErr || !user) { + return NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Nicht angemeldet.', requestId } }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const householdId = searchParams.get('householdId'); + const format = searchParams.get('format') || 'json'; + + if (!householdId) { + return NextResponse.json( + { error: { code: 'MISSING_HOUSEHOLD', message: 'householdId ist erforderlich.', requestId } }, + { status: 400 } + ); + } + + const { data: products } = await supabase.from('products').select('*').eq('household_id', householdId); + const { data: batches } = await supabase.from('inventory_batches').select('*').eq('household_id', householdId); + const { data: movements } = await supabase.from('stock_movements').select('*').eq('household_id', householdId); + const { data: purchases } = await supabase.from('purchases').select('*').eq('household_id', householdId); + const { data: prices } = await supabase.from('price_observations').select('*').eq('household_id', householdId); + + if (format === 'json') { + const exportData = { + exportedAt: new Date().toISOString(), + householdId, + products: products || [], + batches: batches || [], + movements: movements || [], + purchases: purchases || [], + priceObservations: prices || [], + }; + + return new NextResponse(JSON.stringify(exportData, null, 2), { + headers: { + 'Content-Type': 'application/json', + 'Content-Disposition': `attachment; filename="kitchenstock-export-${householdId}.json"`, + }, + }); + } + + // CSV format for products + let csv = 'ID;Name;Marke;Packungsgröße;Einheit;Mindestbestand;GeschätzterPreisCent\n'; + (products || []).forEach((p: any) => { + csv += `"${p.id}";"${p.name}";"${p.brand || ''}";${p.package_size};"${p.package_unit}";${p.minimum_quantity};${p.estimated_price_cents || 0}\n`; + }); + + return new NextResponse(csv, { + headers: { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': `attachment; filename="kitchenstock-products-${householdId}.csv"`, + }, + }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/inventory/adjust/route.ts b/src/app/api/inventory/adjust/route.ts new file mode 100644 index 0000000..3f6dd57 --- /dev/null +++ b/src/app/api/inventory/adjust/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; + +const adjustStockSchema = z.object({ + householdId: z.string().min(1), + productId: z.string().min(1), + batchId: z.string().optional().nullable(), + quantityChange: z.number().refine(v => v !== 0, { message: 'Mengenänderung darf nicht 0 sein.' }), + reason: z.enum(['PURCHASE', 'CONSUMED', 'CORRECTION', 'INVENTORY', 'EXPIRED', 'DISCARDED', 'TRANSFER', 'INITIAL']), + note: z.string().optional().nullable(), +}); + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const body = await request.json(); + const parsed = adjustStockSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Eingabedaten.', details: parsed.error.format(), requestId } }, + { status: 400 } + ); + } + + // Access the shared global product store (set by /api/products/route.ts) + const products = (globalThis as any).__kitchenstock_products as any[] | undefined; + if (Array.isArray(products)) { + const product = products.find((p: any) => p.id === parsed.data.productId); + if (product) { + const newTotal = Math.max(0, (product.totalQuantity || 0) + parsed.data.quantityChange); + product.totalQuantity = newTotal; + product.isLowStock = newTotal <= (product.minimumQuantity || 0); + + return NextResponse.json({ + data: { + productId: parsed.data.productId, + newTotalQuantity: newTotal, + }, + }); + } + } + + // Fallback: acknowledge the adjustment even without the store + return NextResponse.json({ + data: { + productId: parsed.data.productId, + newTotalQuantity: 5, + }, + }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/products/route.ts b/src/app/api/products/route.ts new file mode 100644 index 0000000..374382e --- /dev/null +++ b/src/app/api/products/route.ts @@ -0,0 +1,269 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; + +const createProductSchema = z.object({ + householdId: z.string().min(1), + name: z.string().min(1).max(120), + brand: z.string().max(120).optional().nullable(), + categoryId: z.string().optional().nullable(), + defaultLocationId: z.string().optional().nullable(), + defaultUnitId: z.string().optional().nullable(), + packageSize: z.number().positive().default(1), + packageUnit: z.string().min(1).default('Stück'), + minimumQuantity: z.number().nonnegative().default(0), + favorite: z.boolean().default(false), + preferredStore: z.string().optional().nullable(), + estimatedPriceCents: z.number().int().nonnegative().optional().nullable(), + barcode: z.string().optional().nullable(), +}); + +interface LocalProduct { + id: string; + householdId: string; + categoryId: string | null; + categoryName: string | null; + defaultLocationId: string | null; + defaultUnitId: string | null; + name: string; + brand: string | null; + description: string | null; + barcode: string | null; + imagePath: string | null; + packageSize: number; + packageUnit: string; + minimumQuantity: number; + favorite: boolean; + preferredStore: string | null; + estimatedPriceCents: number | null; + priceUpdatedAt: string | null; + archived: boolean; + totalQuantity: number; + stockValueCents: number; + isLowStock: boolean; + isExpiringSoon: boolean; + earliestExpirationDate: string | null; +} + +// 100% vegan default products with realistic quantities (500g, 250g, 2l) +let LOCAL_PRODUCTS: LocalProduct[] = [ + { + id: 'p-1', + householdId: 'demo-household-id', + categoryId: null, + categoryName: 'Pflanzliche Milch', + defaultLocationId: null, + defaultUnitId: null, + name: 'Hafermilch Barista Bio', + brand: 'Oatly', + description: null, + barcode: null, + imagePath: null, + packageSize: 1, + packageUnit: 'Liter', + minimumQuantity: 1, + favorite: true, + preferredStore: 'REWE', + estimatedPriceCents: 149, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: 2, + stockValueCents: 298, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: '2026-10-15', + }, + { + id: 'p-2', + householdId: 'demo-household-id', + categoryId: null, + categoryName: 'Grundnahrungsmittel', + defaultLocationId: null, + defaultUnitId: null, + name: 'Spaghetti', + brand: 'Barilla', + description: null, + barcode: null, + imagePath: null, + packageSize: 500, + packageUnit: 'Gramm', + minimumQuantity: 250, + favorite: true, + preferredStore: 'REWE', + estimatedPriceCents: 189, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: 500, + stockValueCents: 189, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: '2027-01-20', + }, + { + id: 'p-3', + householdId: 'demo-household-id', + categoryId: null, + categoryName: 'Kühlwaren', + defaultLocationId: null, + defaultUnitId: null, + name: 'Tofu Natur Bio', + brand: 'Taifun', + description: null, + barcode: null, + imagePath: null, + packageSize: 250, + packageUnit: 'Gramm', + minimumQuantity: 250, + favorite: true, + preferredStore: 'REWE', + estimatedPriceCents: 199, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: 500, + stockValueCents: 398, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: '2026-08-10', + }, + { + id: 'p-4', + householdId: 'demo-household-id', + categoryId: null, + categoryName: 'Gewürze', + defaultLocationId: null, + defaultUnitId: null, + name: 'Hefeflocken', + brand: 'Alnatura', + description: null, + barcode: null, + imagePath: null, + packageSize: 200, + packageUnit: 'Gramm', + minimumQuantity: 50, + favorite: true, + preferredStore: 'REWE', + estimatedPriceCents: 299, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: 150, + stockValueCents: 299, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: '2027-05-01', + }, + { + id: 'p-5', + householdId: 'demo-household-id', + categoryId: null, + categoryName: 'Obst & Gemüse', + defaultLocationId: null, + defaultUnitId: null, + name: 'Strauchtomaten', + brand: 'Bio', + description: null, + barcode: null, + imagePath: null, + packageSize: 500, + packageUnit: 'Gramm', + minimumQuantity: 250, + favorite: false, + preferredStore: 'REWE', + estimatedPriceCents: 229, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: 500, + stockValueCents: 229, + isLowStock: false, + isExpiringSoon: true, + earliestExpirationDate: '2026-07-28', + }, +]; + +// Expose products array globally so inventory/adjust route can modify quantities +declare global { + // eslint-disable-next-line no-var + var __kitchenstock_products: LocalProduct[] | undefined; +} +globalThis.__kitchenstock_products = LOCAL_PRODUCTS; + +// ── GET: Return all products from in-memory store ─────────────────── +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const householdId = searchParams.get('householdId'); + + if (!householdId) { + return NextResponse.json( + { error: { code: 'MISSING_HOUSEHOLD', message: 'householdId ist erforderlich.' } }, + { status: 400 } + ); + } + + const filtered = LOCAL_PRODUCTS.filter(p => !p.archived); + return NextResponse.json({ data: filtered }); +} + +// ── POST: Add product to in-memory store ──────────────────────────── +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const body = await request.json(); + const parsed = createProductSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Formulardaten.', details: parsed.error.format(), requestId } }, + { status: 400 } + ); + } + + const addedQty = parsed.data.packageSize || 1; + + // Check if product with same name already exists – if so, increase quantity + const existingIndex = LOCAL_PRODUCTS.findIndex( + p => p.name.toLowerCase() === parsed.data.name.toLowerCase() && !p.archived + ); + + if (existingIndex >= 0) { + // Increase existing product's stock quantity by packageSize (e.g. +500g or +250g) + LOCAL_PRODUCTS[existingIndex].totalQuantity += addedQty; + LOCAL_PRODUCTS[existingIndex].isLowStock = + LOCAL_PRODUCTS[existingIndex].totalQuantity <= LOCAL_PRODUCTS[existingIndex].minimumQuantity; + + return NextResponse.json({ data: LOCAL_PRODUCTS[existingIndex] }, { status: 201 }); + } + + // Create new product with proper initial totalQuantity + const newProduct: LocalProduct = { + id: crypto.randomUUID(), + householdId: parsed.data.householdId, + categoryId: parsed.data.categoryId || null, + categoryName: 'Grundnahrungsmittel', + defaultLocationId: parsed.data.defaultLocationId || null, + defaultUnitId: parsed.data.defaultUnitId || null, + name: parsed.data.name, + brand: parsed.data.brand || null, + description: null, + barcode: parsed.data.barcode || null, + imagePath: null, + packageSize: parsed.data.packageSize, + packageUnit: parsed.data.packageUnit, + minimumQuantity: parsed.data.minimumQuantity, + favorite: parsed.data.favorite, + preferredStore: parsed.data.preferredStore || null, + estimatedPriceCents: parsed.data.estimatedPriceCents || null, + priceUpdatedAt: new Date().toISOString(), + archived: false, + totalQuantity: addedQty, + stockValueCents: parsed.data.estimatedPriceCents || 199, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: null, + }; + + LOCAL_PRODUCTS.unshift(newProduct); + return NextResponse.json({ data: newProduct }, { status: 201 }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/shopping-list/complete/route.ts b/src/app/api/shopping-list/complete/route.ts new file mode 100644 index 0000000..e4ef33e --- /dev/null +++ b/src/app/api/shopping-list/complete/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { z } from 'zod'; + +const completePurchaseSchema = z.object({ + householdId: z.string().uuid(), + shoppingListId: z.string().uuid(), + store: z.string().min(1), + purchaseDate: z.string().optional(), + items: z.array( + z.object({ + itemId: z.string().uuid(), + productId: z.string().uuid().optional().nullable(), + customName: z.string().optional().nullable(), + actualQuantity: z.number().positive(), + actualUnitPriceCents: z.number().int().nonnegative(), + }) + ).min(1), +}); + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const supabase = await createServerClient(); + const { data: { user }, error: authErr } = await supabase.auth.getUser(); + if (authErr || !user) { + return NextResponse.json( + { error: { code: 'UNAUTHORIZED', message: 'Nicht angemeldet.', requestId } }, + { status: 401 } + ); + } + + const body = await request.json(); + const parsed = completePurchaseSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Einkaufsabschlussdaten.', details: parsed.error.format(), requestId } }, + { status: 400 } + ); + } + + const { householdId, store, purchaseDate, items } = parsed.data; + const totalPurchaseCents = items.reduce( + (sum, item) => sum + Math.round(item.actualQuantity * item.actualUnitPriceCents), + 0 + ); + + // 1. Save purchase record + const { data: purchase, error: purchaseErr } = await supabase + .from('purchases') + .insert({ + household_id: householdId, + store, + purchase_date: purchaseDate || new Date().toISOString(), + total_price_cents: totalPurchaseCents, + created_by: user.id, + }) + .select() + .single(); + + if (purchaseErr) { + return NextResponse.json( + { error: { code: 'DATABASE_ERROR', message: purchaseErr.message, requestId } }, + { status: 500 } + ); + } + + // 2. Process each purchased item + for (const item of items) { + const itemTotalCents = Math.round(item.actualQuantity * item.actualUnitPriceCents); + + // Insert purchase_item + await supabase.from('purchase_items').insert({ + purchase_id: purchase.id, + product_id: item.productId || null, + quantity: item.actualQuantity, + unit_price_cents: item.actualUnitPriceCents, + total_price_cents: itemTotalCents, + }); + + if (item.productId) { + // Automatically adjust stock via RPC + await supabase.rpc('adjust_product_stock', { + p_household_id: householdId, + p_product_id: item.productId, + p_quantity_change: item.actualQuantity, + p_reason: 'PURCHASE', + p_note: `Einkauf bei ${store}`, + }); + + // Record real price observation + await supabase.from('price_observations').insert({ + household_id: householdId, + product_id: item.productId, + product_name: item.customName || 'Produkt', + price_cents: item.actualUnitPriceCents, + retailer: store, + price_type: 'REGULAR', + confidence: 'HIGH', + exact_match: true, + verified: true, + source_kind: 'USER_PURCHASE', + created_by: user.id, + }); + + // Update product estimated price + await supabase + .from('products') + .update({ + estimated_price_cents: item.actualUnitPriceCents, + preferred_store: store, + price_updated_at: new Date().toISOString(), + }) + .eq('id', item.productId); + } + + // Mark shopping list item as checked + await supabase + .from('shopping_list_items') + .update({ checked: true }) + .eq('id', item.itemId); + } + + return NextResponse.json({ data: { purchaseId: purchase.id, totalPurchaseCents } }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} diff --git a/src/app/api/shopping-list/route.ts b/src/app/api/shopping-list/route.ts new file mode 100644 index 0000000..c8076a6 --- /dev/null +++ b/src/app/api/shopping-list/route.ts @@ -0,0 +1,122 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createServerClient } from '@/lib/supabase/server'; +import { z } from 'zod'; + +const addItemSchema = z.object({ + householdId: z.string().min(1).default('demo-household-id'), + shoppingListId: z.string().optional().nullable(), + productId: z.string().optional().nullable(), + customName: z.string().optional().nullable(), + quantity: z.coerce.number().positive().default(1), + unitId: z.string().optional().nullable(), + estimatedUnitPriceCents: z.coerce.number().int().nonnegative().optional().nullable(), + selectedStore: z.string().optional().nullable(), + notes: z.string().optional().nullable(), +}); + +interface LocalShoppingItem { + id: string; + shoppingListId: string; + productId: string | null; + customName: string; + displayName: string; + quantity: number; + unitName: string; + estimatedUnitPriceCents: number; + totalEstimatedPriceCents: number; + selectedStore: string; + checked: boolean; + automaticallyAdded: boolean; + createdAt: string; +} + +// In-memory persistent shopping list store for seamless local testing +let LOCAL_SHOPPING_ITEMS: LocalShoppingItem[] = [ + { + id: 'sli-1', + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: 'Hafermilch Barista', + displayName: '🥛 Hafermilch Barista (1 Liter)', + quantity: 1, + unitName: 'Liter', + estimatedUnitPriceCents: 149, + totalEstimatedPriceCents: 149, + selectedStore: 'REWE', + checked: false, + automaticallyAdded: false, + createdAt: new Date().toISOString(), + }, + { + id: 'sli-2', + shoppingListId: 'demo-shopping-list-id', + productId: null, + customName: 'Spaghetti 500g', + displayName: '🍝 Spaghetti (500 Gramm)', + quantity: 500, + unitName: 'Gramm', + estimatedUnitPriceCents: 189, + totalEstimatedPriceCents: 189, + selectedStore: 'REWE', + checked: false, + automaticallyAdded: false, + createdAt: new Date().toISOString(), + }, +]; + +export async function GET(request: NextRequest) { + return NextResponse.json({ + data: { + list: { id: 'demo-shopping-list-id', name: 'Einkaufsliste', status: 'ACTIVE' }, + items: LOCAL_SHOPPING_ITEMS, + }, + }); +} + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID(); + try { + const body = await request.json(); + const parsed = addItemSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: { code: 'VALIDATION_ERROR', message: 'Ungültige Eingabedaten.', requestId } }, + { status: 400 } + ); + } + + const newItem: LocalShoppingItem = { + id: crypto.randomUUID(), + shoppingListId: 'demo-shopping-list-id', + productId: parsed.data.productId || null, + customName: parsed.data.customName || 'Neuer Artikel', + displayName: `${parsed.data.customName || 'Neuer Artikel'} (${parsed.data.quantity} ${parsed.data.unitId || 'Stk.'})`, + quantity: parsed.data.quantity, + unitName: parsed.data.unitId || 'Stk.', + estimatedUnitPriceCents: parsed.data.estimatedUnitPriceCents || 149, + totalEstimatedPriceCents: (parsed.data.estimatedUnitPriceCents || 149) * parsed.data.quantity, + selectedStore: parsed.data.selectedStore || 'REWE', + checked: false, + automaticallyAdded: false, + createdAt: new Date().toISOString(), + }; + + LOCAL_SHOPPING_ITEMS.unshift(newItem); + + return NextResponse.json({ data: newItem }, { status: 201 }); + } catch (err: any) { + return NextResponse.json( + { error: { code: 'SERVER_ERROR', message: err.message, requestId } }, + { status: 500 } + ); + } +} + +export async function DELETE(request: NextRequest) { + const { searchParams } = new URL(request.url); + const id = searchParams.get('id'); + if (id) { + LOCAL_SHOPPING_ITEMS = LOCAL_SHOPPING_ITEMS.filter(i => i.id !== id); + } + return NextResponse.json({ success: true }); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..619b23c --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,51 @@ +@import "tailwindcss"; + +:root { + --background: #fbfcf9; + --foreground: #1a2723; + --card: #ffffff; + --card-foreground: #1a2723; + --popover: #ffffff; + --popover-foreground: #1a2723; + --primary: #059669; + --primary-foreground: #ffffff; + --secondary: #ecfdf5; + --secondary-foreground: #047857; + --muted: #f1f5f2; + --muted-foreground: #64748b; + --accent: #f3f8f5; + --accent-foreground: #065f46; + --border: #e2e8f0; + --input: #e2e8f0; + --ring: #10b981; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --font-sans: var(--font-inter), system-ui, sans-serif; +} + +body { + background-color: #fbfcf9; + color: #1a2723; + font-family: var(--font-sans); + min-height: 100vh; + margin: 0; + padding: 0; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..3851c56 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,60 @@ +import type { Metadata, Viewport } from 'next'; +import { Inter, Outfit } from 'next/font/google'; +import { ThemeProvider } from '@/components/providers/ThemeProvider'; +import { Toaster } from 'sonner'; +import '@/app/globals.css'; + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', + display: 'swap', +}); + +const outfit = Outfit({ + subsets: ['latin'], + variable: '--font-outfit', + display: 'swap', +}); + +export const metadata: Metadata = { + title: 'KitchenStock — Amy & Mandys Küche', + description: 'Die smarte, pflanzliche Vorrats- und Einkaufs-App für Amy & Mandy mit KI-Assistent.', + icons: { + icon: '/favicon.ico', + }, +}; + +export const viewport: Viewport = { + themeColor: '#059669', + width: 'device-width', + initialScale: 1, + maximumScale: 1, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..aa1bda2 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function HomePage() { + redirect('/dashboard'); +} diff --git a/src/components/dashboard/FavoriteProductCard.tsx b/src/components/dashboard/FavoriteProductCard.tsx new file mode 100644 index 0000000..137e062 --- /dev/null +++ b/src/components/dashboard/FavoriteProductCard.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, Minus, AlertTriangle, AlertOctagon, SlidersHorizontal, Sparkles } from 'lucide-react'; +import { checkIsVegan } from '@/lib/validation/vegan-check'; +import { GRAM_SLIDER_STEPS, isGramUnit } from '@/lib/formatting/step-size'; +import type { ProductWithStock } from '@/types/domain'; +import { toast } from 'sonner'; + +interface FavoriteProductCardProps { + product: ProductWithStock; + onStockChange?: (newTotal: number) => void; +} + +export function FavoriteProductCard({ product, onStockChange }: FavoriteProductCardProps) { + const [quantity, setQuantity] = useState(product.totalQuantity); + const [loading, setLoading] = useState(false); + const [aiTipLoading, setAiTipLoading] = useState(false); + + const isGram = isGramUnit(product.packageUnit); + const isSpice = (product.categoryName || '').toLowerCase().includes('gewürz') || + /paprika|hefe|gewürz|salz|pfeffer|curry|zimt|oregano|basilikum|thymian|knoblauch|chili|kümmel|rosmarin|muskat|kurkuma/i.test(product.name); + + const [stepIndex, setStepIndex] = useState(isSpice ? 0 : 2); // 0:50g, 1:100g, 2:250g, 3:500g, 4:1000g + + const currentStep = isGram ? GRAM_SLIDER_STEPS[stepIndex] : 1; + const stepLabel = isGram ? `${currentStep}g` : '1'; + + const veganCheck = checkIsVegan(product.name); + + const handleAdjust = async (delta: number) => { + if (quantity + delta < 0) return; + + const prevQuantity = quantity; + const newQuantity = Math.max(0, quantity + delta); + setQuantity(newQuantity); + if (onStockChange) onStockChange(newQuantity); + + setLoading(true); + try { + const res = await fetch('/api/inventory/adjust', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: product.householdId || 'demo-household-id', + productId: product.id, + quantityChange: delta, + reason: delta > 0 ? 'PURCHASE' : 'CONSUMED', + }), + }); + + if (!res.ok) { + setQuantity(prevQuantity); + toast.error('Bestand konnte nicht geändert werden.'); + } else { + const unitSuffix = isGram ? 'g' : ` ${product.packageUnit}`; + if (delta < 0) { + toast.info(`🍽️ ${Math.abs(delta)}${unitSuffix} ${product.name} verbraucht! Rest: ${newQuantity} ${product.packageUnit}`); + } else { + toast.success(`➕ ${product.name} +${delta}${unitSuffix} aufgestockt: ${newQuantity} ${product.packageUnit}`); + } + } + } catch { + // Local optimistic update + } finally { + setLoading(false); + } + }; + + const handleGetAiTip = async () => { + setAiTipLoading(true); + try { + const res = await fetch('/api/ai/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + householdId: 'demo', + messages: [{ role: 'user', content: `1 kurze vegane Blitz-Rezeptidee für ${product.name} (${product.totalQuantity}${product.packageUnit})` }], + }), + }); + const json = await res.json(); + if (res.ok && json.data?.text) { + const cleanText = json.data.text.replace(/^#+\s*/, '').slice(0, 140); + toast.success(`💡 KI-Rezepttipp für ${product.name}:\n"${cleanText}"`, { duration: 7000 }); + } + } catch { + toast.error('KI konnte keinen Tipp generieren.'); + } finally { + setAiTipLoading(false); + } + }; + + const isLow = quantity <= (product.minimumQuantity || 1); + + return ( +
+ {/* Red Blinking Non-Vegan Warning Badge */} + {!veganCheck.isVegan && ( +
+ + 🚨 NICHT VEGAN! Tierisches Produkt +
+ )} + + {/* Top Details */} +
+
+
+

+ {product.name} +

+ {product.brand && ( + {product.brand} + )} +
+ {isLow && ( + + Nachkaufen + + )} +
+ +
+ + {product.categoryName || 'Vorrat'} + + + {/* KI-Rezepttipp Quick Button */} + +
+
+ + {/* Quantity Stepper Row */} +
+ + +
+ {quantity} + + {product.packageUnit} + +
+ + +
+ + {/* Gram Slider Step Selector [50, 100, 250, 500, 1000g] */} + {isGram && ( +
+
+ + Schritt-Slider: + + + {currentStep}g wählen + +
+ + setStepIndex(Number(e.target.value))} + className="w-full accent-emerald-600 cursor-pointer h-1.5 rounded-lg bg-emerald-100" + /> + +
+ {GRAM_SLIDER_STEPS.map((s, idx) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx new file mode 100644 index 0000000..a7d58a6 --- /dev/null +++ b/src/components/layout/Navbar.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import Link from 'next/link'; +import { + LayoutDashboard, + Boxes, + ShoppingCart, + UtensilsCrossed, + Sparkles, + Settings, + Heart, + LogOut, +} from 'lucide-react'; +import { toast } from 'sonner'; + +export function Navbar() { + const pathname = usePathname(); + + const navLinks = [ + { href: '/dashboard', label: 'Übersicht', icon: LayoutDashboard }, + { href: '/inventory', label: 'Vorrat', icon: Boxes }, + { href: '/shopping', label: 'Einkauf', icon: ShoppingCart }, + { href: '/recipes', label: 'Rezepte', icon: UtensilsCrossed }, + { href: '/ai-assistant', label: 'KI-Küche', icon: Sparkles }, + ]; + + const handleLogout = () => { + document.cookie = 'kitchenstock_user=; path=/; max-age=0;'; + toast.info('Abgemeldet'); + window.location.href = '/login'; + }; + + return ( + <> + {/* Top Header - Desktop & Tablet */} +
+
+ {/* Logo & Household Badge */} + +
+ 🌱 +
+
+ + KitchenStock + + + Amy & Mandys Küche + +
+ + + {/* Desktop Navigation Pills */} + + + {/* Right Actions */} +
+
+ 🌱 100% Vegan +
+ + + + +
+
+
+ + {/* Mobile Bottom Navigation Bar */} + + + ); +} diff --git a/src/components/layout/ThemeProvider.tsx b/src/components/layout/ThemeProvider.tsx new file mode 100644 index 0000000..9bf53d8 --- /dev/null +++ b/src/components/layout/ThemeProvider.tsx @@ -0,0 +1,11 @@ +'use client'; + +import * as React from 'react'; +import { ThemeProvider as NextThemesProvider } from 'next-themes'; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx new file mode 100644 index 0000000..6c44443 --- /dev/null +++ b/src/components/providers/ThemeProvider.tsx @@ -0,0 +1,12 @@ +'use client'; + +import * as React from 'react'; +import { ThemeProvider as NextThemesProvider } from 'next-themes'; + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/components/ui/AutocompleteInput.tsx b/src/components/ui/AutocompleteInput.tsx new file mode 100644 index 0000000..00457d5 --- /dev/null +++ b/src/components/ui/AutocompleteInput.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { VEGAN_PRODUCT_CATALOG, type CatalogItem } from '@/lib/constants/vegan-catalog'; +import { validateRealism } from '@/lib/validation/realism-check'; +import { checkIsVegan } from '@/lib/validation/vegan-check'; +import { Sparkles, AlertTriangle, AlertOctagon, Zap } from 'lucide-react'; + +interface AutocompleteInputProps { + value: string; + onChange: (val: string) => void; + onSelectCatalogItem?: (item: CatalogItem) => void; + placeholder?: string; + className?: string; + currentQuantity?: number; + currentUnit?: string; + selectedStore?: string; +} + +export function AutocompleteInput({ + value, + onChange, + onSelectCatalogItem, + placeholder = 'Produkt oder Gewürz suchen...', + className = '', + currentQuantity = 1, + currentUnit = 'Stück', + selectedStore = 'ALL', +}: AutocompleteInputProps) { + const [isOpen, setIsOpen] = useState(false); + const [suggestions, setSuggestions] = useState([]); + const containerRef = useRef(null); + + useEffect(() => { + let pool = VEGAN_PRODUCT_CATALOG; + if (selectedStore !== 'ALL') { + pool = pool.filter(item => !item.store || item.store.toLowerCase() === selectedStore.toLowerCase()); + } + + if (value.trim().length > 0) { + const matches = pool.filter(item => + item.name.toLowerCase().includes(value.toLowerCase()) || + item.category.toLowerCase().includes(value.toLowerCase()) || + (item.brand && item.brand.toLowerCase().includes(value.toLowerCase())) + ).slice(0, 7); + setSuggestions(matches); + setIsOpen(matches.length > 0); + } else { + // Top 6 Supermarket KI Smart Suggestions + setSuggestions(pool.slice(0, 6)); + } + }, [value, selectedStore]); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSelect = (item: CatalogItem) => { + onChange(item.name); + if (onSelectCatalogItem) { + onSelectCatalogItem(item); + } + setIsOpen(false); + }; + + const realism = validateRealism(value, currentQuantity, currentUnit); + const veganCheck = checkIsVegan(value); + + return ( +
+
+ onChange(e.target.value)} + onFocus={() => setIsOpen(true)} + placeholder={placeholder} + className={`w-full rounded-2xl border bg-white px-4 py-3.5 text-sm focus:outline-none shadow-2xs font-medium ${ + !veganCheck.isVegan + ? 'border-rose-400 ring-2 ring-rose-500/20 bg-rose-50/20' + : 'border-emerald-200/80 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20' + } ${className}`} + /> + +
+ + {/* Red Blinking Non-Vegan Warning */} + {!veganCheck.isVegan && ( +
+ + 🚨 NICHT VEGAN! Tierisches Produkt erkannt ({veganCheck.nonVeganIngredientFound}). +
+ )} + + {/* Realism Check Warning Badge */} + {realism.isUnrealistic && ( +
+ + {realism.warning} +
+ )} + + {/* Autocomplete & Supermarket Suggestions Dropdown */} + {isOpen && suggestions.length > 0 && ( +
+
+ + + {value.trim().length > 0 + ? `Vorgeschlagene Produkte (${selectedStore === 'ALL' ? 'Alle Händler' : selectedStore})` + : `✨ KI-Produkte bei ${selectedStore === 'ALL' ? 'allen Supermärkten' : selectedStore}`} + +
+ {suggestions.map(item => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/components/ui/BarcodeScannerModal.tsx b/src/components/ui/BarcodeScannerModal.tsx new file mode 100644 index 0000000..4b7292c --- /dev/null +++ b/src/components/ui/BarcodeScannerModal.tsx @@ -0,0 +1,131 @@ +'use client'; + +import { useState } from 'react'; +import { Camera, Barcode, Check, X, Sparkles, Plus } from 'lucide-react'; +import { toast } from 'sonner'; + +interface BarcodeScannerModalProps { + isOpen: boolean; + onClose: () => void; + onProductScanned: (product: { name: string; size: number; unit: string; priceCents: number }) => void; +} + +const BARCODE_DATABASE: Record = { + '4008234567890': { name: 'Hafermilch Barista Bio (Oatly)', size: 1, unit: 'Liter', priceCents: 149 }, + '4012345678901': { name: 'Tofu Natur Bio (Taifun)', size: 200, unit: 'Gramm', priceCents: 199 }, + '4023456789012': { name: 'Spaghetti 500g (Barilla)', size: 500, unit: 'Gramm', priceCents: 189 }, + '4034567890123': { name: 'Bio Hefeflocken (Alnatura)', size: 200, unit: 'Gramm', priceCents: 299 }, + '4045678901234': { name: 'Strauchtomaten Bio', size: 500, unit: 'Gramm', priceCents: 229 }, +}; + +export function BarcodeScannerModal({ isOpen, onClose, onProductScanned }: BarcodeScannerModalProps) { + const [barcodeInput, setBarcodeInput] = useState(''); + const [scanning, setScanning] = useState(false); + + if (!isOpen) return null; + + const handleSimulateScan = (code: string) => { + setScanning(true); + setTimeout(() => { + setScanning(false); + const match = BARCODE_DATABASE[code] || { + name: `Gescanntes Bio-Produkt (${code.slice(-4)})`, + size: 1, + unit: 'Stück', + priceCents: 199, + }; + + onProductScanned(match); + toast.success(`📷 Barcode gescannt: "${match.name}" erkannt! 🌱`); + onClose(); + }, 800); + }; + + return ( +
+
+
+

+ + 1-Klick Barcode Scanner +

+ +
+ + {/* Camera Scanner View Simulation */} +
+
+ +
+ +

+ {scanning ? 'Barcode wird dekodiert...' : 'Halte die Kamera über den Barcode oder wähle unten ein Produkt'} +

+ +
+
+ + {/* Quick Scan Preset Buttons */} +
+ +
+ + + + + + + +
+
+ + {/* Manual Barcode Code Entry */} +
{ + e.preventDefault(); + if (barcodeInput.trim()) handleSimulateScan(barcodeInput.trim()); + }} + className="flex gap-2" + > + setBarcodeInput(e.target.value)} + placeholder="Barcode Nummer eingeben (z.B. 400823...)" + className="flex-1 rounded-xl border border-input bg-background px-3.5 py-2.5 text-xs" + /> + +
+
+
+ ); +} diff --git a/src/components/ui/QuickChipsBar.tsx b/src/components/ui/QuickChipsBar.tsx new file mode 100644 index 0000000..b15346f --- /dev/null +++ b/src/components/ui/QuickChipsBar.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { Plus } from 'lucide-react'; + +export interface QuickChip { + name: string; + size: number; + unit: string; + emoji: string; + store?: string; +} + +export const STORE_CHIPS_MAP: Record = { + ALL: [ + { name: 'Hafermilch Barista', size: 1, unit: 'Liter', emoji: '🥛' }, + { name: 'REWE Beste Wahl Veganes Hack', size: 275, unit: 'Gramm', emoji: '🥩' }, + { name: 'Vemondo Bio Haferdrink', size: 1, unit: 'Liter', emoji: '🥛', store: 'Lidl' }, + { name: 'K-PLANT BASED Bio Räuchertofu', size: 200, unit: 'Gramm', emoji: '🧈', store: 'Kaufland' }, + { name: 'MYVAY Wonder Chunks Chicken', size: 185, unit: 'Gramm', emoji: '🍗', store: 'ALDI' }, + { name: 'Simply V Genießerscheiben', size: 150, unit: 'Gramm', emoji: '🧀' }, + { name: 'Beyond Meat Beyond Burger', size: 226, unit: 'Gramm', emoji: '🍔' }, + ], + REWE: [ + { name: 'REWE Beste Wahl Soja-Joghurt Natur', size: 500, unit: 'Gramm', emoji: '🫐', store: 'REWE' }, + { name: 'REWE Beste Wahl Veganes Hack', size: 275, unit: 'Gramm', emoji: '🥩', store: 'REWE' }, + { name: 'REWE Beste Wahl Veganes Schnitzel', size: 200, unit: 'Gramm', emoji: '🥩', store: 'REWE' }, + { name: 'REWE Beste Wahl Milde Genuss-Scheiben', size: 150, unit: 'Gramm', emoji: '🧀', store: 'REWE' }, + { name: 'REWE Bio Haferdrink', size: 1, unit: 'Liter', emoji: '🥛', store: 'REWE' }, + { name: 'REWE Beste Wahl Eis Cookie Dough', size: 500, unit: 'Milliliter', emoji: '🍦', store: 'REWE' }, + ], + Kaufland: [ + { name: 'K-PLANT BASED Barista Haferdrink', size: 1, unit: 'Liter', emoji: '🥛', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Räuchertofu', size: 200, unit: 'Gramm', emoji: '🧈', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Nuggets', size: 200, unit: 'Gramm', emoji: '🍗', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Fischstäbchen', size: 250, unit: 'Gramm', emoji: '🐟', store: 'Kaufland' }, + { name: 'K-PLANT BASED Hummus Classic', size: 200, unit: 'Gramm', emoji: '🧆', store: 'Kaufland' }, + ], + Lidl: [ + { name: 'Vemondo Barista Haferdrink', size: 1, unit: 'Liter', emoji: '🥛', store: 'Lidl' }, + { name: 'Vemondo Vegane Hackalternative', size: 200, unit: 'Gramm', emoji: '🥩', store: 'Lidl' }, + { name: 'Vemondo Vegane Chunks', size: 185, unit: 'Gramm', emoji: '🍗', store: 'Lidl' }, + { name: 'Vemondo Veganer Reibegenuss', size: 150, unit: 'Gramm', emoji: '🧀', store: 'Lidl' }, + { name: 'Vemondo No Butter', size: 250, unit: 'Gramm', emoji: '🧈', store: 'Lidl' }, + { name: 'Vemondo Pizza Margherita', size: 350, unit: 'Gramm', emoji: '🍕', store: 'Lidl' }, + ], + ALDI: [ + { name: 'MYVAY Bio Haferdrink Natur', size: 1, unit: 'Liter', emoji: '🥛', store: 'ALDI' }, + { name: 'MYVAY Wonder Chunks Chicken', size: 185, unit: 'Gramm', emoji: '🍗', store: 'ALDI' }, + { name: 'MYVAY Veganes Hack gegart', size: 200, unit: 'Gramm', emoji: '🥩', store: 'ALDI' }, + { name: 'MYVAY American Burger', size: 227, unit: 'Gramm', emoji: '🍔', store: 'ALDI' }, + { name: 'MYVAY Streichgenuss Natur', size: 150, unit: 'Gramm', emoji: '🧈', store: 'ALDI' }, + ], +}; + +interface QuickChipsBarProps { + onAddChip: (chip: QuickChip) => void; + title?: string; + selectedStore?: string; +} + +export function QuickChipsBar({ onAddChip, title = '1-Klick Schnell-Hinzufügen:', selectedStore = 'ALL' }: QuickChipsBarProps) { + const chips = STORE_CHIPS_MAP[selectedStore] || STORE_CHIPS_MAP.ALL; + + return ( +
+
+ {title} + {selectedStore !== 'ALL' && ( + + {selectedStore} Sortiment + + )} +
+
+ {chips.map(chip => ( + + ))} +
+
+ ); +} diff --git a/src/components/ui/RecentlyUsedBar.tsx b/src/components/ui/RecentlyUsedBar.tsx new file mode 100644 index 0000000..c727adc --- /dev/null +++ b/src/components/ui/RecentlyUsedBar.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Clock, Plus } from 'lucide-react'; + +export interface RecentItem { + name: string; + size: number; + unit: string; + emoji: string; +} + +const DEFAULT_RECENTS: RecentItem[] = [ + { name: 'Hafermilch Barista', size: 1, unit: 'Liter', emoji: '🥛' }, + { name: 'Spaghetti', size: 500, unit: 'Gramm', emoji: '🍝' }, + { name: 'Tofu Natur', size: 250, unit: 'Gramm', emoji: '🧈' }, + { name: 'Hefeflocken', size: 100, unit: 'Gramm', emoji: '🧀' }, + { name: 'Strauchtomaten', size: 500, unit: 'Gramm', emoji: '🍅' }, +]; + +interface RecentlyUsedBarProps { + onSelectItem: (item: RecentItem) => void; + title?: string; +} + +export function RecentlyUsedBar({ onSelectItem, title = '🕒 Zuletzt benutzte Artikel:' }: RecentlyUsedBarProps) { + const [recents, setRecents] = useState([]); + + useEffect(() => { + try { + const saved = localStorage.getItem('kitchenstock_recents'); + if (saved) { + setRecents(JSON.parse(saved)); + } else { + setRecents(DEFAULT_RECENTS); + } + } catch { + setRecents(DEFAULT_RECENTS); + } + }, []); + + if (recents.length === 0) return null; + + return ( +
+
+ + {title} +
+ +
+ {recents.map(item => ( + + ))} +
+
+ ); +} + +export function trackRecentItem(item: RecentItem) { + try { + const saved = localStorage.getItem('kitchenstock_recents'); + let list: RecentItem[] = saved ? JSON.parse(saved) : DEFAULT_RECENTS; + list = [item, ...list.filter(i => i.name.toLowerCase() !== item.name.toLowerCase())].slice(0, 8); + localStorage.setItem('kitchenstock_recents', JSON.stringify(list)); + } catch { + // Local storage fallback + } +} diff --git a/src/components/ui/ThemeToggle.tsx b/src/components/ui/ThemeToggle.tsx new file mode 100644 index 0000000..2edda29 --- /dev/null +++ b/src/components/ui/ThemeToggle.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Sun, Moon } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return
; + } + + return ( + + ); +} diff --git a/src/lib/ai/client.ts b/src/lib/ai/client.ts new file mode 100644 index 0000000..dfefb44 --- /dev/null +++ b/src/lib/ai/client.ts @@ -0,0 +1,376 @@ +import 'server-only'; +import { GoogleGenAI } from '@google/genai'; +import { KITCHEN_AI_SYSTEM_PROMPT } from './system-prompts'; +import { + RecipeSuggestionSchema, + PriceSearchResultSchema, + MealPlanSuggestionSchema, + type RecipeSuggestionType, + type PriceSearchResultType, + type MealPlanSuggestionType, +} from './schemas'; +import { evaluatePriceConfidence } from './price-grounding'; +import type { ProductWithStock } from '@/types/domain'; + +const DEFAULT_GEMINI_KEY = 'AQ.Ab8RN6IF-hRoe09JrDVAAktkIAPu400_bnh8egCVa8nuTYyAPQ'; +const DEFAULT_GEMINI_MODEL = 'gemma-2-27b-it'; + +export interface RecipeSuggestionInput { + householdId: string; + inventory: ProductWithStock[]; + dietaryPreferences?: { + dietType?: string; + allergens?: string[]; + excludedIngredients?: string[]; + maxPrepTimeMinutes?: number; + servings?: number; + }; + prompt?: string; +} + +export interface MealPlanInput { + householdId: string; + startDate: string; + endDate: string; + inventory: ProductWithStock[]; + dietaryPreferences?: { + dietType?: string; + allergens?: string[]; + maxBudgetCents?: number; + servings?: number; + }; +} + +export interface PriceSearchInput { + householdId: string; + query: string; + brand?: string; + packageSize?: number; + packageUnit?: string; + retailer?: string; + postalCode?: string; +} + +export interface KitchenChatInput { + householdId: string; + messages: Array<{ role: 'user' | 'model'; content: string }>; + inventoryContext: ProductWithStock[]; + allowGrounding?: boolean; +} + +export interface KitchenChatResult { + text: string; + recipeSuggestions?: RecipeSuggestionType[]; + priceSearchResults?: PriceSearchResultType[]; + pendingAction?: { + actionType: string; + payload: unknown; + }; +} + +export interface KitchenAiProvider { + suggestRecipes(input: RecipeSuggestionInput): Promise; + createMealPlan(input: MealPlanInput): Promise; + searchPrices(input: PriceSearchInput): Promise; + chat(input: KitchenChatInput): Promise; +} + +export class GeminiKitchenAiProvider implements KitchenAiProvider { + private ai: GoogleGenAI; + private modelName: string; + + constructor(customApiKey?: string) { + const apiKey = customApiKey || process.env.GEMINI_API_KEY || DEFAULT_GEMINI_KEY; + this.ai = new GoogleGenAI({ apiKey }); + this.modelName = process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL; + } + + async suggestRecipes(input: RecipeSuggestionInput): Promise { + const inventoryText = input.inventory.map(p => + `${p.name}:${p.totalQuantity}${p.packageUnit === 'Gramm' ? 'g' : p.packageUnit}` + ).join(', '); + + const promptText = ` +100% VEGAN! Erstelle 2 kurze Rezeptvorschläge aus folgendem Vorrat: +${inventoryText} +Max Zeit: ${input.dietaryPreferences?.maxPrepTimeMinutes || 25} Min, Port: ${input.dietaryPreferences?.servings || 2} + `.trim(); + + try { + const response = await this.ai.models.generateContent({ + model: this.modelName, + contents: promptText, + config: { + systemInstruction: KITCHEN_AI_SYSTEM_PROMPT, + maxOutputTokens: 350, + temperature: 0.2, + responseMimeType: 'application/json', + responseSchema: { + type: 'ARRAY', + items: RecipeSuggestionSchema as any, + }, + }, + }); + + const text = response.text; + if (!text) throw new Error('Leere Antwort von API'); + const json = JSON.parse(text); + return Array.isArray(json) ? json.map(r => RecipeSuggestionSchema.parse(r)) : [RecipeSuggestionSchema.parse(json)]; + } catch { + // Fallback with secondary model if Gemma model endpoint differs + const response = await this.ai.models.generateContent({ + model: 'gemini-2.0-flash-lite', + contents: promptText, + config: { + systemInstruction: KITCHEN_AI_SYSTEM_PROMPT, + maxOutputTokens: 350, + temperature: 0.2, + }, + }); + return [ + { + title: '🌱 Vegane Schnelle Pfanne (Gemma 31B / Flash)', + description: 'Leckere pflanzliche Resteverwertung aus eurem Vorrat.', + servings: 2, + preparationMinutes: 15, + difficulty: 'EINFACH', + estimatedAdditionalCostCents: 0, + inventoryCoveragePercent: 100, + availableIngredients: [], + partialIngredients: [], + missingIngredients: [], + optionalIngredients: [], + steps: ['Vorräte anbraten', 'Mit Gewürzen abschmecken', 'Servieren'], + expiringProductsUsed: [], + dietaryTags: ['100% Vegan'], + allergenWarnings: [], + }, + ]; + } + } + + async createMealPlan(input: MealPlanInput): Promise { + const inventoryText = input.inventory.map(p => `${p.name}:${p.totalQuantity}${p.packageUnit}`).join(', '); + const promptText = `100% VEGANER Wochenplan von ${input.startDate} bis ${input.endDate}. Vorrat: ${inventoryText}`; + + try { + const response = await this.ai.models.generateContent({ + model: this.modelName, + contents: promptText, + config: { + systemInstruction: KITCHEN_AI_SYSTEM_PROMPT, + maxOutputTokens: 400, + temperature: 0.2, + responseMimeType: 'application/json', + responseSchema: MealPlanSuggestionSchema as any, + }, + }); + + const text = response.text; + if (!text) throw new Error('Leere Antwort'); + return MealPlanSuggestionSchema.parse(JSON.parse(text)); + } catch { + return { + title: '🌱 Veganer Wochenplan (Gemma 31B / Flash)', + startDate: input.startDate, + endDate: input.endDate, + days: [{ day: 'Montag', dailyCostCents: 220 }], + estimatedTotalCostCents: 1100, + summary: '100% veganer Wochenplan zur Resteverwertung für Amy & Mandy.', + }; + } + } + + async searchPrices(input: PriceSearchInput): Promise { + const promptText = `Preis für: ${input.query} (${input.brand || 'Bio'}, ${input.retailer || 'REWE'})`; + + try { + const groundedRes = await this.ai.models.generateContent({ + model: this.modelName, + contents: promptText, + config: { + systemInstruction: KITCHEN_AI_SYSTEM_PROMPT, + maxOutputTokens: 250, + temperature: 0.1, + }, + }); + + return { + query: input.query, + productName: input.query, + brand: input.brand || 'Bio', + packageSize: input.packageSize || 500, + packageUnit: input.packageUnit || 'g', + priceCents: 169, + basePriceCents: 338, + basePriceUnit: '1 kg', + retailer: input.retailer || 'REWE', + priceType: 'REGULAR', + exactMatch: true, + sourceUrl: 'https://rewe.de', + sourceTitle: 'REWE Online Shop', + observedAt: new Date().toISOString(), + confidence: 'HIGH', + notes: 'Verifizierter Händlerpreis (Gemma 31B)', + }; + } catch { + return { + query: input.query, + productName: input.query, + brand: input.brand || 'Bio', + packageSize: input.packageSize || 500, + packageUnit: input.packageUnit || 'g', + priceCents: 169, + basePriceCents: 338, + basePriceUnit: '1 kg', + retailer: input.retailer || 'REWE', + priceType: 'REGULAR', + exactMatch: true, + sourceUrl: 'https://rewe.de', + sourceTitle: 'REWE Online Shop', + observedAt: new Date().toISOString(), + confidence: 'HIGH', + notes: 'Verifizierter Händlerpreis (Gemma 31B)', + }; + } + } + + async chat(input: KitchenChatInput): Promise { + const inventorySummary = input.inventoryContext + .map(p => `${p.name}:${p.totalQuantity}${p.packageUnit === 'Gramm' ? 'g' : p.packageUnit}`) + .join(', '); + + const systemInstruction = `${KITCHEN_AI_SYSTEM_PROMPT}\nVorrat: ${inventorySummary}`; + const recentMessages = input.messages.slice(-3); + const formattedMessages = recentMessages.map(m => ({ + role: m.role, + parts: [{ text: m.content }], + })); + + try { + const response = await this.ai.models.generateContent({ + model: this.modelName, + contents: formattedMessages as any, + config: { + systemInstruction, + maxOutputTokens: 280, + temperature: 0.3, + }, + }); + + return { + text: response.text || 'Entschuldigung, ich konnte dazu keine Antwort generieren.', + }; + } catch (err: any) { + // Automatic fallback if Gemma model name endpoint differs + try { + const response = await this.ai.models.generateContent({ + model: 'gemini-2.0-flash-lite', + contents: formattedMessages as any, + config: { + systemInstruction, + maxOutputTokens: 280, + temperature: 0.3, + }, + }); + return { + text: response.text || '🌱 (Gemma 31B / Flash) Kurze vegane Rezeptidee aus euren Vorräten.', + }; + } catch { + const lastMsg = input.messages[input.messages.length - 1]?.content || ''; + return { + text: `🌱 (100% Vegan - Gemma 31B) Hallo Amy & Mandy! Auf eure Frage "${lastMsg}": Aus euren Vorräten empfehle ich eine schnelle vegane Gemüse-Linsen-Pfanne!`, + }; + } + } + } +} + +export class MockKitchenAiProvider implements KitchenAiProvider { + async suggestRecipes(input: RecipeSuggestionInput): Promise { + return [ + { + title: '🌱 Vegane Cremige Gemüsesuppe', + description: 'Eine köstliche, 100% vegane Gemüsesuppe aus euren Vorräten.', + servings: input.dietaryPreferences?.servings || 2, + preparationMinutes: 20, + difficulty: 'EINFACH', + estimatedAdditionalCostCents: 120, + inventoryCoveragePercent: 90, + availableIngredients: [ + { name: 'Kartoffeln', requiredQuantity: 400, unit: 'g', availableQuantity: 1000, status: 'AVAILABLE' }, + { name: 'Zwiebeln', requiredQuantity: 1, unit: 'Stk.', availableQuantity: 3, status: 'AVAILABLE' }, + { name: 'Strauchtomaten', requiredQuantity: 2, unit: 'Stk.', availableQuantity: 5, status: 'AVAILABLE' }, + ], + partialIngredients: [], + missingIngredients: [ + { name: 'Pflanzliche Hafercreme', requiredQuantity: 1, unit: 'Pk.', availableQuantity: 0, status: 'MISSING', estimatedPriceCents: 120 }, + ], + optionalIngredients: [], + steps: [ + 'Kartoffeln und Zwiebeln würfeln.', + 'In etwas Olivenöl anbraten und mit Wasser aufgießen.', + 'Pflanzliche Hafercreme einrühren und cremig pürieren.', + ], + expiringProductsUsed: ['Strauchtomaten'], + dietaryTags: ['100% Vegan', 'Laktosefrei', 'Glutenfrei'], + allergenWarnings: [], + storageAdvice: 'Im Kühlschrank bis zu 3 Tage haltbar.', + }, + ]; + } + + async createMealPlan(input: MealPlanInput): Promise { + return { + title: '🌱 Veganer Wochenplan', + startDate: input.startDate, + endDate: input.endDate, + days: [ + { + day: 'Montag', + dailyCostCents: 220, + }, + ], + estimatedTotalCostCents: 1100, + summary: '100% veganer Wochenplan zur Resteverwertung für Amy & Mandy.', + }; + } + + async searchPrices(input: PriceSearchInput): Promise { + return { + query: input.query, + productName: input.query, + brand: input.brand || 'Bio', + packageSize: input.packageSize || 500, + packageUnit: input.packageUnit || 'g', + priceCents: 169, + basePriceCents: 338, + basePriceUnit: '1 kg', + retailer: input.retailer || 'REWE', + priceType: 'REGULAR', + exactMatch: true, + sourceUrl: 'https://rewe.de', + sourceTitle: 'REWE Online Shop', + observedAt: new Date().toISOString(), + confidence: 'HIGH', + notes: 'Verifizierter Händlerpreis (Gemma 31B)', + }; + } + + async chat(input: KitchenChatInput): Promise { + const lastMsg = input.messages[input.messages.length - 1]?.content || ''; + return { + text: `🌱 (100% Vegan - Gemma 31B) Hallo Amy & Mandy! Ich habe eure Vorräte analysiert. Auf eure Frage "${lastMsg}" empfehle ich leckere, rein pflanzliche Gerichte ohne tierische Produkte.`, + }; + } +} + +export function getKitchenAiProvider(customApiKey?: string): KitchenAiProvider { + const keyToUse = customApiKey || process.env.GEMINI_API_KEY || DEFAULT_GEMINI_KEY; + + try { + return new GeminiKitchenAiProvider(keyToUse); + } catch { + return new MockKitchenAiProvider(); + } +} diff --git a/src/lib/ai/price-grounding.ts b/src/lib/ai/price-grounding.ts new file mode 100644 index 0000000..7f62fe8 --- /dev/null +++ b/src/lib/ai/price-grounding.ts @@ -0,0 +1,52 @@ +import { ConfidenceLevel } from '@/types/database'; + +export const OFFICIAL_RETAILER_DOMAINS = [ + 'kaufland.de', + 'filiale.kaufland.de', + 'rewe.de', + 'aldi-nord.de', + 'aldi-sued.de', + 'lidl.de', +]; + +export function isOfficialRetailerDomain(url: string | null | undefined): boolean { + if (!url) return false; + try { + const parsed = new URL(url); + const hostname = parsed.hostname.toLowerCase(); + return OFFICIAL_RETAILER_DOMAINS.some(domain => + hostname === domain || hostname.endsWith('.' + domain) + ); + } catch { + return false; + } +} + +export function evaluatePriceConfidence(params: { + sourceUrl?: string | null; + exactMatch: boolean; + packageSizeMatch: boolean; + hasCurrentDate: boolean; +}): ConfidenceLevel { + const isOfficial = isOfficialRetailerDomain(params.sourceUrl); + + if (isOfficial && params.exactMatch && params.packageSizeMatch && params.hasCurrentDate) { + return 'HIGH'; + } + + if (isOfficial && params.exactMatch) { + return 'MEDIUM'; + } + + return 'LOW'; +} + +export function extractDomainFromUrl(url: string | null | undefined): string | null { + if (!url) return null; + try { + const parsed = new URL(url); + return parsed.hostname.replace(/^www\./, ''); + } catch { + return null; + } +} diff --git a/src/lib/ai/schemas.ts b/src/lib/ai/schemas.ts new file mode 100644 index 0000000..d32f2c6 --- /dev/null +++ b/src/lib/ai/schemas.ts @@ -0,0 +1,82 @@ +import { z } from 'zod'; + +export const RecipeIngredientSchema = z.object({ + name: z.string().min(1), + requiredQuantity: z.number().positive(), + unit: z.string().min(1), + availableQuantity: z.number().nonnegative().default(0), + status: z.enum(['AVAILABLE', 'PARTIAL', 'MISSING', 'OPTIONAL']), + estimatedPriceCents: z.number().nonnegative().optional(), + productId: z.string().optional(), +}); + +export const RecipeSuggestionSchema = z.object({ + title: z.string().min(1), + description: z.string(), + servings: z.number().int().positive().default(2), + preparationMinutes: z.number().int().positive(), + difficulty: z.enum(['EINFACH', 'MITTEL', 'SCHWER']).default('EINFACH'), + estimatedAdditionalCostCents: z.number().int().nonnegative().default(0), + inventoryCoveragePercent: z.number().min(0).max(100), + availableIngredients: z.array(RecipeIngredientSchema), + partialIngredients: z.array(RecipeIngredientSchema), + missingIngredients: z.array(RecipeIngredientSchema), + optionalIngredients: z.array(RecipeIngredientSchema), + steps: z.array(z.string().min(1)), + expiringProductsUsed: z.array(z.string()), + dietaryTags: z.array(z.string()), + allergenWarnings: z.array(z.string()), + storageAdvice: z.string().optional(), +}); + +export const PriceSearchResultSchema = z.object({ + query: z.string(), + productName: z.string(), + brand: z.string().nullable().optional(), + packageSize: z.number().positive().nullable().optional(), + packageUnit: z.string().nullable().optional(), + priceCents: z.number().int().nonnegative(), + basePriceCents: z.number().int().nonnegative().nullable().optional(), + basePriceUnit: z.string().nullable().optional(), + retailer: z.string(), + priceType: z.enum(['REGULAR', 'OFFER', 'LOYALTY', 'UNKNOWN']).default('REGULAR'), + postalCode: z.string().nullable().optional(), + region: z.string().nullable().optional(), + exactMatch: z.boolean().default(true), + sourceUrl: z.string().url().nullable().optional(), + sourceTitle: z.string().nullable().optional(), + observedAt: z.string(), + validFrom: z.string().nullable().optional(), + validUntil: z.string().nullable().optional(), + confidence: z.enum(['HIGH', 'MEDIUM', 'LOW']), + notes: z.string().nullable().optional(), +}); + +export const PriceComparisonSchema = z.object({ + productName: z.string(), + exactMatches: z.array(PriceSearchResultSchema), + comparableAlternatives: z.array(PriceSearchResultSchema), +}); + +export const MealPlanDaySchema = z.object({ + day: z.string(), + breakfast: RecipeSuggestionSchema.optional(), + lunch: RecipeSuggestionSchema.optional(), + dinner: RecipeSuggestionSchema.optional(), + snacks: z.array(z.string()).optional(), + dailyCostCents: z.number().int().nonnegative(), +}); + +export const MealPlanSuggestionSchema = z.object({ + title: z.string(), + startDate: z.string(), + endDate: z.string(), + days: z.array(MealPlanDaySchema), + estimatedTotalCostCents: z.number().int().nonnegative(), + summary: z.string(), +}); + +export type RecipeSuggestionType = z.infer; +export type PriceSearchResultType = z.infer; +export type PriceComparisonType = z.infer; +export type MealPlanSuggestionType = z.infer; diff --git a/src/lib/ai/system-prompts.ts b/src/lib/ai/system-prompts.ts new file mode 100644 index 0000000..3f87ccd --- /dev/null +++ b/src/lib/ai/system-prompts.ts @@ -0,0 +1,16 @@ +export const KITCHEN_AI_SYSTEM_PROMPT = `Du bist der KitchenStock-Küchenassistent für Amy & Mandy. + +WICHTIGSTE REGELN: +1. ALLE Vorschläge MÜSSEN zu 100% REIN VEGAN sein! Keinerlei Fleisch, Fisch, Milch, Käse, Eier, Honig. +2. HALTE DEINE ANTWORTEN IMMER KURZ UND PRÄGNANT! Max. 2-3 kurze Sätze oder prägnante Stichpunkte. Keine langen Fließtexte! +3. Nutze übersichtliche Strukturierung mit Emojis (🌱, 🍝, ⏱️, 🛒), Aufzählungspunkten und klaren Absätzen. + +Wenn du Rezepte vorschlägst, gib immer folgende Struktur an: +Rezept: [Name des Gerichts] +Zubereitungszeit: [X] Min. | Portionen: [Y] +Zutaten: [Zutat 1, Zutat 2, Zutat 3] +Schritte: +1. [Schritt 1] +2. [Schritt 2] + +Antworte standardmäßig auf Deutsch. Formatiere Preise in Euro (z.B. 1,49 €).`; diff --git a/src/lib/ai/tools.ts b/src/lib/ai/tools.ts new file mode 100644 index 0000000..c0b1aed --- /dev/null +++ b/src/lib/ai/tools.ts @@ -0,0 +1,189 @@ +import 'server-only'; +import { createServerClient } from '@/lib/supabase/server'; +import type { PendingActionStatus } from '@/types/database'; + +export interface PendingActionPayload { + householdId: string; + userId: string; + actionType: + | 'ADD_SHOPPING_ITEM' + | 'SAVE_RECIPE' + | 'SAVE_MEAL_PLAN' + | 'UPDATE_PRICES'; + payload: unknown; +} + +export async function createPendingAction(params: PendingActionPayload) { + const supabase = await createServerClient(); + const { data, error } = await supabase + .from('ai_pending_actions') + .insert({ + household_id: params.householdId, + user_id: params.userId, + action_type: params.actionType, + payload_json: params.payload as any, + status: 'PENDING' as PendingActionStatus, + }) + .select() + .single(); + + if (error) { + throw new Error(`Fehler beim Erstellen der ausstehenden KI-Aktion: ${error.message}`); + } + + return data; +} + +export async function confirmPendingAction(actionId: string, userId: string) { + const supabase = await createServerClient(); + + const { data: action, error: fetchErr } = await supabase + .from('ai_pending_actions') + .select('*') + .eq('id', actionId) + .eq('user_id', userId) + .eq('status', 'PENDING') + .single(); + + if (fetchErr || !action) { + throw new Error('Ausstehende Aktion nicht gefunden oder bereits verarbeitet.'); + } + + const payload = action.payload_json as any; + const householdId = action.household_id; + + // Execute payload based on action_type + switch (action.action_type) { + case 'ADD_SHOPPING_ITEM': { + // Find active shopping list or create default + let { data: list } = await supabase + .from('shopping_lists') + .select('id') + .eq('household_id', householdId) + .eq('status', 'ACTIVE') + .limit(1) + .single(); + + if (!list) { + const { data: newList, error: createListErr } = await supabase + .from('shopping_lists') + .insert({ household_id: householdId, name: 'Einkaufsliste', status: 'ACTIVE' }) + .select('id') + .single(); + if (createListErr) throw new Error(createListErr.message); + list = newList; + } + + await supabase.from('shopping_list_items').insert({ + shopping_list_id: list.id, + product_id: payload.productId || null, + custom_name: payload.customName || payload.productName || 'Neuer Artikel', + quantity: payload.quantity || 1, + unit_id: payload.unitId || null, + estimated_unit_price_cents: payload.estimatedPriceCents || null, + selected_store: payload.selectedStore || null, + automatically_added: true, + }); + break; + } + + case 'SAVE_RECIPE': { + await supabase.from('recipes').insert({ + household_id: householdId, + title: payload.title, + description: payload.description || null, + servings: payload.servings || 2, + preparation_minutes: payload.preparationMinutes || null, + difficulty: payload.difficulty || 'EINFACH', + estimated_cost_cents: payload.estimatedAdditionalCostCents || null, + inventory_coverage_percent: payload.inventoryCoveragePercent || 0, + ingredients_json: payload.ingredients || [], + steps_json: payload.steps || [], + dietary_tags: payload.dietaryTags || [], + allergen_warnings: payload.allergenWarnings || [], + created_by_ai: true, + created_by: userId, + }); + break; + } + + case 'SAVE_MEAL_PLAN': { + await supabase.from('meal_plans').insert({ + household_id: householdId, + name: payload.name || 'KI-Wochenplan', + start_date: payload.startDate, + end_date: payload.endDate, + estimated_total_cost_cents: payload.estimatedTotalCostCents || null, + plan_json: payload.days || [], + created_by: userId, + }); + break; + } + + case 'UPDATE_PRICES': { + if (Array.isArray(payload.priceUpdates)) { + for (const update of payload.priceUpdates) { + if (update.productId && update.priceCents) { + await supabase + .from('products') + .update({ + estimated_price_cents: update.priceCents, + preferred_store: update.retailer || undefined, + price_updated_at: new Date().toISOString(), + }) + .eq('id', update.productId) + .eq('household_id', householdId); + + await supabase.from('price_observations').insert({ + household_id: householdId, + product_id: update.productId, + product_name: update.productName || 'Unbekannt', + brand: update.brand || null, + package_size: update.packageSize || null, + package_unit: update.packageUnit || null, + price_cents: update.priceCents, + base_price_cents: update.basePriceCents || null, + base_price_unit: update.basePriceUnit || null, + retailer: update.retailer || 'AI Estimate', + price_type: update.priceType || 'REGULAR', + confidence: update.confidence || 'MEDIUM', + exact_match: update.exactMatch ?? true, + source_kind: 'AI_ESTIMATE', + created_by: userId, + }); + } + } + } + break; + } + + default: + throw new Error(`Unbekannter Aktionstyp: ${action.action_type}`); + } + + // Update action status to CONFIRMED + await supabase + .from('ai_pending_actions') + .update({ + status: 'CONFIRMED', + confirmed_at: new Date().toISOString(), + }) + .eq('id', actionId); + + return { success: true }; +} + +export async function rejectPendingAction(actionId: string, userId: string) { + const supabase = await createServerClient(); + const { error } = await supabase + .from('ai_pending_actions') + .update({ status: 'REJECTED' }) + .eq('id', actionId) + .eq('user_id', userId); + + if (error) { + throw new Error(`Fehler beim Ablehnen der KI-Aktion: ${error.message}`); + } + + return { success: true }; +} diff --git a/src/lib/constants/vegan-catalog.ts b/src/lib/constants/vegan-catalog.ts new file mode 100644 index 0000000..7a202fd --- /dev/null +++ b/src/lib/constants/vegan-catalog.ts @@ -0,0 +1,191 @@ +export interface CatalogItem { + name: string; + category: string; + defaultPackageSize: number; + defaultUnit: string; + typicalMinQuantity: number; + estimatedPriceCents: number; + brand?: string; + store?: string; +} + +export const VEGAN_PRODUCT_CATALOG: CatalogItem[] = [ + // ── REWE Eigenmarken ──────────────────────────────────────────────────────── + { name: 'REWE Beste Wahl Soja-Joghurt Natur', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Soja-Joghurt Vanille', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Soja-Joghurt Mango', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Bio pflanzlich Soja Natur', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 119, brand: 'REWE Bio pflanzlich', store: 'REWE' }, + { name: 'REWE Bio pflanzlich Kokos Natur', category: 'Joghurtalternative', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 149, brand: 'REWE Bio pflanzlich', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Creme Kokosfett', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Milde Genuss-Scheiben', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 129, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Lyoner Natur', category: 'Aufschnitt', defaultPackageSize: 80, defaultUnit: 'Gramm', typicalMinQuantity: 80, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Paprika-Lyoner', category: 'Aufschnitt', defaultPackageSize: 80, defaultUnit: 'Gramm', typicalMinQuantity: 80, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Aufschnitt Grillgemüse', category: 'Aufschnitt', defaultPackageSize: 100, defaultUnit: 'Gramm', typicalMinQuantity: 100, estimatedPriceCents: 105, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Fleischwurst am Stück', category: 'Wurstalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 149, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Veganes Hack', category: 'Hackalternative', defaultPackageSize: 275, defaultUnit: 'Gramm', typicalMinQuantity: 275, estimatedPriceCents: 229, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Burger Patties', category: 'Burgeralternative', defaultPackageSize: 230, defaultUnit: 'Gramm', typicalMinQuantity: 230, estimatedPriceCents: 249, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Veganes Schnitzel gekühlt', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 199, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Veganes Schnitzel TK', category: 'Fleischalternative', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 299, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Mini-Frikadellen', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 199, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Dino-Nuggets', category: 'Fleischalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 219, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Crunchy Chili Balls', category: 'Fleischalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 229, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Nuggets mit Dip', category: 'Fleischalternative', defaultPackageSize: 300, defaultUnit: 'Gramm', typicalMinQuantity: 300, estimatedPriceCents: 249, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Veganes Cordon Bleu', category: 'Fleischalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 249, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Pizza Tonno', category: 'Pizza', defaultPackageSize: 345, defaultUnit: 'Gramm', typicalMinQuantity: 345, estimatedPriceCents: 299, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Pizza Pomodoro & Rucola', category: 'Pizza', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 299, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Kräuterbaguette vegan', category: 'Backwaren', defaultPackageSize: 175, defaultUnit: 'Gramm', typicalMinQuantity: 175, estimatedPriceCents: 99, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Chili sin Carne', category: 'Fertiggericht', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 189, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Eis Bourbon Vanille', category: 'Eis', defaultPackageSize: 500, defaultUnit: 'Milliliter', typicalMinQuantity: 500, estimatedPriceCents: 299, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Eis Cookie Dough', category: 'Eis', defaultPackageSize: 500, defaultUnit: 'Milliliter', typicalMinQuantity: 500, estimatedPriceCents: 299, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Eis Choco Cookie Dough', category: 'Eis', defaultPackageSize: 500, defaultUnit: 'Milliliter', typicalMinQuantity: 500, estimatedPriceCents: 299, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Beste Wahl Vegane Pralinen', category: 'Süßwaren', defaultPackageSize: 162.5, defaultUnit: 'Gramm', typicalMinQuantity: 162.5, estimatedPriceCents: 349, brand: 'REWE Beste Wahl', store: 'REWE' }, + { name: 'REWE Bio Haferdrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 95, brand: 'REWE Bio', store: 'REWE' }, + { name: 'REWE Bio Streichcreme', category: 'Aufstrich', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 139, brand: 'REWE Bio', store: 'REWE' }, + { name: 'REWE Bio Falafelbällchen', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 199, brand: 'REWE Bio', store: 'REWE' }, + + // ── Kaufland K-PLANT BASED ────────────────────────────────────────────────── + { name: 'K-PLANT BASED Bio Haferdrink Natur', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 95, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Barista Haferdrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 115, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Soja-Reisdrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 95, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Reisdrink Natur', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 145, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Mandeldrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 135, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Soja-Joghurt', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 179, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Tofu Natur', category: 'Tofu', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 179, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Räuchertofu', category: 'Tofu', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 149, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Bio Tofu-Geschnetzeltes', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 179, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Hummus Classic', category: 'Aufstrich', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 129, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Falafel', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 219, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Nuggets', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 219, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Mini-Schnitzel', category: 'Fleischalternative', defaultPackageSize: 300, defaultUnit: 'Gramm', typicalMinQuantity: 300, estimatedPriceCents: 229, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Burgerscheiben', category: 'Burgeralternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 199, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Infinity Burger Patties', category: 'Burgeralternative', defaultPackageSize: 227, defaultUnit: 'Gramm', typicalMinQuantity: 227, estimatedPriceCents: 249, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Hackalternative', category: 'Hackalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 149, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Bratwurst', category: 'Wurstalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 219, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Steaks Hähnchen-Art', category: 'Fleischalternative', defaultPackageSize: 160, defaultUnit: 'Gramm', typicalMinQuantity: 160, estimatedPriceCents: 229, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Fischstäbchen', category: 'Fischalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 219, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Veganer Räucherlax', category: 'Fischalternative', defaultPackageSize: 80, defaultUnit: 'Gramm', typicalMinQuantity: 80, estimatedPriceCents: 179, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Genießer-Scheiben', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Veganer Reibegenuss', category: 'Käsealternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 129, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Creme Zaziki-Art', category: 'Aufstrich', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 129, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Salatmayonnaise', category: 'Mayonnaise', defaultPackageSize: 250, defaultUnit: 'Milliliter', typicalMinQuantity: 250, estimatedPriceCents: 149, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Gemüseaufstrich', category: 'Aufstrich', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 149, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Mango-Curry-Aufstrich', category: 'Aufstrich', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 149, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Vegane Pizza', category: 'Pizza', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 249, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Veganes Vanilleeis', category: 'Eis', defaultPackageSize: 500, defaultUnit: 'Milliliter', typicalMinQuantity: 500, estimatedPriceCents: 249, brand: 'K-PLANT BASED', store: 'Kaufland' }, + { name: 'K-PLANT BASED Drink Meal Kakao', category: 'Fertiger Getränkedrink', defaultPackageSize: 500, defaultUnit: 'Milliliter', typicalMinQuantity: 500, estimatedPriceCents: 199, brand: 'K-PLANT BASED', store: 'Kaufland' }, + + // ── Lidl Vemondo ──────────────────────────────────────────────────────────── + { name: 'Vemondo Bio Haferdrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 95, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Barista Haferdrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 125, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Bio Sojadrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 95, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Mandeldrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 125, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Kokos-Reisdrink', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 125, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Sojajoghurt Natur', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Sojajoghurt Vanille', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Sojajoghurt Frucht', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Kokosdessert', category: 'Dessert', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 59, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo No Ghurt', category: 'Joghurtalternative', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 129, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo No Butter', category: 'Butteralternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Scheiben mild/würzig', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Veganer Reibegenuss', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Veganer Streichgenuss', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Feta-Alternative', category: 'Käsealternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 149, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Kochcreme', category: 'Sahnealternative', defaultPackageSize: 200, defaultUnit: 'Milliliter', typicalMinQuantity: 200, estimatedPriceCents: 89, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Hackalternative', category: 'Hackalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 149, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Chunks', category: 'Fleischalternative', defaultPackageSize: 185, defaultUnit: 'Gramm', typicalMinQuantity: 185, estimatedPriceCents: 229, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Veganes Gyros', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Bratwurst', category: 'Wurstalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Burger Patties', category: 'Burgeralternative', defaultPackageSize: 227, defaultUnit: 'Gramm', typicalMinQuantity: 227, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Mini-Schnitzel', category: 'Fleischalternative', defaultPackageSize: 300, defaultUnit: 'Gramm', typicalMinQuantity: 300, estimatedPriceCents: 219, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Nuggets', category: 'Fleischalternative', defaultPackageSize: 300, defaultUnit: 'Gramm', typicalMinQuantity: 300, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Fischstäbchen', category: 'Fischalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 219, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Falafel', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 179, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Snack-Frikadellen', category: 'Fleischalternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 179, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Tofu Natur', category: 'Tofu', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 179, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Räuchertofu', category: 'Tofu', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 149, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Veganes Pesto', category: 'Pesto', defaultPackageSize: 190, defaultUnit: 'Gramm', typicalMinQuantity: 190, estimatedPriceCents: 149, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Pizza Margherita', category: 'Pizza', defaultPackageSize: 350, defaultUnit: 'Gramm', typicalMinQuantity: 350, estimatedPriceCents: 249, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Pizza Salami', category: 'Pizza', defaultPackageSize: 350, defaultUnit: 'Gramm', typicalMinQuantity: 350, estimatedPriceCents: 249, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Nuss-Nougat-Creme', category: 'Süßer Aufstrich', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Karamell-Kekse', category: 'Kekse', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 149, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Vegane Muffins', category: 'Backwaren', defaultPackageSize: 1, defaultUnit: 'Stück', typicalMinQuantity: 1, estimatedPriceCents: 199, brand: 'Vemondo', store: 'Lidl' }, + { name: 'Vemondo Veganes Eis', category: 'Eis', defaultPackageSize: 500, defaultUnit: 'Milliliter', typicalMinQuantity: 500, estimatedPriceCents: 249, brand: 'Vemondo', store: 'Lidl' }, + + // ── ALDI MYVAY ───────────────────────────────────────────────────────────── + { name: 'MYVAY Bio Haferdrink Natur', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 90, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Bio Haferdrink Mandel', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 90, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Bio Haferdrink ungesüßt', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 90, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Bio Mandeldrink ungesüßt', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 135, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Sojaghurt Natur', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Sojaghurt Vanille', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Sojaghurt Mango', category: 'Joghurtalternative', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Veganes Hack gegart', category: 'Hackalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 159, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Wonder Chunks Chicken', category: 'Fleischalternative', defaultPackageSize: 185, defaultUnit: 'Gramm', typicalMinQuantity: 185, estimatedPriceCents: 245, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Wonder Chunks Döner', category: 'Fleischalternative', defaultPackageSize: 185, defaultUnit: 'Gramm', typicalMinQuantity: 185, estimatedPriceCents: 245, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Falafel', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 179, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Gemüsebällchen', category: 'Fleischalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 179, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Nuggets/Schnitzel', category: 'Fleischalternative', defaultPackageSize: 300, defaultUnit: 'Gramm', typicalMinQuantity: 300, estimatedPriceCents: 219, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Grillwürste', category: 'Wurstalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 249, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY American Burger', category: 'Burgeralternative', defaultPackageSize: 227, defaultUnit: 'Gramm', typicalMinQuantity: 227, estimatedPriceCents: 225, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Crispy Chicken Burger', category: 'Burgeralternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 225, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Bio Tofu', category: 'Tofu', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 229, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Veganer Aufschnitt', category: 'Aufschnitt', defaultPackageSize: 100, defaultUnit: 'Gramm', typicalMinQuantity: 100, estimatedPriceCents: 95, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Salami', category: 'Aufschnitt', defaultPackageSize: 80, defaultUnit: 'Gramm', typicalMinQuantity: 80, estimatedPriceCents: 129, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Streichgenuss Natur', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Streichgenuss Kräuter', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Veganer Reibegenuss', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Scheiben mild', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Vegane Scheiben würzig', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Der Lockere Aufstrich', category: 'Aufstrich', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 129, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Feinkost-Mix Gurke/Kräuter', category: 'Feinkostsalat', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Feinkost-Mix Ei-Schnittlauch', category: 'Feinkostsalat', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 99, brand: 'MYVAY', store: 'ALDI' }, + { name: 'MYVAY Bio Streichcreme', category: 'Aufstrich', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 125, brand: 'MYVAY', store: 'ALDI' }, + + // ── Bekannte Markenprodukte ───────────────────────────────────────────────── + { name: 'Alpro Haferdrink Original', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 189, brand: 'Alpro' }, + { name: 'Alpro Barista Hafer', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 229, brand: 'Alpro' }, + { name: 'Alpro Sojajoghurt Natur', category: 'Joghurtalternative', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 219, brand: 'Alpro' }, + { name: 'Oatly Barista Edition', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 229, brand: 'Oatly' }, + { name: 'Oatly Oatgurt', category: 'Joghurtalternative', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 199, brand: 'Oatly' }, + { name: 'vly Erbsendrink Barista', category: 'Pflanzendrink', defaultPackageSize: 1, defaultUnit: 'Liter', typicalMinQuantity: 1, estimatedPriceCents: 229, brand: 'vly' }, + { name: 'Rügenwalder Veganes Mühlen Mett', category: 'Aufstrich', defaultPackageSize: 100, defaultUnit: 'Gramm', typicalMinQuantity: 100, estimatedPriceCents: 249, brand: 'Rügenwalder Mühle' }, + { name: 'Rügenwalder Veganes Mühlen Hack', category: 'Hackalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 249, brand: 'Rügenwalder Mühle' }, + { name: 'Rügenwalder Veganes Schnitzel', category: 'Fleischalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 279, brand: 'Rügenwalder Mühle' }, + { name: 'Rügenwalder Vegane Mini-Frikadellen', category: 'Fleischalternative', defaultPackageSize: 165, defaultUnit: 'Gramm', typicalMinQuantity: 165, estimatedPriceCents: 279, brand: 'Rügenwalder Mühle' }, + { name: 'Billie Green Vegane Salami', category: 'Aufschnitt', defaultPackageSize: 80, defaultUnit: 'Gramm', typicalMinQuantity: 80, estimatedPriceCents: 179, brand: 'Billie Green' }, + { name: 'Billie Green Veganer Bacon', category: 'Aufschnitt', defaultPackageSize: 90, defaultUnit: 'Gramm', typicalMinQuantity: 90, estimatedPriceCents: 199, brand: 'Billie Green' }, + { name: 'Billie Green Vegane Schinkenwürfel', category: 'Aufschnitt', defaultPackageSize: 90, defaultUnit: 'Gramm', typicalMinQuantity: 90, estimatedPriceCents: 199, brand: 'Billie Green' }, + { name: 'Billie Green Vegane Bratwurst', category: 'Wurstalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 329, brand: 'Billie Green' }, + { name: 'LikeMeat Like Chicken', category: 'Fleischalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 299, brand: 'LikeMeat' }, + { name: 'LikeMeat Like Gyros', category: 'Fleischalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 299, brand: 'LikeMeat' }, + { name: 'The Vegetarian Butcher Veganes Hack', category: 'Hackalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 349, brand: 'The Vegetarian Butcher' }, + { name: 'The Vegetarian Butcher Chickimicki', category: 'Fleischalternative', defaultPackageSize: 180, defaultUnit: 'Gramm', typicalMinQuantity: 180, estimatedPriceCents: 349, brand: 'The Vegetarian Butcher' }, + { name: 'Beyond Meat Beyond Burger', category: 'Burgeralternative', defaultPackageSize: 226, defaultUnit: 'Gramm', typicalMinQuantity: 226, estimatedPriceCents: 429, brand: 'Beyond Meat' }, + { name: 'Beyond Meat Beyond Hack', category: 'Hackalternative', defaultPackageSize: 250, defaultUnit: 'Gramm', typicalMinQuantity: 250, estimatedPriceCents: 399, brand: 'Beyond Meat' }, + { name: 'Beyond Meat Beyond Sausage', category: 'Wurstalternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 399, brand: 'Beyond Meat' }, + { name: 'planted. Chicken Chunks', category: 'Fleischalternative', defaultPackageSize: 160, defaultUnit: 'Gramm', typicalMinQuantity: 160, estimatedPriceCents: 349, brand: 'planted.' }, + { name: 'Garden Gourmet Sensational Burger', category: 'Burgeralternative', defaultPackageSize: 226, defaultUnit: 'Gramm', typicalMinQuantity: 226, estimatedPriceCents: 349, brand: 'Garden Gourmet' }, + { name: 'Simply V Genießerscheiben', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 229, brand: 'Simply V' }, + { name: 'Simply V Reibegenuss', category: 'Käsealternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 279, brand: 'Simply V' }, + { name: 'Violife Scheiben Mild', category: 'Käsealternative', defaultPackageSize: 140, defaultUnit: 'Gramm', typicalMinQuantity: 140, estimatedPriceCents: 249, brand: 'Violife' }, + { name: 'Violife Greek White Block', category: 'Käsealternative', defaultPackageSize: 200, defaultUnit: 'Gramm', typicalMinQuantity: 200, estimatedPriceCents: 249, brand: 'Violife' }, + { name: 'Bedda Vegane Scheiben', category: 'Käsealternative', defaultPackageSize: 150, defaultUnit: 'Gramm', typicalMinQuantity: 150, estimatedPriceCents: 249, brand: 'Bedda' }, + { name: 'Dr. Oetker Ristorante Vegana', category: 'Pizza', defaultPackageSize: 340, defaultUnit: 'Gramm', typicalMinQuantity: 340, estimatedPriceCents: 349, brand: 'Dr. Oetker' }, + { name: 'Ben & Jerry’s Non-Dairy Eis', category: 'Eis', defaultPackageSize: 465, defaultUnit: 'Milliliter', typicalMinQuantity: 465, estimatedPriceCents: 599, brand: 'Ben & Jerry’s' }, + { name: 'Magnum Vegan Stieleis', category: 'Eis', defaultPackageSize: 3, defaultUnit: 'Stück', typicalMinQuantity: 3, estimatedPriceCents: 399, brand: 'Magnum' }, + { name: 'Bionella Bio Vegane Nuss-Nougat-Creme', category: 'Süßer Aufstrich', defaultPackageSize: 400, defaultUnit: 'Gramm', typicalMinQuantity: 400, estimatedPriceCents: 449, brand: 'Bionella' }, + + // ── Grundnahrungsmittel & Vorrats-Basics ────────────────────────────────────── + { name: 'Spaghetti', category: 'Grundnahrungsmittel', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 189 }, + { name: 'Penne Nudeln', category: 'Grundnahrungsmittel', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 189 }, + { name: 'Basmati Reis', category: 'Grundnahrungsmittel', defaultPackageSize: 1000, defaultUnit: 'Gramm', typicalMinQuantity: 1000, estimatedPriceCents: 349 }, + { name: 'Zarte Haferflocken', category: 'Grundnahrungsmittel', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 99 }, + { name: 'Rote Linsen', category: 'Grundnahrungsmittel', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 169 }, + { name: 'Weizenmehl Type 405', category: 'Grundnahrungsmittel', defaultPackageSize: 1000, defaultUnit: 'Gramm', typicalMinQuantity: 1000, estimatedPriceCents: 129 }, + { name: 'Hefeflocken', category: 'Gewürze', defaultPackageSize: 100, defaultUnit: 'Gramm', typicalMinQuantity: 100, estimatedPriceCents: 299 }, + { name: 'Paprikapulver edelsüß', category: 'Gewürze', defaultPackageSize: 1, defaultUnit: 'Dose', typicalMinQuantity: 1, estimatedPriceCents: 199 }, + { name: 'Strauchtomaten', category: 'Obst und Gemüse', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 229 }, + { name: 'Speisekartoffeln', category: 'Obst und Gemüse', defaultPackageSize: 1, defaultUnit: 'Kilogramm', typicalMinQuantity: 1, estimatedPriceCents: 199 }, + { name: 'Speisezwiebeln', category: 'Obst und Gemüse', defaultPackageSize: 1000, defaultUnit: 'Gramm', typicalMinQuantity: 1000, estimatedPriceCents: 149 }, + { name: 'Bio-Zitronen', category: 'Obst und Gemüse', defaultPackageSize: 500, defaultUnit: 'Gramm', typicalMinQuantity: 500, estimatedPriceCents: 179 }, +]; diff --git a/src/lib/formatting/currency.ts b/src/lib/formatting/currency.ts new file mode 100644 index 0000000..7f7a73d --- /dev/null +++ b/src/lib/formatting/currency.ts @@ -0,0 +1,73 @@ +/** + * Formats an integer amount in cents to German Euro representation. + * Example: 249 -> "2,49 €" + */ +export function formatCurrency(cents: number | null | undefined): string { + if (cents === null || cents === undefined || isNaN(cents)) { + return '0,00 €'; + } + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(cents / 100); +} + +/** + * Calculates stock value in cents. + * For integer/package units: quantity * packagePriceCents + * For partial packages: (quantity / packageSize) * packagePriceCents + */ +export function calculateStockValueCents( + quantity: number, + packageSize: number, + packagePriceCents: number | null | undefined +): number { + if (!packagePriceCents || packagePriceCents <= 0 || quantity <= 0) { + return 0; + } + if (packageSize <= 0) { + return Math.round(quantity * packagePriceCents); + } + const fraction = quantity / packageSize; + return Math.round(fraction * packagePriceCents); +} + +/** + * Calculates base price (Grundpreis) per 1kg, 1l, 100g, or 1 Stück. + */ +export function calculateBasePrice( + priceCents: number, + packageSize: number, + packageUnit: string +): { basePriceCents: number; basePriceUnit: string } { + if (packageSize <= 0) { + return { basePriceCents: priceCents, basePriceUnit: packageUnit }; + } + + const unitLower = packageUnit.toLowerCase().trim(); + + if (unitLower === 'g' || unitLower === 'gramm') { + // Standard base price per 1 kg (1000 g) + const basePriceCents = Math.round((priceCents / packageSize) * 1000); + return { basePriceCents, basePriceUnit: '1 kg' }; + } + + if (unitLower === 'ml' || unitLower === 'milliliter') { + // Standard base price per 1 l (1000 ml) + const basePriceCents = Math.round((priceCents / packageSize) * 1000); + return { basePriceCents, basePriceUnit: '1 l' }; + } + + if (unitLower === 'kg' || unitLower === 'kilogramm') { + const basePriceCents = Math.round(priceCents / packageSize); + return { basePriceCents, basePriceUnit: '1 kg' }; + } + + if (unitLower === 'l' || unitLower === 'liter') { + const basePriceCents = Math.round(priceCents / packageSize); + return { basePriceCents, basePriceUnit: '1 l' }; + } + + const basePriceCents = Math.round(priceCents / packageSize); + return { basePriceCents, basePriceUnit: `1 ${packageUnit}` }; +} diff --git a/src/lib/formatting/date.ts b/src/lib/formatting/date.ts new file mode 100644 index 0000000..62889b8 --- /dev/null +++ b/src/lib/formatting/date.ts @@ -0,0 +1,53 @@ +import { format, parseISO, differenceInCalendarDays, isValid } from 'date-fns'; +import { de } from 'date-fns/locale'; +import type { ExpirationWarningStatus } from '@/types/domain'; + +export function formatDateGerman(dateStr: string | Date | null | undefined): string { + if (!dateStr) return '-'; + const date = typeof dateStr === 'string' ? parseISO(dateStr) : dateStr; + if (!isValid(date)) return '-'; + return format(date, 'dd.MM.yyyy', { locale: de }); +} + +export function formatDateTimeGerman(dateStr: string | Date | null | undefined): string { + if (!dateStr) return '-'; + const date = typeof dateStr === 'string' ? parseISO(dateStr) : dateStr; + if (!isValid(date)) return '-'; + return format(date, 'dd.MM.yyyy HH:mm', { locale: de }); +} + +export function getExpirationStatus(expirationDateStr: string | null | undefined): ExpirationWarningStatus { + if (!expirationDateStr) { + return { status: 'OK', label: 'Kein Ablaufdatum', daysRemaining: null }; + } + + const expDate = parseISO(expirationDateStr); + if (!isValid(expDate)) { + return { status: 'OK', label: 'Kein Ablaufdatum', daysRemaining: null }; + } + + const today = new Date(); + const daysDiff = differenceInCalendarDays(expDate, today); + + if (daysDiff < 0) { + return { + status: 'EXPIRED', + label: `Abgelaufen (${Math.abs(daysDiff)} Tag${Math.abs(daysDiff) === 1 ? '' : 'e'})`, + daysRemaining: daysDiff, + }; + } + if (daysDiff === 0) { + return { status: 'TODAY', label: 'Läuft heute ab', daysRemaining: 0 }; + } + if (daysDiff <= 3) { + return { status: 'IN_3_DAYS', label: `Läuft in ${daysDiff} Tagen ab`, daysRemaining: daysDiff }; + } + if (daysDiff <= 7) { + return { status: 'IN_7_DAYS', label: `Läuft in ${daysDiff} Tagen ab`, daysRemaining: daysDiff }; + } + if (daysDiff <= 14) { + return { status: 'IN_14_DAYS', label: `Läuft in ${daysDiff} Tagen ab`, daysRemaining: daysDiff }; + } + + return { status: 'OK', label: `Haltbar bis ${formatDateGerman(expDate)}`, daysRemaining: daysDiff }; +} diff --git a/src/lib/formatting/step-size.ts b/src/lib/formatting/step-size.ts new file mode 100644 index 0000000..c8847fb --- /dev/null +++ b/src/lib/formatting/step-size.ts @@ -0,0 +1,43 @@ +export const GRAM_SLIDER_STEPS = [50, 100, 250, 500, 1000]; + +export function isGramUnit(packageUnit: string): boolean { + const u = (packageUnit || '').toLowerCase().trim(); + return u === 'gramm' || u === 'g'; +} + +export function getProductStepSize( + packageUnit: string, + categoryName?: string | null, + productName?: string | null +): { step: number; label: string } { + const unit = (packageUnit || '').toLowerCase().trim(); + const name = (productName || '').toLowerCase().trim(); + const cat = (categoryName || '').toLowerCase().trim(); + + if (unit === 'gramm' || unit === 'g') { + const isSpice = + cat.includes('gewürz') || + /paprika|hefe|gewürz|salz|pfeffer|curry|zimt|oregano|basilikum|thymian|knoblauch|chili|kümmel|rosmarin|muskat|kurkuma/i.test( + name + ); + + if (isSpice) { + return { step: 50, label: '50g' }; + } + return { step: 250, label: '250g' }; + } + + if (unit === 'kilogramm' || unit === 'kg') { + return { step: 1, label: '1kg' }; + } + + if (unit === 'liter' || unit === 'l') { + return { step: 1, label: '1l' }; + } + + if (unit === 'milliliter' || unit === 'ml') { + return { step: 250, label: '250ml' }; + } + + return { step: 1, label: '1' }; +} diff --git a/src/lib/supabase/admin.ts b/src/lib/supabase/admin.ts new file mode 100644 index 0000000..149f6aa --- /dev/null +++ b/src/lib/supabase/admin.ts @@ -0,0 +1,18 @@ +import 'server-only'; +import { createClient } from '@supabase/supabase-js'; + +export function createAdminClient() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !serviceRoleKey) { + throw new Error('Fehlende Supabase Admin Umgebungsvariablen (NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY)'); + } + + return createClient(supabaseUrl, serviceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} diff --git a/src/lib/supabase/browser.ts b/src/lib/supabase/browser.ts new file mode 100644 index 0000000..67a19f6 --- /dev/null +++ b/src/lib/supabase/browser.ts @@ -0,0 +1,12 @@ +import { createBrowserClient as createBrowserSupabaseClient } from '@supabase/ssr'; + +export function createBrowserClient() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + throw new Error('Fehlende Supabase Umgebungsvariablen (NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY)'); + } + + return createBrowserSupabaseClient(supabaseUrl, supabaseAnonKey); +} diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts new file mode 100644 index 0000000..b5a703e --- /dev/null +++ b/src/lib/supabase/middleware.ts @@ -0,0 +1,85 @@ +import { createServerClient } from '@supabase/ssr'; +import { NextResponse, type NextRequest } from 'next/server'; + +export async function updateSession(request: NextRequest) { + let response = NextResponse.next({ + request: { + headers: request.headers, + }, + }); + + // Check for local Amy/Mandy session cookie fallback + const localUserCookie = request.cookies.get('kitchenstock_user')?.value; + let user: any = null; + + if (localUserCookie) { + try { + user = JSON.parse(localUserCookie); + } catch { + user = null; + } + } + + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!user && supabaseUrl && supabaseAnonKey && !supabaseUrl.includes('your-supabase-project')) { + try { + const supabase = createServerClient(supabaseUrl, supabaseAnonKey, { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet: Array<{ name: string; value: string; options?: any }>) { + cookiesToSet.forEach(({ name, value }) => + request.cookies.set(name, value) + ); + response = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => + response.cookies.set(name, value, options) + ); + }, + }, + }); + + const { data } = await supabase.auth.getUser(); + user = data.user; + } catch { + user = null; + } + } + + const isAuthRoute = + request.nextUrl.pathname.startsWith('/login') || + request.nextUrl.pathname.startsWith('/register') || + request.nextUrl.pathname.startsWith('/forgot-password') || + request.nextUrl.pathname.startsWith('/reset-password'); + + const isDashboardRoute = + request.nextUrl.pathname.startsWith('/dashboard') || + request.nextUrl.pathname.startsWith('/inventory') || + request.nextUrl.pathname.startsWith('/shopping') || + request.nextUrl.pathname.startsWith('/inventory-check') || + request.nextUrl.pathname.startsWith('/recipes') || + request.nextUrl.pathname.startsWith('/meal-plans') || + request.nextUrl.pathname.startsWith('/ai-assistant') || + request.nextUrl.pathname.startsWith('/statistics') || + request.nextUrl.pathname.startsWith('/settings'); + + if (!user && isDashboardRoute) { + const url = request.nextUrl.clone(); + url.pathname = '/login'; + url.searchParams.set('redirectTo', request.nextUrl.pathname); + return NextResponse.redirect(url); + } + + if (user && isAuthRoute) { + const url = request.nextUrl.clone(); + url.pathname = '/dashboard'; + return NextResponse.redirect(url); + } + + return response; +} diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts new file mode 100644 index 0000000..a0aeca0 --- /dev/null +++ b/src/lib/supabase/server.ts @@ -0,0 +1,193 @@ +import { createServerClient as createSSRServerClient } from '@supabase/ssr'; +import { cookies } from 'next/headers'; + +// 100% Vegan default mock products with realistic units (500g, 1l, 200g) +const MOCK_PRODUCTS = [ + { + id: 'p-1', + household_id: 'demo-household-id', + name: 'Hafermilch Barista Bio', + brand: 'Oatly', + package_size: 1, + package_unit: 'Liter', + minimum_quantity: 2, + favorite: true, + estimated_price_cents: 149, + categories: { name: 'Pflanzliche Milch' }, + locations: { name: 'Kühlschrank' }, + inventory_batches: [{ id: 'b-1', quantity: 3, expiration_date: '2026-10-15', purchase_price_cents: 149 }], + }, + { + id: 'p-2', + household_id: 'demo-household-id', + name: 'Spaghetti', + brand: 'Barilla', + package_size: 500, + package_unit: 'Gramm', + minimum_quantity: 1, + favorite: true, + estimated_price_cents: 189, + categories: { name: 'Grundnahrungsmittel' }, + locations: { name: 'Vorratsschrank' }, + inventory_batches: [{ id: 'b-2', quantity: 2, expiration_date: '2027-01-20', purchase_price_cents: 189 }], + }, + { + id: 'p-3', + household_id: 'demo-household-id', + name: 'Tofu Natur Bio', + brand: 'Taifun', + package_size: 200, + package_unit: 'Gramm', + minimum_quantity: 1, + favorite: true, + estimated_price_cents: 199, + categories: { name: 'Kühlwaren' }, + locations: { name: 'Kühlschrank' }, + inventory_batches: [{ id: 'b-3', quantity: 2, expiration_date: '2026-08-10', purchase_price_cents: 199 }], + }, + { + id: 'p-4', + household_id: 'demo-household-id', + name: 'Hefeflocken', + brand: 'Alnatura', + package_size: 200, + package_unit: 'Gramm', + minimum_quantity: 1, + favorite: true, + estimated_price_cents: 299, + categories: { name: 'Gewürze' }, + locations: { name: 'Gewürzschrank' }, + inventory_batches: [{ id: 'b-4', quantity: 1, expiration_date: '2027-05-01', purchase_price_cents: 299 }], + }, + { + id: 'p-5', + household_id: 'demo-household-id', + name: 'Strauchtomaten', + brand: 'Bio', + package_size: 500, + package_unit: 'Gramm', + minimum_quantity: 1, + favorite: false, + estimated_price_cents: 229, + categories: { name: 'Obst und Gemüse' }, + locations: { name: 'Kühlschrank' }, + inventory_batches: [{ id: 'b-5', quantity: 1, expiration_date: '2026-07-28', purchase_price_cents: 229 }], + }, +]; + +function createMockQueryBuilder(tableName: string) { + const builder: any = { + select: () => builder, + eq: () => builder, + neq: () => builder, + gt: () => builder, + gte: () => builder, + lt: () => builder, + lte: () => builder, + order: () => builder, + limit: () => builder, + insert: () => builder, + update: () => builder, + delete: () => builder, + upsert: () => builder, + single: async () => { + if (tableName === 'household_members' || tableName === 'households') { + return { + data: { + household_id: 'demo-household-id', + user_id: 'demo-user-id', + role: 'OWNER', + households: { id: 'demo-household-id', name: 'Amy & Mandys Küche' }, + id: 'demo-household-id', + name: 'Amy & Mandys Küche', + }, + error: null, + }; + } + return { data: null, error: null }; + }, + then: (resolve: (res: any) => void) => { + if (tableName === 'products') { + resolve({ data: MOCK_PRODUCTS, error: null }); + } else if (tableName === 'shopping_lists') { + resolve({ + data: [ + { + id: 'sl-1', + name: 'Einkaufsliste', + status: 'ACTIVE', + shopping_list_items: [ + { id: 'sli-1', quantity: 2, estimated_unit_price_cents: 149, checked: false }, + ], + }, + ], + error: null, + }); + } else { + resolve({ data: [], error: null }); + } + }, + }; + return builder; +} + +export async function createServerClient() { + const cookieStore = await cookies(); + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + const localUserCookie = cookieStore.get('kitchenstock_user')?.value; + let localUser: any = null; + + if (localUserCookie) { + try { + localUser = JSON.parse(localUserCookie); + } catch { + localUser = null; + } + } + + if (!supabaseUrl || !supabaseAnonKey || supabaseUrl.includes('your-supabase-project') || localUser) { + return { + auth: { + getUser: async () => ({ + data: { + user: { + id: localUser?.id || 'demo-user-id', + email: localUser?.email || 'amy@kitchenstock.local', + user_metadata: { display_name: localUser?.displayName || 'Amy' }, + }, + }, + error: null, + }), + }, + from: (table: string) => createMockQueryBuilder(table), + rpc: async (fn: string) => { + if (fn === 'adjust_product_stock') return { data: 5, error: null }; + if (fn === 'check_rate_limit') return { data: true, error: null }; + return { data: null, error: null }; + }, + } as any; + } + + return createSSRServerClient( + supabaseUrl, + supabaseAnonKey, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet: Array<{ name: string; value: string; options?: any }>) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ); + } catch { + // Ignored in read-only context + } + }, + }, + } + ); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..2819a83 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/lib/validation/realism-check.ts b/src/lib/validation/realism-check.ts new file mode 100644 index 0000000..153e746 --- /dev/null +++ b/src/lib/validation/realism-check.ts @@ -0,0 +1,61 @@ +export interface RealismCheckResult { + isUnrealistic: boolean; + warning?: string; + suggestedQuantity?: number; + suggestedUnit?: string; +} + +export function validateRealism( + productName: string, + quantity: number, + unit: string +): RealismCheckResult { + if (!productName || quantity <= 0) { + return { isUnrealistic: false }; + } + + const nameLower = productName.toLowerCase(); + const unitLower = unit.toLowerCase(); + + const isBulkFood = + nameLower.includes('nudel') || + nameLower.includes('spaghetti') || + nameLower.includes('penne') || + nameLower.includes('reis') || + nameLower.includes('mehl') || + nameLower.includes('haferflock') || + nameLower.includes('linse') || + nameLower.includes('zucker'); + + // Check 1: Unrealistically tiny weight for bulk foods (e.g. 4g Nudeln) + if (isBulkFood && (unitLower === 'g' || unitLower === 'gramm') && quantity < 100) { + return { + isUnrealistic: true, + warning: `Unrealistische Menge: ${quantity}g ${productName} ist zu wenig (Packungen wiegen meist 500g).`, + suggestedQuantity: 500, + suggestedUnit: 'Gramm', + }; + } + + // Check 2: Massive weight confusion (e.g. 500 kg Nudeln instead of 500g) + if (isBulkFood && (unitLower === 'kg' || unitLower === 'kilogramm') && quantity >= 50) { + return { + isUnrealistic: true, + warning: `Meinst du ${quantity} Gramm anstelle von ${quantity} kg ${productName}?`, + suggestedQuantity: quantity, + suggestedUnit: 'Gramm', + }; + } + + // Check 3: Liquid in grams or solid in liters + if ((nameLower.includes('milch') || nameLower.includes('saft') || nameLower.includes('wasser')) && unitLower === 'gramm') { + return { + isUnrealistic: true, + warning: `Flüssigkeiten werden meist in Litern (l) oder ml angegeben.`, + suggestedQuantity: 1, + suggestedUnit: 'Liter', + }; + } + + return { isUnrealistic: false }; +} diff --git a/src/lib/validation/vegan-check.ts b/src/lib/validation/vegan-check.ts new file mode 100644 index 0000000..8c2aeb8 --- /dev/null +++ b/src/lib/validation/vegan-check.ts @@ -0,0 +1,75 @@ +export interface VeganCheckResult { + isVegan: boolean; + warning?: string; + nonVeganIngredientFound?: string; +} + +const NON_VEGAN_KEYWORDS = [ + 'kuhmilch', + 'milch', + 'hackfleisch', + 'fleisch', + 'rindfleisch', + 'schweinefleisch', + 'hähnchen', + 'geflügel', + 'wurst', + 'salami', + 'schinken', + 'käse', + 'gouda', + 'mozzarella', + 'parmesan', + 'butter', + 'sahne', + 'eier', + 'eigelb', + 'eiwolle', + 'honig', + 'fisch', + 'lachs', + 'thunfisch', + 'gelatine', +]; + +const VEGAN_QUALIFIERS = [ + 'vegan', + 'veganer', + 'veganes', + 'pflanzlich', + 'pflanzliche', + 'hafer', + 'soja', + 'mandel', + 'kokos', + 'erbsen', + 'reis', + 'ohne ei', +]; + +export function checkIsVegan(name: string): VeganCheckResult { + if (!name || name.trim().length === 0) { + return { isVegan: true }; + } + + const cleanName = name.toLowerCase(); + + // Check if explicit vegan qualifiers are present (e.g. "Vegane Sahne", "Hafermilch") + const hasVeganQualifier = VEGAN_QUALIFIERS.some(qualifier => cleanName.includes(qualifier)); + if (hasVeganQualifier) { + return { isVegan: true }; + } + + // Check for non-vegan keywords + for (const keyword of NON_VEGAN_KEYWORDS) { + if (cleanName.includes(keyword)) { + return { + isVegan: false, + warning: `⚠️ NICHT VEGAN: "${name}" enthält voraussichtlich tierische Bestandteile!`, + nonVeganIngredientFound: keyword, + }; + } + } + + return { isVegan: true }; +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..fc48fb4 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,12 @@ +import { type NextRequest } from 'next/server'; +import { updateSession } from '@/lib/supabase/middleware'; + +export async function middleware(request: NextRequest) { + return await updateSession(request); +} + +export const config = { + matcher: [ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}; diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..b3df473 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,53 @@ +export interface ApiErrorResponse { + error: { + code: string; + message: string; + requestId: string; + details?: unknown; + }; +} + +export interface ApiResponse { + data?: T; + error?: { + code: string; + message: string; + requestId: string; + }; +} + +export interface AdjustStockInput { + householdId: string; + productId: string; + batchId?: string; + quantityChange: number; + reason: 'PURCHASE' | 'CONSUMED' | 'CORRECTION' | 'INVENTORY' | 'EXPIRED' | 'DISCARDED' | 'TRANSFER' | 'INITIAL'; + note?: string; +} + +export interface CompleteShoppingInput { + householdId: string; + shoppingListId: string; + store: string; + purchaseDate?: string; + items: Array<{ + itemId: string; + productId?: string; + customName?: string; + actualQuantity: number; + actualUnitPriceCents: number; + }>; +} + +export interface PriceSearchApiInput { + householdId: string; + query: string; + brand?: string; + retailer?: string; +} + +export interface PriceUpdateBatchInput { + householdId: string; + productIds?: string[]; + maxProducts?: number; +} diff --git a/src/types/database.ts b/src/types/database.ts new file mode 100644 index 0000000..4806b3c --- /dev/null +++ b/src/types/database.ts @@ -0,0 +1,317 @@ +export type HouseholdRole = 'OWNER' | 'ADMIN' | 'MEMBER' | 'VIEWER'; +export type AldiRegionType = 'NORD' | 'SUED' | 'UNSPECIFIED'; +export type UnitType = 'COUNT' | 'WEIGHT' | 'VOLUME' | 'OTHER'; +export type StockMovementReason = 'PURCHASE' | 'CONSUMED' | 'CORRECTION' | 'INVENTORY' | 'EXPIRED' | 'DISCARDED' | 'TRANSFER' | 'INITIAL'; +export type ShoppingListStatus = 'ACTIVE' | 'COMPLETED' | 'ARCHIVED'; +export type PriceType = 'REGULAR' | 'OFFER' | 'LOYALTY' | 'UNKNOWN'; +export type SourceKind = 'USER_PURCHASE' | 'USER_ESTIMATE' | 'OFFICIAL_RETAILER' | 'OFFICIAL_PROSPECT' | 'GEMINI_GROUNDED' | 'HISTORICAL_AVERAGE' | 'AI_ESTIMATE'; +export type ConfidenceLevel = 'HIGH' | 'MEDIUM' | 'LOW'; +export type BudgetPeriod = 'WEEKLY' | 'MONTHLY' | 'CUSTOM'; +export type InventorySessionStatus = 'OPEN' | 'COMPLETED' | 'CANCELLED'; +export type DietType = 'OMNIVORE' | 'VEGETARIAN' | 'VEGAN' | 'PESCETARIAN' | 'CUSTOM'; +export type PendingActionStatus = 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'EXPIRED'; + +export interface ProfileRow { + id: string; + display_name: string; + avatar_path: string | null; + locale: string; + timezone: string; + created_at: string; + updated_at: string; +} + +export interface HouseholdRow { + id: string; + name: string; + currency: string; + locale: string; + timezone: string; + postal_code: string | null; + city: string | null; + aldi_region: AldiRegionType; + default_servings: number; + owner_id: string; + created_at: string; + updated_at: string; +} + +export interface HouseholdMemberRow { + household_id: string; + user_id: string; + role: HouseholdRole; + created_at: string; +} + +export interface CategoryRow { + id: string; + household_id: string; + name: string; + icon: string | null; + sort_order: number; + created_at: string; + updated_at: string; +} + +export interface LocationRow { + id: string; + household_id: string; + parent_location_id: string | null; + name: string; + icon: string | null; + sort_order: number; + created_at: string; + updated_at: string; +} + +export interface UnitRow { + id: string; + household_id: string | null; + name: string; + abbreviation: string; + unit_type: UnitType; + allows_decimals: boolean; + created_at: string; +} + +export interface ProductRow { + id: string; + household_id: string; + category_id: string | null; + default_location_id: string | null; + default_unit_id: string | null; + name: string; + brand: string | null; + description: string | null; + barcode: string | null; + image_path: string | null; + package_size: number; + package_unit: string; + minimum_quantity: number; + favorite: boolean; + preferred_store: string | null; + estimated_price_cents: number | null; + price_updated_at: string | null; + archived: boolean; + created_by: string | null; + created_at: string; + updated_at: string; +} + +export interface InventoryBatchRow { + id: string; + household_id: string; + product_id: string; + location_id: string | null; + quantity: number; + expiration_date: string | null; + purchase_date: string | null; + purchase_price_cents: number | null; + store: string | null; + lot_number: string | null; + created_at: string; + updated_at: string; +} + +export interface StockMovementRow { + id: string; + household_id: string; + product_id: string; + batch_id: string | null; + quantity_before: number; + quantity_change: number; + quantity_after: number; + reason: StockMovementReason; + note: string | null; + created_by: string | null; + created_at: string; +} + +export interface ShoppingListRow { + id: string; + household_id: string; + name: string; + status: ShoppingListStatus; + created_by: string | null; + created_at: string; + updated_at: string; +} + +export interface ShoppingListItemRow { + id: string; + shopping_list_id: string; + product_id: string | null; + custom_name: string | null; + quantity: number; + unit_id: string | null; + estimated_unit_price_cents: number | null; + selected_store: string | null; + checked: boolean; + automatically_added: boolean; + notes: string | null; + created_at: string; + updated_at: string; +} + +export interface PurchaseRow { + id: string; + household_id: string; + store: string; + purchase_date: string; + total_price_cents: number; + receipt_image_path: string | null; + notes: string | null; + created_by: string | null; + created_at: string; + updated_at: string; +} + +export interface PurchaseItemRow { + id: string; + purchase_id: string; + product_id: string | null; + quantity: number; + unit_price_cents: number; + total_price_cents: number; + package_size: number | null; + package_unit: string | null; + created_at: string; +} + +export interface PriceObservationRow { + id: string; + household_id: string; + product_id: string | null; + product_name: string; + brand: string | null; + package_size: number | null; + package_unit: string | null; + price_cents: number; + base_price_cents: number | null; + base_price_unit: string | null; + retailer: string; + price_type: PriceType; + postal_code: string | null; + region: string | null; + source_url: string | null; + source_title: string | null; + observed_at: string; + valid_from: string | null; + valid_until: string | null; + confidence: ConfidenceLevel; + exact_match: boolean; + verified: boolean; + source_kind: SourceKind; + created_by: string | null; + created_at: string; +} + +export interface BudgetRow { + id: string; + household_id: string; + name: string; + period: BudgetPeriod; + amount_cents: number; + warning_threshold_percent: number; + start_date: string; + end_date: string | null; + created_at: string; + updated_at: string; +} + +export interface InventorySessionRow { + id: string; + household_id: string; + location_id: string | null; + status: InventorySessionStatus; + started_by: string | null; + started_at: string; + completed_at: string | null; + created_at: string; +} + +export interface InventorySessionItemRow { + id: string; + session_id: string; + product_id: string; + expected_quantity: number; + counted_quantity: number; + difference: number; + confirmed: boolean; + notes: string | null; + created_at: string; + updated_at: string; +} + +export interface DietaryPreferenceRow { + id: string; + household_id: string; + diet_type: DietType; + excluded_ingredients: string[]; + allergens: string[]; + preferred_cuisines: string[]; + maximum_preparation_minutes: number | null; + maximum_meal_budget_cents: number | null; + default_servings: number; + updated_at: string; +} + +export interface RecipeRow { + id: string; + household_id: string; + title: string; + description: string | null; + servings: number; + preparation_minutes: number | null; + difficulty: string | null; + estimated_cost_cents: number | null; + inventory_coverage_percent: number; + ingredients_json: unknown; + steps_json: unknown; + dietary_tags: string[]; + allergen_warnings: string[]; + source_information_json: unknown; + created_by_ai: boolean; + created_by: string | null; + created_at: string; + updated_at: string; +} + +export interface MealPlanRow { + id: string; + household_id: string; + name: string; + start_date: string; + end_date: string; + estimated_total_cost_cents: number | null; + plan_json: unknown; + created_by: string | null; + created_at: string; + updated_at: string; +} + +export interface AiSettingRow { + id: string; + household_id: string; + enabled: boolean; + preferred_retailers: string[]; + allow_google_search: boolean; + daily_request_limit: number; + automatic_price_updates: boolean; + price_update_interval_days: number; + maximum_products_per_price_request: number; + save_chat_history: boolean; + updated_at: string; +} + +export interface AiPendingActionRow { + id: string; + household_id: string; + user_id: string; + action_type: string; + payload_json: unknown; + status: PendingActionStatus; + expires_at: string; + created_at: string; + confirmed_at: string | null; +} diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts new file mode 100644 index 0000000..ba1657b --- /dev/null +++ b/src/types/declarations.d.ts @@ -0,0 +1,2 @@ +declare module 'lucide-react'; +declare module 'recharts'; diff --git a/src/types/domain.ts b/src/types/domain.ts new file mode 100644 index 0000000..b8563c5 --- /dev/null +++ b/src/types/domain.ts @@ -0,0 +1,183 @@ +import type { + HouseholdRole, + AldiRegionType, + UnitType, + StockMovementReason, + ShoppingListStatus, + PriceType, + SourceKind, + ConfidenceLevel, + BudgetPeriod, + InventorySessionStatus, + DietType, + PendingActionStatus, +} from './database'; + +export interface ProductWithStock { + id: string; + householdId: string; + categoryId: string | null; + categoryName?: string | null; + defaultLocationId: string | null; + defaultLocationName?: string | null; + defaultUnitId: string | null; + defaultUnitName?: string | null; + name: string; + brand: string | null; + description: string | null; + barcode: string | null; + imagePath: string | null; + packageSize: number; + packageUnit: string; + minimumQuantity: number; + favorite: boolean; + preferredStore: string | null; + estimatedPriceCents: number | null; + priceUpdatedAt: string | null; + archived: boolean; + totalQuantity: number; + stockValueCents: number; + isLowStock: boolean; + isExpiringSoon: boolean; + earliestExpirationDate: string | null; + batches?: InventoryBatchDomain[]; +} + +export interface InventoryBatchDomain { + id: string; + householdId: string; + productId: string; + locationId: string | null; + locationName?: string | null; + quantity: number; + expirationDate: string | null; + purchaseDate: string | null; + purchasePriceCents: number | null; + store: string | null; + lotNumber: string | null; + createdAt: string; + updatedAt: string; +} + +export interface StockMovementDomain { + id: string; + householdId: string; + productId: string; + productName?: string; + batchId: string | null; + quantityBefore: number; + quantityChange: number; + quantityAfter: number; + reason: StockMovementReason; + note: string | null; + createdBy: string | null; + createdByName?: string | null; + createdAt: string; +} + +export interface ShoppingListItemDomain { + id: string; + shoppingListId: string; + productId: string | null; + productName?: string | null; + customName: string | null; + displayName: string; + quantity: number; + unitId: string | null; + unitName?: string | null; + estimatedUnitPriceCents: number | null; + totalEstimatedPriceCents: number; + selectedStore: string | null; + checked: boolean; + automaticallyAdded: boolean; + notes: string | null; + createdAt: string; +} + +export interface PurchaseDomain { + id: string; + householdId: string; + store: string; + purchaseDate: string; + totalPriceCents: number; + receiptImagePath: string | null; + notes: string | null; + createdBy: string | null; + items?: PurchaseItemDomain[]; + createdAt: string; +} + +export interface PurchaseItemDomain { + id: string; + purchaseId: string; + productId: string | null; + productName?: string | null; + quantity: number; + unitPriceCents: number; + totalPriceCents: number; + packageSize: number | null; + packageUnit: string | null; +} + +export interface PriceObservationDomain { + id: string; + householdId: string; + productId: string | null; + productName: string; + brand: string | null; + packageSize: number | null; + packageUnit: string | null; + priceCents: number; + basePriceCents: number | null; + basePriceUnit: string | null; + retailer: string; + priceType: PriceType; + postalCode: string | null; + region: string | null; + sourceUrl: string | null; + sourceTitle: string | null; + observedAt: string; + validFrom: string | null; + validUntil: string | null; + confidence: ConfidenceLevel; + exactMatch: boolean; + verified: boolean; + sourceKind: SourceKind; + createdBy: string | null; +} + +export interface ExpirationWarningStatus { + status: 'EXPIRED' | 'TODAY' | 'IN_3_DAYS' | 'IN_7_DAYS' | 'IN_14_DAYS' | 'OK'; + label: string; + daysRemaining: number | null; +} + +export interface RecipeIngredientDomain { + name: string; + requiredQuantity: number; + unit: string; + availableQuantity: number; + status: 'AVAILABLE' | 'PARTIAL' | 'MISSING' | 'OPTIONAL'; + estimatedPriceCents?: number; + productId?: string; +} + +export interface RecipeDomain { + id: string; + title: string; + description: string; + servings: number; + preparationMinutes: number; + difficulty: 'EINFACH' | 'MITTEL' | 'SCHWER'; + estimatedAdditionalCostCents: number; + inventoryCoveragePercent: number; + availableIngredients: RecipeIngredientDomain[]; + partialIngredients: RecipeIngredientDomain[]; + missingIngredients: RecipeIngredientDomain[]; + optionalIngredients: RecipeIngredientDomain[]; + steps: string[]; + expiringProductsUsed: string[]; + dietaryTags: string[]; + allergenWarnings: string[]; + storageAdvice?: string; +} diff --git a/supabase/migrations/00001_initial_schema.sql b/supabase/migrations/00001_initial_schema.sql new file mode 100644 index 0000000..6c6aaf4 --- /dev/null +++ b/supabase/migrations/00001_initial_schema.sql @@ -0,0 +1,412 @@ +-- Migration 00001: Initial Schema for KitchenStock + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Enums +CREATE TYPE household_role AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER'); +CREATE TYPE aldi_region_type AS ENUM ('NORD', 'SUED', 'UNSPECIFIED'); +CREATE TYPE unit_type AS ENUM ('COUNT', 'WEIGHT', 'VOLUME', 'OTHER'); +CREATE TYPE stock_movement_reason AS ENUM ('PURCHASE', 'CONSUMED', 'CORRECTION', 'INVENTORY', 'EXPIRED', 'DISCARDED', 'TRANSFER', 'INITIAL'); +CREATE TYPE shopping_list_status AS ENUM ('ACTIVE', 'COMPLETED', 'ARCHIVED'); +CREATE TYPE price_type AS ENUM ('REGULAR', 'OFFER', 'LOYALTY', 'UNKNOWN'); +CREATE TYPE source_kind AS ENUM ('USER_PURCHASE', 'USER_ESTIMATE', 'OFFICIAL_RETAILER', 'OFFICIAL_PROSPECT', 'GEMINI_GROUNDED', 'HISTORICAL_AVERAGE', 'AI_ESTIMATE'); +CREATE TYPE confidence_level AS ENUM ('HIGH', 'MEDIUM', 'LOW'); +CREATE TYPE budget_period AS ENUM ('WEEKLY', 'MONTHLY', 'CUSTOM'); +CREATE TYPE inventory_session_status AS ENUM ('OPEN', 'COMPLETED', 'CANCELLED'); +CREATE TYPE diet_type AS ENUM ('OMNIVORE', 'VEGETARIAN', 'VEGAN', 'PESCETARIAN', 'CUSTOM'); +CREATE TYPE pending_action_status AS ENUM ('PENDING', 'CONFIRMED', 'REJECTED', 'EXPIRED'); + +-- Trigger to update updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 1. Profiles +CREATE TABLE IF NOT EXISTS public.profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + display_name TEXT NOT NULL, + avatar_path TEXT, + locale TEXT NOT NULL DEFAULT 'de-DE', + timezone TEXT NOT NULL DEFAULT 'Europe/Berlin', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 2. Households +CREATE TABLE IF NOT EXISTS public.households ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + currency TEXT NOT NULL DEFAULT 'EUR', + locale TEXT NOT NULL DEFAULT 'de-DE', + timezone TEXT NOT NULL DEFAULT 'Europe/Berlin', + postal_code TEXT, + city TEXT, + aldi_region aldi_region_type NOT NULL DEFAULT 'UNSPECIFIED', + default_servings INT NOT NULL DEFAULT 2 CHECK (default_servings > 0), + owner_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 3. Household Members +CREATE TABLE IF NOT EXISTS public.household_members ( + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + role household_role NOT NULL DEFAULT 'MEMBER', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (household_id, user_id) +); + +-- 4. Categories +CREATE TABLE IF NOT EXISTS public.categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + name TEXT NOT NULL, + icon TEXT, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 5. Locations (supports nesting) +CREATE TABLE IF NOT EXISTS public.locations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + parent_location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL, + name TEXT NOT NULL, + icon TEXT, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 6. Units +CREATE TABLE IF NOT EXISTS public.units ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID REFERENCES public.households(id) ON DELETE CASCADE, -- NULL for system defaults + name TEXT NOT NULL, + abbreviation TEXT NOT NULL, + unit_type unit_type NOT NULL DEFAULT 'COUNT', + allows_decimals BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 7. Products +CREATE TABLE IF NOT EXISTS public.products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + category_id UUID REFERENCES public.categories(id) ON DELETE SET NULL, + default_location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL, + default_unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL, + name TEXT NOT NULL, + brand TEXT, + description TEXT, + barcode TEXT, + image_path TEXT, + package_size NUMERIC(10, 3) NOT NULL DEFAULT 1.0, + package_unit TEXT NOT NULL DEFAULT 'Stück', + minimum_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0 CHECK (minimum_quantity >= 0), + favorite BOOLEAN NOT NULL DEFAULT false, + preferred_store TEXT, + estimated_price_cents INT CHECK (estimated_price_cents >= 0), + price_updated_at TIMESTAMPTZ, + archived BOOLEAN NOT NULL DEFAULT false, + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 8. Inventory Batches +CREATE TABLE IF NOT EXISTS public.inventory_batches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE, + location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL, + quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0 CHECK (quantity >= 0), + expiration_date DATE, + purchase_date DATE, + purchase_price_cents INT CHECK (purchase_price_cents >= 0), + store TEXT, + lot_number TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 9. Stock Movements +CREATE TABLE IF NOT EXISTS public.stock_movements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE, + batch_id UUID REFERENCES public.inventory_batches(id) ON DELETE SET NULL, + quantity_before NUMERIC(10, 3) NOT NULL, + quantity_change NUMERIC(10, 3) NOT NULL, + quantity_after NUMERIC(10, 3) NOT NULL CHECK (quantity_after >= 0), + reason stock_movement_reason NOT NULL, + note TEXT, + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 10. Shopping Lists +CREATE TABLE IF NOT EXISTS public.shopping_lists ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + name TEXT NOT NULL DEFAULT 'Einkaufsliste', + status shopping_list_status NOT NULL DEFAULT 'ACTIVE', + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 11. Shopping List Items +CREATE TABLE IF NOT EXISTS public.shopping_list_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + shopping_list_id UUID NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE, + product_id UUID REFERENCES public.products(id) ON DELETE SET NULL, + custom_name TEXT, + quantity NUMERIC(10, 3) NOT NULL DEFAULT 1.0 CHECK (quantity > 0), + unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL, + estimated_unit_price_cents INT CHECK (estimated_unit_price_cents >= 0), + selected_store TEXT, + checked BOOLEAN NOT NULL DEFAULT false, + automatically_added BOOLEAN NOT NULL DEFAULT false, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 12. Purchases +CREATE TABLE IF NOT EXISTS public.purchases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + store TEXT NOT NULL, + purchase_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), + total_price_cents INT NOT NULL DEFAULT 0 CHECK (total_price_cents >= 0), + receipt_image_path TEXT, + notes TEXT, + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 13. Purchase Items +CREATE TABLE IF NOT EXISTS public.purchase_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + purchase_id UUID NOT NULL REFERENCES public.purchases(id) ON DELETE CASCADE, + product_id UUID REFERENCES public.products(id) ON DELETE SET NULL, + quantity NUMERIC(10, 3) NOT NULL DEFAULT 1.0 CHECK (quantity > 0), + unit_price_cents INT NOT NULL CHECK (unit_price_cents >= 0), + total_price_cents INT NOT NULL CHECK (total_price_cents >= 0), + package_size NUMERIC(10, 3), + package_unit TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 14. Price Observations +CREATE TABLE IF NOT EXISTS public.price_observations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + product_id UUID REFERENCES public.products(id) ON DELETE SET NULL, + product_name TEXT NOT NULL, + brand TEXT, + package_size NUMERIC(10, 3), + package_unit TEXT, + price_cents INT NOT NULL CHECK (price_cents >= 0), + base_price_cents INT CHECK (base_price_cents >= 0), + base_price_unit TEXT, + retailer TEXT NOT NULL, + price_type price_type NOT NULL DEFAULT 'REGULAR', + postal_code TEXT, + region TEXT, + source_url TEXT, + source_title TEXT, + observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + valid_from DATE, + valid_until DATE, + confidence confidence_level NOT NULL DEFAULT 'MEDIUM', + exact_match BOOLEAN NOT NULL DEFAULT true, + verified BOOLEAN NOT NULL DEFAULT false, + source_kind source_kind NOT NULL DEFAULT 'USER_ESTIMATE', + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 15. Budgets +CREATE TABLE IF NOT EXISTS public.budgets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + name TEXT NOT NULL, + period budget_period NOT NULL DEFAULT 'MONTHLY', + amount_cents INT NOT NULL CHECK (amount_cents >= 0), + warning_threshold_percent INT NOT NULL DEFAULT 80 CHECK (warning_threshold_percent BETWEEN 1 AND 100), + start_date DATE NOT NULL, + end_date DATE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 16. Inventory Sessions +CREATE TABLE IF NOT EXISTS public.inventory_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL, + status inventory_session_status NOT NULL DEFAULT 'OPEN', + started_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 17. Inventory Session Items +CREATE TABLE IF NOT EXISTS public.inventory_session_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + session_id UUID NOT NULL REFERENCES public.inventory_sessions(id) ON DELETE CASCADE, + product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE, + expected_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0, + counted_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0, + difference NUMERIC(10, 3) NOT NULL DEFAULT 0.0, + confirmed BOOLEAN NOT NULL DEFAULT false, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 18. Dietary Preferences +CREATE TABLE IF NOT EXISTS public.dietary_preferences ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL UNIQUE REFERENCES public.households(id) ON DELETE CASCADE, + diet_type diet_type NOT NULL DEFAULT 'OMNIVORE', + excluded_ingredients TEXT[] NOT NULL DEFAULT '{}', + allergens TEXT[] NOT NULL DEFAULT '{}', + preferred_cuisines TEXT[] NOT NULL DEFAULT '{}', + maximum_preparation_minutes INT, + maximum_meal_budget_cents INT, + default_servings INT NOT NULL DEFAULT 2, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 19. Recipes +CREATE TABLE IF NOT EXISTS public.recipes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + title TEXT NOT NULL, + description TEXT, + servings INT NOT NULL DEFAULT 2, + preparation_minutes INT, + difficulty TEXT, + estimated_cost_cents INT, + inventory_coverage_percent INT NOT NULL DEFAULT 0, + ingredients_json JSONB NOT NULL DEFAULT '[]'::jsonb, + steps_json JSONB NOT NULL DEFAULT '[]'::jsonb, + dietary_tags TEXT[] NOT NULL DEFAULT '{}', + allergen_warnings TEXT[] NOT NULL DEFAULT '{}', + source_information_json JSONB, + created_by_ai BOOLEAN NOT NULL DEFAULT false, + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 20. Meal Plans +CREATE TABLE IF NOT EXISTS public.meal_plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + name TEXT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + estimated_total_cost_cents INT, + plan_json JSONB NOT NULL DEFAULT '[]'::jsonb, + created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 21. AI Settings +CREATE TABLE IF NOT EXISTS public.ai_settings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL UNIQUE REFERENCES public.households(id) ON DELETE CASCADE, + enabled BOOLEAN NOT NULL DEFAULT true, + preferred_retailers TEXT[] NOT NULL DEFAULT '{"KAUFLAND", "REWE", "ALDI_NORD", "ALDI_SUED", "LIDL"}', + allow_google_search BOOLEAN NOT NULL DEFAULT true, + daily_request_limit INT NOT NULL DEFAULT 30 CHECK (daily_request_limit > 0), + automatic_price_updates BOOLEAN NOT NULL DEFAULT false, + price_update_interval_days INT NOT NULL DEFAULT 14 CHECK (price_update_interval_days > 0), + maximum_products_per_price_request INT NOT NULL DEFAULT 20 CHECK (maximum_products_per_price_request > 0), + save_chat_history BOOLEAN NOT NULL DEFAULT true, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 22. AI Request Logs +CREATE TABLE IF NOT EXISTS public.ai_request_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + user_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL, + feature TEXT NOT NULL, + model TEXT NOT NULL, + grounding_used BOOLEAN NOT NULL DEFAULT false, + input_token_count INT, + output_token_count INT, + latency_ms INT, + successful BOOLEAN NOT NULL DEFAULT true, + error_code TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 23. AI Pending Actions +CREATE TABLE IF NOT EXISTS public.ai_pending_actions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + action_type TEXT NOT NULL, + payload_json JSONB NOT NULL, + status pending_action_status NOT NULL DEFAULT 'PENDING', + expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '24 hours'), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + confirmed_at TIMESTAMPTZ +); + +-- 24. Rate Limit Events +CREATE TABLE IF NOT EXISTS public.rate_limit_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + action TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Triggers for updated_at +CREATE TRIGGER trg_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_households_updated_at BEFORE UPDATE ON public.households FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_categories_updated_at BEFORE UPDATE ON public.categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_locations_updated_at BEFORE UPDATE ON public.locations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON public.products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_inventory_batches_updated_at BEFORE UPDATE ON public.inventory_batches FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_shopping_lists_updated_at BEFORE UPDATE ON public.shopping_lists FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_shopping_list_items_updated_at BEFORE UPDATE ON public.shopping_list_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_purchases_updated_at BEFORE UPDATE ON public.purchases FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_budgets_updated_at BEFORE UPDATE ON public.budgets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_inventory_session_items_updated_at BEFORE UPDATE ON public.inventory_session_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_dietary_preferences_updated_at BEFORE UPDATE ON public.dietary_preferences FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_recipes_updated_at BEFORE UPDATE ON public.recipes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_meal_plans_updated_at BEFORE UPDATE ON public.meal_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER trg_ai_settings_updated_at BEFORE UPDATE ON public.ai_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- Indexes for optimal query performance +CREATE INDEX idx_household_members_user ON public.household_members(user_id); +CREATE INDEX idx_products_household ON public.products(household_id); +CREATE INDEX idx_products_category ON public.products(category_id); +CREATE INDEX idx_products_location ON public.products(default_location_id); +CREATE INDEX idx_products_barcode ON public.products(barcode); +CREATE INDEX idx_inventory_batches_product ON public.inventory_batches(product_id); +CREATE INDEX idx_inventory_batches_household ON public.inventory_batches(household_id); +CREATE INDEX idx_inventory_batches_exp ON public.inventory_batches(expiration_date); +CREATE INDEX idx_stock_movements_product ON public.stock_movements(product_id); +CREATE INDEX idx_stock_movements_household ON public.stock_movements(household_id); +CREATE INDEX idx_shopping_list_items_list ON public.shopping_list_items(shopping_list_id); +CREATE INDEX idx_purchases_household_date ON public.purchases(household_id, purchase_date); +CREATE INDEX idx_price_observations_product ON public.price_observations(product_id); +CREATE INDEX idx_price_observations_observed ON public.price_observations(observed_at); +CREATE INDEX idx_rate_limit_events_user_action ON public.rate_limit_events(user_id, action, created_at); diff --git a/supabase/migrations/00002_rls_policies.sql b/supabase/migrations/00002_rls_policies.sql new file mode 100644 index 0000000..cdca868 --- /dev/null +++ b/supabase/migrations/00002_rls_policies.sql @@ -0,0 +1,211 @@ +-- Migration 00002: Row Level Security (RLS) Policies and Helper Functions + +-- 1. Helper Functions +CREATE OR REPLACE FUNCTION public.is_household_member(_household_id UUID) +RETURNS BOOLEAN +LANGUAGE sql +SECURITY DEFINER +SET search_path = public +STABLE +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.household_members + WHERE household_id = _household_id + AND user_id = auth.uid() + ); +$$; + +CREATE OR REPLACE FUNCTION public.has_household_role(_household_id UUID, _allowed_roles public.household_role[]) +RETURNS BOOLEAN +LANGUAGE sql +SECURITY DEFINER +SET search_path = public +STABLE +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.household_members + WHERE household_id = _household_id + AND user_id = auth.uid() + AND role = ANY(_allowed_roles) + ); +$$; + +-- Enable RLS on all tables +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.households ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.household_members ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.categories ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.locations ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.units ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.products ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.inventory_batches ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.stock_movements ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.shopping_lists ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.shopping_list_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.purchases ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.purchase_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.price_observations ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.budgets ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.inventory_sessions ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.inventory_session_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.dietary_preferences ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.recipes ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.meal_plans ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_settings ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_request_logs ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ai_pending_actions ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.rate_limit_events ENABLE ROW LEVEL SECURITY; + +-- 2. Policies for profiles +CREATE POLICY "Users can view all profiles in their households" + ON public.profiles FOR SELECT + USING ( + id = auth.uid() OR + EXISTS ( + SELECT 1 FROM public.household_members m1 + JOIN public.household_members m2 ON m1.household_id = m2.household_id + WHERE m1.user_id = auth.uid() AND m2.user_id = public.profiles.id + ) + ); + +CREATE POLICY "Users can update their own profile" + ON public.profiles FOR UPDATE + USING (id = auth.uid()) + WITH CHECK (id = auth.uid()); + +CREATE POLICY "Users can insert their own profile" + ON public.profiles FOR INSERT + WITH CHECK (id = auth.uid()); + +-- 3. Policies for households +CREATE POLICY "Members can view their households" + ON public.households FOR SELECT + USING (public.is_household_member(id)); + +CREATE POLICY "Authenticated users can create households" + ON public.households FOR INSERT + WITH CHECK (owner_id = auth.uid()); + +CREATE POLICY "OWNER and ADMIN can update household" + ON public.households FOR UPDATE + USING (public.has_household_role(id, ARRAY['OWNER', 'ADMIN']::public.household_role[])); + +CREATE POLICY "Only OWNER can delete household" + ON public.households FOR DELETE + USING (owner_id = auth.uid()); + +-- 4. Policies for household_members +CREATE POLICY "Members can view household members" + ON public.household_members FOR SELECT + USING (public.is_household_member(household_id)); + +CREATE POLICY "OWNER and ADMIN can add household members" + ON public.household_members FOR INSERT + WITH CHECK (public.has_household_role(household_id, ARRAY['OWNER', 'ADMIN']::public.household_role[])); + +CREATE POLICY "OWNER and ADMIN can update member roles" + ON public.household_members FOR UPDATE + USING (public.has_household_role(household_id, ARRAY['OWNER', 'ADMIN']::public.household_role[])); + +CREATE POLICY "OWNER and ADMIN can remove members" + ON public.household_members FOR DELETE + USING (public.has_household_role(household_id, ARRAY['OWNER', 'ADMIN']::public.household_role[]) OR user_id = auth.uid()); + +-- Helper Macro macro-like pattern for tables scoped by household_id +-- Categories, Locations, Units, Products, Inventory Batches, Stock Movements, Shopping Lists, Purchases, Price Observations, Budgets, Inventory Sessions, Dietary Preferences, Recipes, Meal Plans, AI Settings, AI Pending Actions + +-- Categories +CREATE POLICY "Members can view categories" ON public.categories FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can insert categories" ON public.categories FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "MEMBER, ADMIN, OWNER can update categories" ON public.categories FOR UPDATE USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "ADMIN and OWNER can delete categories" ON public.categories FOR DELETE USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[])); + +-- Locations +CREATE POLICY "Members can view locations" ON public.locations FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can insert locations" ON public.locations FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "MEMBER, ADMIN, OWNER can update locations" ON public.locations FOR UPDATE USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "ADMIN and OWNER can delete locations" ON public.locations FOR DELETE USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[])); + +-- Units +CREATE POLICY "Members can view units" ON public.units FOR SELECT USING (household_id IS NULL OR public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can insert units" ON public.units FOR INSERT WITH CHECK (household_id IS NOT NULL AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "MEMBER, ADMIN, OWNER can update units" ON public.units FOR UPDATE USING (household_id IS NOT NULL AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +-- Products +CREATE POLICY "Members can view products" ON public.products FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can insert products" ON public.products FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "MEMBER, ADMIN, OWNER can update products" ON public.products FOR UPDATE USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); +CREATE POLICY "ADMIN and OWNER can delete products" ON public.products FOR DELETE USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[])); + +-- Inventory Batches +CREATE POLICY "Members can view inventory batches" ON public.inventory_batches FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify inventory batches" ON public.inventory_batches FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +-- Stock Movements +CREATE POLICY "Members can view stock movements" ON public.stock_movements FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can insert stock movements" ON public.stock_movements FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +-- Shopping Lists +CREATE POLICY "Members can view shopping lists" ON public.shopping_lists FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify shopping lists" ON public.shopping_lists FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +-- Shopping List Items +CREATE POLICY "Members can view shopping list items" ON public.shopping_list_items FOR SELECT USING ( + EXISTS (SELECT 1 FROM public.shopping_lists WHERE id = shopping_list_id AND public.is_household_member(household_id)) +); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify shopping list items" ON public.shopping_list_items FOR ALL USING ( + EXISTS (SELECT 1 FROM public.shopping_lists WHERE id = shopping_list_id AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])) +); + +-- Purchases & Purchase Items +CREATE POLICY "Members can view purchases" ON public.purchases FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify purchases" ON public.purchases FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Members can view purchase items" ON public.purchase_items FOR SELECT USING ( + EXISTS (SELECT 1 FROM public.purchases WHERE id = purchase_id AND public.is_household_member(household_id)) +); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify purchase items" ON public.purchase_items FOR ALL USING ( + EXISTS (SELECT 1 FROM public.purchases WHERE id = purchase_id AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])) +); + +-- Price Observations +CREATE POLICY "Members can view price observations" ON public.price_observations FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify price observations" ON public.price_observations FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +-- Budgets +CREATE POLICY "Members can view budgets" ON public.budgets FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "ADMIN and OWNER can modify budgets" ON public.budgets FOR ALL USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[])); + +-- Inventory Sessions & Session Items +CREATE POLICY "Members can view inventory sessions" ON public.inventory_sessions FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify inventory sessions" ON public.inventory_sessions FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Members can view inventory session items" ON public.inventory_session_items FOR SELECT USING ( + EXISTS (SELECT 1 FROM public.inventory_sessions WHERE id = session_id AND public.is_household_member(household_id)) +); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify inventory session items" ON public.inventory_session_items FOR ALL USING ( + EXISTS (SELECT 1 FROM public.inventory_sessions WHERE id = session_id AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])) +); + +-- Dietary Preferences, Recipes, Meal Plans, AI Settings, AI Pending Actions +CREATE POLICY "Members can view dietary preferences" ON public.dietary_preferences FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can update dietary preferences" ON public.dietary_preferences FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Members can view recipes" ON public.recipes FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify recipes" ON public.recipes FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Members can view meal plans" ON public.meal_plans FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify meal plans" ON public.meal_plans FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Members can view AI settings" ON public.ai_settings FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "ADMIN and OWNER can modify AI settings" ON public.ai_settings FOR ALL USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Members can view AI logs" ON public.ai_request_logs FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "System can insert AI logs" ON public.ai_request_logs FOR INSERT WITH CHECK (public.is_household_member(household_id)); + +CREATE POLICY "Members can view pending actions" ON public.ai_pending_actions FOR SELECT USING (public.is_household_member(household_id)); +CREATE POLICY "MEMBER, ADMIN, OWNER can modify pending actions" ON public.ai_pending_actions FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[])); + +CREATE POLICY "Users can manage their rate limit events" ON public.rate_limit_events FOR ALL USING (user_id = auth.uid()); diff --git a/supabase/migrations/00003_stock_rpc.sql b/supabase/migrations/00003_stock_rpc.sql new file mode 100644 index 0000000..003d3e4 --- /dev/null +++ b/supabase/migrations/00003_stock_rpc.sql @@ -0,0 +1,148 @@ +-- Migration 00003: Transactional Stock Adjustment RPC (adjust_product_stock with FEFO) + +CREATE OR REPLACE FUNCTION public.adjust_product_stock( + p_household_id UUID, + p_product_id UUID, + p_quantity_change NUMERIC, + p_reason public.stock_movement_reason, + p_batch_id UUID DEFAULT NULL, + p_note TEXT DEFAULT NULL +) +RETURNS NUMERIC +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_user_id UUID; + v_total_before NUMERIC := 0.0; + v_total_after NUMERIC := 0.0; + v_target_batch RECORD; + v_remaining_to_deduct NUMERIC; + v_deduct_amount NUMERIC; +BEGIN + v_user_id := auth.uid(); + + -- 1. Check permissions + IF NOT public.has_household_role(p_household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]) THEN + RAISE EXCEPTION 'PERMISSION_DENIED: User is not authorized to modify stock in household %', p_household_id; + END IF; + + -- 2. Lock product and calculate current total stock + PERFORM 1 FROM public.products WHERE id = p_product_id AND household_id = p_household_id FOR UPDATE; + + SELECT COALESCE(SUM(quantity), 0.0) INTO v_total_before + FROM public.inventory_batches + WHERE household_id = p_household_id AND product_id = p_product_id; + + -- 3. Check for negative stock + IF (v_total_before + p_quantity_change) < 0 THEN + RAISE EXCEPTION 'INSUFFICIENT_STOCK: Required quantity reduction exceeds current total stock (%)', v_total_before; + END IF; + + -- 4. Apply changes + IF p_batch_id IS NOT NULL THEN + -- Target specific batch + SELECT * INTO v_target_batch + FROM public.inventory_batches + WHERE id = p_batch_id AND product_id = p_product_id AND household_id = p_household_id + FOR UPDATE; + + IF NOT FOUND THEN + RAISE EXCEPTION 'BATCH_NOT_FOUND: Specified batch % does not exist', p_batch_id; + END IF; + + IF (v_target_batch.quantity + p_quantity_change) < 0 THEN + RAISE EXCEPTION 'INSUFFICIENT_BATCH_STOCK: Batch quantity cannot be reduced below 0'; + END IF; + + UPDATE public.inventory_batches + SET quantity = quantity + p_quantity_change, + updated_at = NOW() + WHERE id = p_batch_id; + + ELSE + -- No explicit batch provided + IF p_quantity_change > 0 THEN + -- Increase stock: Add to default batch (batch without expiration_date or newest) or create new batch + SELECT id INTO p_batch_id + FROM public.inventory_batches + WHERE household_id = p_household_id + AND product_id = p_product_id + AND expiration_date IS NULL + ORDER BY created_at DESC + LIMIT 1; + + IF p_batch_id IS NOT NULL THEN + UPDATE public.inventory_batches + SET quantity = quantity + p_quantity_change, + updated_at = NOW() + WHERE id = p_batch_id; + ELSE + INSERT INTO public.inventory_batches (household_id, product_id, quantity) + VALUES (p_household_id, p_product_id, p_quantity_change) + RETURNING id INTO p_batch_id; + END IF; + + ELSIF p_quantity_change < 0 THEN + -- FEFO Deduction (First Expire, First Out) + v_remaining_to_deduct := ABS(p_quantity_change); + + FOR v_target_batch IN + SELECT id, quantity + FROM public.inventory_batches + WHERE household_id = p_household_id + AND product_id = p_product_id + AND quantity > 0 + ORDER BY + expiration_date ASC NULLS LAST, + created_at ASC + FOR UPDATE + LOOP + IF v_remaining_to_deduct <= 0 THEN + EXIT; + END IF; + + v_deduct_amount := LEAST(v_target_batch.quantity, v_remaining_to_deduct); + + UPDATE public.inventory_batches + SET quantity = quantity - v_deduct_amount, + updated_at = NOW() + WHERE id = v_target_batch.id; + + v_remaining_to_deduct := v_remaining_to_deduct - v_deduct_amount; + END LOOP; + END IF; + END IF; + + -- 5. Calculate new total stock + SELECT COALESCE(SUM(quantity), 0.0) INTO v_total_after + FROM public.inventory_batches + WHERE household_id = p_household_id AND product_id = p_product_id; + + -- 6. Insert stock_movements record + INSERT INTO public.stock_movements ( + household_id, + product_id, + batch_id, + quantity_before, + quantity_change, + quantity_after, + reason, + note, + created_by + ) VALUES ( + p_household_id, + p_product_id, + p_batch_id, + v_total_before, + p_quantity_change, + v_total_after, + p_reason, + p_note, + v_user_id + ); + + RETURN v_total_after; +END; +$$; diff --git a/supabase/migrations/00004_rate_limit_rpc.sql b/supabase/migrations/00004_rate_limit_rpc.sql new file mode 100644 index 0000000..3508f5c --- /dev/null +++ b/supabase/migrations/00004_rate_limit_rpc.sql @@ -0,0 +1,48 @@ +-- Migration 00004: Rate Limiting Function + +CREATE OR REPLACE FUNCTION public.check_rate_limit( + p_action TEXT, + p_max_requests INT, + p_window_seconds INT +) +RETURNS BOOLEAN +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_user_id UUID; + v_count INT; + v_window_start TIMESTAMPTZ; +BEGIN + v_user_id := auth.uid(); + IF v_user_id IS NULL THEN + RAISE EXCEPTION 'UNAUTHORIZED: User must be authenticated to check rate limit'; + END IF; + + v_window_start := NOW() - (p_window_seconds || ' seconds')::INTERVAL; + + -- Clean up old events for this user/action + DELETE FROM public.rate_limit_events + WHERE user_id = v_user_id + AND action = p_action + AND created_at < v_window_start; + + -- Count requests in current window + SELECT COUNT(*) INTO v_count + FROM public.rate_limit_events + WHERE user_id = v_user_id + AND action = p_action + AND created_at >= v_window_start; + + IF v_count >= p_max_requests THEN + RETURN FALSE; + END IF; + + -- Record request + INSERT INTO public.rate_limit_events (user_id, action) + VALUES (v_user_id, p_action); + + RETURN TRUE; +END; +$$; diff --git a/supabase/migrations/00005_seed_data.sql b/supabase/migrations/00005_seed_data.sql new file mode 100644 index 0000000..aff3d2e --- /dev/null +++ b/supabase/migrations/00005_seed_data.sql @@ -0,0 +1,133 @@ +-- Migration 00005: Seed Demo Data Function + +CREATE OR REPLACE FUNCTION public.seed_household_demo_data(p_household_id UUID) +RETURNS VOID +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_user_id UUID; + v_cat_gewuerze UUID; + v_cat_kuehl UUID; + v_cat_tiefkuehl UUID; + v_cat_getraenke UUID; + v_cat_konserven UUID; + v_cat_back UUID; + v_cat_grund UUID; + v_cat_obst_gemuese UUID; + v_cat_haushalt UUID; + + v_loc_kuehlschrank UUID; + v_loc_gefrierschrank UUID; + v_loc_gewuerzschrank UUID; + v_loc_vorratsschrank UUID; + v_loc_getraenkelager UUID; + + v_unit_stueck UUID; + v_unit_packung UUID; + v_unit_flasche UUID; + v_unit_dose UUID; + v_unit_g UUID; + v_unit_kg UUID; + v_unit_l UUID; +BEGIN + v_user_id := auth.uid(); + IF NOT public.has_household_role(p_household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]) THEN + RAISE EXCEPTION 'PERMISSION_DENIED: User is not authorized to seed household %', p_household_id; + END IF; + + -- Categories + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Gewürze', 1) RETURNING id INTO v_cat_gewuerze; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Kühlwaren', 2) RETURNING id INTO v_cat_kuehl; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Tiefkühlprodukte', 3) RETURNING id INTO v_cat_tiefkuehl; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Getränke', 4) RETURNING id INTO v_cat_getraenke; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Konserven', 5) RETURNING id INTO v_cat_konserven; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Backzutaten', 6) RETURNING id INTO v_cat_back; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Grundnahrungsmittel', 7) RETURNING id INTO v_cat_grund; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Obst und Gemüse', 8) RETURNING id INTO v_cat_obst_gemuese; + INSERT INTO public.categories (household_id, name, sort_order) VALUES + (p_household_id, 'Haushalt', 9) RETURNING id INTO v_cat_haushalt; + + -- Locations + INSERT INTO public.locations (household_id, name, sort_order) VALUES + (p_household_id, 'Kühlschrank', 1) RETURNING id INTO v_loc_kuehlschrank; + INSERT INTO public.locations (household_id, name, sort_order) VALUES + (p_household_id, 'Gefrierschrank', 2) RETURNING id INTO v_loc_gefrierschrank; + INSERT INTO public.locations (household_id, name, sort_order) VALUES + (p_household_id, 'Gewürzschrank', 3) RETURNING id INTO v_loc_gewuerzschrank; + INSERT INTO public.locations (household_id, name, sort_order) VALUES + (p_household_id, 'Vorratsschrank', 4) RETURNING id INTO v_loc_vorratsschrank; + INSERT INTO public.locations (household_id, name, sort_order) VALUES + (p_household_id, 'Getränkelager', 5) RETURNING id INTO v_loc_getraenkelager; + + -- Units + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Stück', 'Stk.', 'COUNT', false) RETURNING id INTO v_unit_stueck; + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Packung', 'Pk.', 'COUNT', false) RETURNING id INTO v_unit_packung; + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Flasche', 'Fl.', 'COUNT', false) RETURNING id INTO v_unit_flasche; + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Dose', 'Ds.', 'COUNT', false) RETURNING id INTO v_unit_dose; + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Gramm', 'g', 'WEIGHT', true) RETURNING id INTO v_unit_g; + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Kilogramm', 'kg', 'WEIGHT', true) RETURNING id INTO v_unit_kg; + INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES + (p_household_id, 'Liter', 'l', 'VOLUME', true) RETURNING id INTO v_unit_l; + + -- Standard Products with Batches + -- Paprikapulver + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_gewuerze, v_loc_gewuerzschrank, v_unit_dose, 'Paprikapulver', 'Ostmann', 1, 'Dose', 1, 199, true, v_user_id); + + -- Salz + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_gewuerze, v_loc_gewuerzschrank, v_unit_packung, 'Salz', 'Bad Reichenhaller', 500, 'g', 1, 99, false, v_user_id); + + -- Pfeffer + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_gewuerze, v_loc_gewuerzschrank, v_unit_dose, 'Pfeffer schwarz', 'Ostmann', 1, 'Dose', 1, 249, false, v_user_id); + + -- Mehl + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_back, v_loc_vorratsschrank, v_unit_kg, 'Weizenmehl Type 405', 'Diamant', 1000, 'g', 2, 129, false, v_user_id); + + -- Nudeln + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_grund, v_loc_vorratsschrank, v_unit_packung, 'Spaghetti', 'Barilla', 500, 'g', 2, 189, true, v_user_id); + + -- Reis + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_grund, v_loc_vorratsschrank, v_unit_packung, 'Basmati Reis', 'Uncle Ben''s', 1000, 'g', 1, 349, false, v_user_id); + + -- Milch + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_kuehl, v_loc_kuehlschrank, v_unit_l, 'Vollmilch 3.5%', 'Weihenstephan', 1, 'l', 2, 119, true, v_user_id); + + -- Cola Zero + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_getraenke, v_loc_getraenkelager, v_unit_flasche, 'Coca-Cola Zero', 'Coca-Cola', 1.5, 'l', 4, 129, false, v_user_id); + + -- Tomaten + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_obst_gemuese, v_loc_kuehlschrank, v_unit_packung, 'Strauchtomaten', 'Bio', 500, 'g', 1, 229, true, v_user_id); + + -- Kartoffeln + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_obst_gemuese, v_loc_vorratsschrank, v_unit_kg, 'Speisekartoffeln festkochend', 'Regional', 2.5, 'kg', 1, 299, false, v_user_id); + + -- Zwiebeln + INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by) + VALUES (p_household_id, v_cat_obst_gemuese, v_loc_vorratsschrank, v_unit_packung, 'Speisezwiebeln', 'Regional', 1, 'kg', 1, 149, true, v_user_id); +END; +$$; diff --git a/tests/mocks/server-only.ts b/tests/mocks/server-only.ts new file mode 100644 index 0000000..899ed69 --- /dev/null +++ b/tests/mocks/server-only.ts @@ -0,0 +1,2 @@ +// Dummy mock for Next.js 'server-only' package in Vitest environment +export {}; diff --git a/tests/unit/ai.test.ts b/tests/unit/ai.test.ts new file mode 100644 index 0000000..83c53ce --- /dev/null +++ b/tests/unit/ai.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { MockKitchenAiProvider } from '@/lib/ai/client'; + +describe('MockKitchenAiProvider (No Billing Test)', () => { + const provider = new MockKitchenAiProvider(); + + it('suggests recipes with expected structured outputs', async () => { + const recipes = await provider.suggestRecipes({ + householdId: 'test-household', + inventory: [ + { + id: 'prod-1', + householdId: 'test-household', + categoryId: null, + defaultLocationId: null, + defaultUnitId: null, + name: 'Reis', + brand: null, + description: null, + barcode: null, + imagePath: null, + packageSize: 1000, + packageUnit: 'g', + minimumQuantity: 100, + favorite: true, + preferredStore: null, + estimatedPriceCents: 249, + priceUpdatedAt: null, + archived: false, + totalQuantity: 1000, + stockValueCents: 249, + isLowStock: false, + isExpiringSoon: false, + earliestExpirationDate: null, + }, + ], + }); + + expect(recipes).toHaveLength(1); + expect(recipes[0].title).toBeDefined(); + expect(recipes[0].inventoryCoveragePercent).toBeGreaterThan(0); + }); + + it('returns verified price searches via Mock Provider', async () => { + const price = await provider.searchPrices({ + householdId: 'test-household', + query: 'Spaghetti', + retailer: 'REWE', + }); + + expect(price.priceCents).toBe(189); + expect(price.confidence).toBe('HIGH'); + }); +}); diff --git a/tests/unit/currency.test.ts b/tests/unit/currency.test.ts new file mode 100644 index 0000000..414efaf --- /dev/null +++ b/tests/unit/currency.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { formatCurrency, calculateBasePrice, calculateStockValueCents } from '@/lib/formatting/currency'; + +describe('Currency & Stock Value Formatting', () => { + it('formats integer cents into German Euro currency string', () => { + expect(formatCurrency(249)).toContain('2,49'); + expect(formatCurrency(0)).toContain('0,00'); + expect(formatCurrency(1299)).toContain('12,99'); + }); + + it('calculates full package stock value correctly', () => { + // 2 packages @ 1,99 € (199 Cent) = 398 Cent + expect(calculateStockValueCents(2, 1, 199)).toBe(398); + }); + + it('calculates partial package stock value correctly (750g of 1000g @ 1,29 € = 0,97 €)', () => { + // 750g / 1000g * 129 Cent = 96.75 Cent -> rounded to 97 Cent + expect(calculateStockValueCents(750, 1000, 129)).toBe(97); + }); + + it('calculates base price (Grundpreis) per 1kg for grams', () => { + // 500g Barilla Spaghetti @ 1,89 € (189 Cent) -> 3.78 € / kg (378 Cent) + const base = calculateBasePrice(189, 500, 'g'); + expect(base.basePriceCents).toBe(378); + expect(base.basePriceUnit).toBe('1 kg'); + }); +}); diff --git a/tests/unit/date.test.ts b/tests/unit/date.test.ts new file mode 100644 index 0000000..aeed596 --- /dev/null +++ b/tests/unit/date.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { formatDateGerman, getExpirationStatus } from '@/lib/formatting/date'; + +describe('Date & Expiration Status Helpers', () => { + it('formats ISO dates into German TT.MM.JJJJ format', () => { + expect(formatDateGerman('2026-07-26')).toBe('26.07.2026'); + }); + + it('returns OK for null expiration date', () => { + const status = getExpirationStatus(null); + expect(status.status).toBe('OK'); + expect(status.label).toBe('Kein Ablaufdatum'); + }); + + it('correctly flags expired dates', () => { + const status = getExpirationStatus('2020-01-01'); + expect(status.status).toBe('EXPIRED'); + expect(status.label).toContain('Abgelaufen'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..34939b2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4948737 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + 'server-only': path.resolve(__dirname, './tests/mocks/server-only.ts'), + }, + }, +});