From e45d5b73fbb44bafc1ea92960c944458325ff577 Mon Sep 17 00:00:00 2001 From: Jordy Hoogeveen Date: Fri, 4 Sep 2026 09:19:14 +0200 Subject: [PATCH] =?UTF-8?q?Initi=C3=ABle=20commit:=20VibeFinance=203-conta?= =?UTF-8?q?iner=20opzet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basislijn van de bestaande applicatie (Vite/React frontend, Express backend, PostgreSQL) zoals die in de index stond, plus dev-, release- en documentatiescripts. Co-Authored-By: Claude Opus 5 (1M context) --- BEHEER.md | 194 + Changelog.md | 67 + README.md | 191 + backend/Dockerfile | 41 + backend/package.json | 27 + backend/src/config/index.js | 11 + backend/src/constants/initialData.js | 30 + backend/src/db.js | 33 + backend/src/logger.js | 9 + backend/src/middleware/auth.js | 22 + backend/src/middleware/errorHandler.js | 6 + backend/src/routes/auth.js | 135 + backend/src/routes/data.js | 12 + backend/src/routes/health.js | 7 + backend/src/routes/users.js | 42 + backend/src/server.js | 61 + backend/src/stores/dataStore.js | 22 + backend/src/stores/userStore.js | 43 + dev.ps1 | 176 + dev.sh | 39 + docker-compose.dev.yml | 88 + docker-compose.yml | 66 + frontend/Dockerfile | 42 + frontend/index.html | 17 + frontend/nginx-spa.conf | 32 + frontend/package-lock.json | 4934 +++++++++++++++++ frontend/package.json | 24 + frontend/public/favicon.svg | 15 + .../icons/vibefinance-logo-wordmark.svg | 26 + frontend/public/icons/vibefinance-logo.svg | 15 + frontend/src/App.css | 28 + frontend/src/App.jsx | 306 + frontend/src/components/GebruikersBeheer.jsx | 377 ++ frontend/src/components/InstellingenModal.jsx | 112 + frontend/src/components/LoginBackground.jsx | 220 + frontend/src/components/LoginPage.jsx | 378 ++ frontend/src/components/NavBar.jsx | 286 + frontend/src/components/ProfielPopup.jsx | 265 + frontend/src/components/ui/index.jsx | 498 ++ frontend/src/constants/index.js | 76 + frontend/src/context/AppContext.jsx | 226 + frontend/src/hooks/useTheme.js | 23 + frontend/src/main.jsx | 9 + frontend/src/pages/DashboardTab.jsx | 258 + frontend/src/pages/EigenVermogenTab.jsx | 653 +++ frontend/src/pages/SchuldTab.jsx | 452 ++ frontend/src/pages/VoortgangTab.jsx | 438 ++ frontend/src/utils/format.js | 33 + frontend/src/utils/index.js | 2 + frontend/src/utils/storage.js | 75 + frontend/vite.config.js | 41 + package.json | 8 + release.ps1 | 208 + release.sh | 35 + 54 files changed, 11434 insertions(+) create mode 100644 BEHEER.md create mode 100644 Changelog.md create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/package.json create mode 100644 backend/src/config/index.js create mode 100644 backend/src/constants/initialData.js create mode 100644 backend/src/db.js create mode 100644 backend/src/logger.js create mode 100644 backend/src/middleware/auth.js create mode 100644 backend/src/middleware/errorHandler.js create mode 100644 backend/src/routes/auth.js create mode 100644 backend/src/routes/data.js create mode 100644 backend/src/routes/health.js create mode 100644 backend/src/routes/users.js create mode 100644 backend/src/server.js create mode 100644 backend/src/stores/dataStore.js create mode 100644 backend/src/stores/userStore.js create mode 100644 dev.ps1 create mode 100644 dev.sh create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/nginx-spa.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons/vibefinance-logo-wordmark.svg create mode 100644 frontend/public/icons/vibefinance-logo.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/components/GebruikersBeheer.jsx create mode 100644 frontend/src/components/InstellingenModal.jsx create mode 100644 frontend/src/components/LoginBackground.jsx create mode 100644 frontend/src/components/LoginPage.jsx create mode 100644 frontend/src/components/NavBar.jsx create mode 100644 frontend/src/components/ProfielPopup.jsx create mode 100644 frontend/src/components/ui/index.jsx create mode 100644 frontend/src/constants/index.js create mode 100644 frontend/src/context/AppContext.jsx create mode 100644 frontend/src/hooks/useTheme.js create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/DashboardTab.jsx create mode 100644 frontend/src/pages/EigenVermogenTab.jsx create mode 100644 frontend/src/pages/SchuldTab.jsx create mode 100644 frontend/src/pages/VoortgangTab.jsx create mode 100644 frontend/src/utils/format.js create mode 100644 frontend/src/utils/index.js create mode 100644 frontend/src/utils/storage.js create mode 100644 frontend/vite.config.js create mode 100644 package.json create mode 100644 release.ps1 create mode 100644 release.sh diff --git a/BEHEER.md b/BEHEER.md new file mode 100644 index 0000000..164a5fd --- /dev/null +++ b/BEHEER.md @@ -0,0 +1,194 @@ +# VibeFinance — Beheer + +--- + +## Ontwikkeling + +```powershell +.\dev.ps1 # build + start (standaard) +.\dev.ps1 build # alleen bouwen +.\dev.ps1 build -Target backend # alleen backend bouwen +.\dev.ps1 build -Target frontend # alleen frontend bouwen +.\dev.ps1 up -Target backend # alleen backend herbouwen + herstarten +.\dev.ps1 up -Target frontend # alleen frontend herbouwen + herstarten +.\dev.ps1 push # dev-images naar registry pushen +.\dev.ps1 push -Target backend # alleen backend pushen +.\dev.ps1 push -Target frontend # alleen frontend pushen +``` + +> Op macOS/Linux: gebruik `./dev.sh` met dezelfde argumenten. + +> ⚠️ `.\dev.ps1 up` (beide services) en `.\dev.ps1 down` voeren intern `docker compose down -v` uit — dit wist de development-database. Gebruik voor een herstart zonder dataverlies: + +```bash +docker compose -f docker-compose.dev.yml down +docker compose -f docker-compose.dev.yml up -d --build +``` + +--- + +## Release workflow + +```powershell +# 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x" +# 2. +.\release.ps1 # bouw, push naar registry, maak Git-tag aan +.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren +.\release.ps1 -NoBuild # sla docker build over, push bestaande images + +# 3. Versienummer in package.json naar "x.x.(x+1)-dev" voor volgende cyclus +``` + +--- + +## Docker + +```bash +# Update via pre-built images +docker compose pull && docker compose up -d + +# Reset inclusief database ⚠️ wist alle data +docker compose down -v && docker compose up -d +``` + +### PostgreSQL versie-upgrade + +Bij een upgrade van PostgreSQL (bijv. 16 → 18) is de bestaande data niet compatibel. Stappen: + +```bash +# 1. Backup maken +docker exec vibefinance_postgres pg_dump -U vibefinance vibefinance > backup-$(date +%F).sql + +# 2. Oude containers en volumes verwijderen +docker compose down -v + +# 3. Nieuwe versie starten +docker compose up -d + +# 4. Backup terugzetten +docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-YYYY-MM-DD.sql +``` + +--- + +## Architectuur — nginx proxy + +De frontend-container (nginx) serveert de React-app en proxyt `/api/`-verzoeken intern door naar de backend-container op poort 3001. Dit is geconfigureerd in `frontend/nginx-spa.conf`: + +```nginx +location /api/ { + proxy_pass http://backend:3001; +} +``` + +> Ontbreekt dit blok, dan krijgen gebruikers "Kan geen verbinding maken met de server" bij het inloggen — ook al draaien alle containers. + +--- + +## Logs en diagnose + +```bash +# Live logs (dev) +docker compose -f docker-compose.dev.yml logs -f + +# Live logs (productie) +docker compose logs -f + +# Status containers +docker compose ps + +# Database inspecteren (dev) +docker exec -it vibefinance-postgres-dev psql -U vibefinance -d vibefinance + +# Database inspecteren (productie) +docker exec -it vibefinance_postgres psql -U vibefinance -d vibefinance +``` + +--- + +## Data persistentie + +De backend slaat alle data op in **PostgreSQL**. + +| Omgeving | Volume | Mount pad | +|---|---|---| +| Productie | `vibefinance_pgdata` | `/var/lib/postgresql` | +| Development | `postgres_dev_data` | `/var/lib/postgresql` | + +> PostgreSQL 18 gebruikt `PGDATA=/var/lib/postgresql/18/docker`. Het volume is gemount op `/var/lib/postgresql` zodat data bewaard blijft na een container restart. + +**Backup maken:** + +```bash +# Dev +docker exec vibefinance-postgres-dev pg_dump -U vibefinance vibefinance > backup-$(date +%F).sql + +# Productie +docker exec vibefinance_postgres pg_dump -U vibefinance vibefinance > backup-$(date +%F).sql +``` + +**Backup terugzetten:** + +```bash +# Dev +docker exec -i vibefinance-postgres-dev psql -U vibefinance -d vibefinance < backup-2026-04-08.sql + +# Productie +docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-2026-04-08.sql +``` + +--- + +## Gebruikersbeheer + +De eerste geregistreerde gebruiker krijgt automatisch de **Admin**-rol. Verdere accounts kunnen via de app aangemaakt worden (Gebruikersbeheer, Admin vereist) of via de API. + +Rollen: +| Rol | Rechten | +|---|---| +| `Admin` | Alles, inclusief gebruikersbeheer | +| `Bewerker` | Data lezen en bewerken | +| `Viewer` | Alleen lezen | + +--- + +## Productie checklist + +- [ ] `POSTGRES_PASSWORD` instellen op een sterk wachtwoord +- [ ] `JWT_SECRET` aanpassen (genereer met `openssl rand -hex 64`) +- [ ] `CORS_ORIGIN` instellen op je domeinnaam +- [ ] `FRONTEND_PORT` aanpassen indien poort 3300 al in gebruik is +- [ ] TLS afhandelen via een externe reverse proxy (bijv. Nginx Proxy Manager, Caddy, Traefik) + +--- + +## Registry + +``` +vibehoogie.duckdns.org:3000/vibe/vibefinance-backend +vibehoogie.duckdns.org:3000/vibe/vibefinance-frontend + +Tags: + latest productie — meest recente release + x.x.x productie — specifieke versie + latest-dev development — meest recente dev build + x.x.x-dev development — specifieke dev versie +``` + +Inloggen op registry: +```bash +docker login vibehoogie.duckdns.org:3000 +``` + +--- + +## Update-notificaties + +De app vergelijkt de draaiende versie met de laatste release op Gitea. Configuratie staat in `package.json`: + +```json +"repository": "https://vibehoogie.duckdns.org:3000/vibe/VibeFinance", +"updateCheckUrl": "https://vibehoogie.duckdns.org:3000/api/v1/repos/vibe/VibeFinance/releases/latest" +``` + +Vite en de build-scripts lezen deze velden automatisch uit — geen handmatige build-args nodig. diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..d4c9b56 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,67 @@ +# Changelog + +## [0.1.3] - 2026-04-16 + +### Toegevoegd +- Versienummer met logo zichtbaar boven de logout-knop in de sidebar + +### Gewijzigd +- Registry verplaatst van intern IP (`10.0.3.108:3000`) naar publieke DuckDNS-hostnaam (`vibehoogie.duckdns.org`) — toegankelijk via Nginx Proxy Manager +- PostgreSQL productie geüpgraded van versie 16 naar 18 (volume mount aangepast naar `/var/lib/postgresql`) +- Witte rand rondom de app verwijderd (globale CSS reset toegevoegd, `App.css` gekoppeld aan `App.jsx`) + +### Opgelost +- Login gaf "Kan geen verbinding maken met de server": `nginx-spa.conf` miste een `/api/` proxy-blok naar de backend-container — alle API-calls kwamen niet aan + +--- + +## [0.0.4] - 2026-04-08 + +### Toegevoegd +- _Nog in ontwikkeling_ + +--- + +## [0.0.3] - 2026-04-08 + +### Toegevoegd +- Gebruikersbeheer haalt accounts op uit de backend API (was: localStorage) +- Gebruikers bewerken: naam, e-mailadres en wachtwoord aanpassen via ✏️ knop +- Nieuwe gebruiker aanmaken vereist nu ook een wachtwoord +- Eerste geregistreerde gebruiker krijgt automatisch de Admin-rol +- `aangemaakt` kolom toegevoegd aan de `users` tabel (zichtbaar in gebruikersbeheer) +- Custom dropdown component voor rolselectie (past donker thema) +- Voortgang overzicht gebruikt eigen `evVoortgangHistory` — los van geïnvesteerde fiat + +### Opgelost +- Data ging verloren na container restart: PostgreSQL 18 gebruikt een ander `PGDATA` pad (`/var/lib/postgresql/18/docker`). Volume nu gemount op `/var/lib/postgresql` +- Geïnvesteerde fiat invullen vulde automatisch ook het voortgangsoverzicht in + +--- + +## [0.0.2] - 2026-04-01 + +### Toegevoegd +- PostgreSQL als persistente database (vervangt in-memory opslag) +- JWT authenticatie met instelbare vervaltijd +- Backup en restore via ZIP-bestand +- Rate limiting op auth-endpoints +- Helmet security headers +- Docker multi-stage builds (dev / productie) +- Dev script (`dev.ps1` / `dev.sh`) voor bouwen, starten en pushen + +### Gewijzigd +- Gebruikersdata per account opgeslagen (niet meer gedeeld) + +--- + +## [0.0.1] - 2026-03-01 + +### Toegevoegd +- Initiële release van VibeFinance +- Dashboard met overzicht van eigen vermogen, schulden en netto vermogen +- Eigen vermogen beheer met categorieën en historiek +- Schulden beheer met blokken en afbetalingsoverzicht +- Voortgangspagina met grafieken (EV en schuld over tijd) +- Gebruikersbeheer en authenticatie +- Dark mode ondersteuning diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5b198f --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +
+ +# 💜 VibeFinance + +**Persoonlijk vermogensdashboard** + +Eigen vermogen · Schulden · Voortgang · Gebruikersbeheer + +![React](https://img.shields.io/badge/React_18-20232A?style=flat&logo=react&logoColor=61DAFB) +![Node.js](https://img.shields.io/badge/Node.js_22-339933?style=flat&logo=node.js&logoColor=white) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL_18-4169E1?style=flat&logo=postgresql&logoColor=white) +![Docker](https://img.shields.io/badge/Docker_Compose-2496ED?style=flat&logo=docker&logoColor=white) +![Version](https://img.shields.io/badge/versie-0.1.4-8b5cf6?style=flat) +![Built with Claude](https://img.shields.io/badge/Gebouwd_met-Claude.ai-8b5cf6?style=flat&logo=anthropic&logoColor=white) + +
+ +--- + +## 🤖 Gebouwd met Claude.ai + +Deze applicatie is volledig ontworpen en ontwikkeld met behulp van [Claude.ai](https://claude.ai) — de AI-assistent van Anthropic. Van architectuur tot gebruikersinterface: alles is stap voor stap tot stand gekomen in samenwerking met Claude. + +--- + +## ⚡ Quick Start + +> Vereisten: Docker Engine 24+ en Docker Compose v2+ + +**`.env`** — kopieer `.env.example` en vul in: + +```env +POSTGRES_USER=vibefinance +POSTGRES_PASSWORD=kies_een_sterk_wachtwoord +JWT_SECRET=minimaal_64_willekeurige_tekens +FRONTEND_PORT=3300 +``` + +```bash +# Genereer een veilige JWT_SECRET +openssl rand -hex 64 +``` + +```bash +docker compose pull && docker compose up -d +``` + +Navigeer naar `http://:3300` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol. + +### Development (hot reload) + +```powershell +.\dev.ps1 +``` + +Frontend bereikbaar op **http://localhost** (poort 80 → Vite dev server op 5173). + +> **Let op:** `.\dev.ps1 up` en `.\dev.ps1 down` wissen de development-database (volumes). Gebruik voor een gewone herstart `docker compose -f docker-compose.dev.yml down` gevolgd door `docker compose -f docker-compose.dev.yml up -d --build`. + +--- + +## 🗂️ Functionaliteit + +| Scherm | Wat het doet | +|---|---| +| 📊 **Dashboard** | Overzicht van totaal eigen vermogen, schulden en netto vermogen | +| 💰 **Eigen vermogen** | Vermogensposten per categorie beheren + geïnvesteerde fiat historiek | +| 📉 **Schulden** | Schulden bijhouden per blok met afbetalingsoverzicht | +| 📈 **Voortgang** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) | +| 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) | + +--- + +## 🏗️ Architectuur + +``` +vibefinance/ +├── docker-compose.yml # Productie orchestratie (pre-built images) +├── docker-compose.dev.yml # Standalone dev stack (hot reload) +├── .env.example # Kopieer naar .env en vul in +├── package.json # Versienummer — single source of truth +├── dev.ps1 # Dev script (bouwen, starten, pushen) +│ +├── frontend/ # React / Vite SPA +│ ├── Dockerfile # Multi-stage: dev → build → nginx +│ ├── nginx-spa.conf # SPA fallback + /api proxy naar backend +│ ├── vite.config.js +│ └── src/ +│ ├── App.jsx # Root – wiring + idle-logout +│ ├── main.jsx +│ ├── constants/ # Kleuren, tabs, begindata +│ ├── utils/ # Formatters, helpers +│ ├── hooks/ # useTheme +│ ├── context/ # AppContext (global state + API sync) +│ ├── components/ +│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…) +│ │ ├── LoginPage.jsx +│ │ ├── NavBar.jsx +│ │ ├── ProfielPopup.jsx +│ │ └── GebruikersBeheer.jsx +│ └── pages/ +│ ├── DashboardTab.jsx +│ ├── EigenVermogenTab.jsx +│ ├── SchuldTab.jsx +│ └── VoortgangTab.jsx +│ +└── backend/ # Express REST API + ├── Dockerfile # Multi-stage: dev → production (non-root) + └── src/ + ├── server.js # Entry point + ├── db.js # PostgreSQL connectie + schema migraties + ├── config/ # Omgevingsvariabelen + ├── constants/ # Begindata (initialData.js) + ├── middleware/ # auth.js, errorHandler.js + ├── routes/ # auth, data, users, health + └── stores/ # userStore, dataStore +``` + +**Stack:** React 18 · Vite · Node.js 22 · Express 4 · PostgreSQL 18 · JWT · bcrypt · Docker Compose v2 + +--- + +## 🔌 API + +Beveiligde endpoints vereisen `Authorization: Bearer `. Rate limiting: 20 req/15 min op auth-routes, 200 req/15 min globaal. + +| Route | Methoden | Omschrijving | +|---|---|---| +| `/api/auth/register` | POST | Account aanmaken (eerste user = Admin) | +| `/api/auth/login` | POST | Inloggen, JWT token terug | +| `/api/auth/me` | GET, PATCH | Profiel ophalen en bijwerken | +| `/api/auth/me/password` | POST | Wachtwoord wijzigen | +| `/api/data` | GET, PUT, DELETE | Financiële data per gebruiker | +| `/api/users` | GET, POST, PATCH, DELETE | Gebruikersbeheer (Admin only) | +| `/api/health` | GET | Status + uptime | + +--- + +## 🗄️ Database + +| Tabel | Omschrijving | +|---|---| +| `users` | Accounts met bcrypt hash, rol, actief-vlag en aanmaakdatum | +| `user_data` | Financiële data per gebruiker (JSON) | + +| Omgeving | Volume | Mount pad | +|---|---|---| +| Productie | `vibefinance_pgdata` | `/var/lib/postgresql` | +| Development | `postgres_dev_data` | `/var/lib/postgresql` | + +> PostgreSQL 18 gebruikt `PGDATA=/var/lib/postgresql/18/docker` — het volume is daarom gemount op `/var/lib/postgresql` (niet `/data`). + +--- + +## ⚙️ Omgevingsvariabelen + +| Variabele | Standaard | Omschrijving | +|---|---|---| +| `POSTGRES_PASSWORD` | — | **Verplicht** | +| `JWT_SECRET` | — | **Verplicht** — minimaal 64 tekens | +| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam | +| `POSTGRES_DB` | `vibefinance` | Database naam | +| `JWT_EXPIRES` | `7d` | Geldigheidsduur token | +| `CORS_ORIGIN` | — | Externe domeinnaam | +| `FRONTEND_PORT` | `3300` | Externe poort frontend | + +--- + +## 🔐 Beveiliging + +- Wachtwoorden gehasht met bcrypt (cost factor 12) +- JWT authenticatie met instelbare vervaltijd +- Backend uitsluitend bereikbaar via nginx proxy (`/api/` in `nginx-spa.conf`) +- Database bereikbaar alleen binnen het interne Docker netwerk +- Containers draaien als non-root +- Helmet security headers +- Rate limiting op alle endpoints + +--- + +## 🛠️ Beheer + +Zie `BEHEER.md` voor commando's voor lokale ontwikkeling, releases, logs en database-toegang. + +--- + +
+ +Privé gebruik · gebouwd met [Claude.ai](https://claude.ai) + +
diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f2179e6 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,41 @@ +# ╔══════════════════════════════════════════════╗ +# ║ Backend Dockerfile – multi-stage ║ +# ╚══════════════════════════════════════════════╝ + +# ── Stage 1: deps (incl. native build tools) ───── +FROM node:22-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install && npm cache clean --force + +# ── Stage 2: development (hot reload via --watch) ─ +FROM deps AS development +COPY . . +EXPOSE 3001 +CMD ["node", "--watch", "src/server.js"] + +# ── Stage 3: productie-deps (apart gebouwd) ─────── +FROM deps AS prod-deps +RUN npm prune --omit=dev + +# ── Stage 4: production (minimale image) ────────── +FROM node:22-alpine AS production +LABEL maintainer="VibeFinance" + +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +WORKDIR /app + +COPY --from=prod-deps /app/node_modules ./node_modules +COPY package.json ./ +COPY src/ ./src/ + +RUN chown -R appuser:appgroup /app +USER appuser + +EXPOSE 3001 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:3001/api/health || exit 1 + +CMD ["node", "src/server.js"] diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..7a56287 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "vibefinance-backend", + "private": true, + "type": "module", + "scripts": { + "start": "node src/server.js", + "dev": "node --watch src/server.js", + "lint": "eslint src --ext .js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-rate-limit": "^7.4.0", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "pg": "^8.13.3", + "pino": "^9.0.0", + "pino-http": "^10.0.0", + "pino-pretty": "^13.0.0" + }, + "devDependencies": { + "eslint": "^8.57.0" + } +} diff --git a/backend/src/config/index.js b/backend/src/config/index.js new file mode 100644 index 0000000..6b90ae6 --- /dev/null +++ b/backend/src/config/index.js @@ -0,0 +1,11 @@ +import "dotenv/config"; + +export const config = { + NODE_ENV: process.env.NODE_ENV || "development", + PORT: parseInt(process.env.PORT || "3001", 10), + CORS_ORIGIN: process.env.CORS_ORIGIN || "http://localhost", + JWT_SECRET: process.env.JWT_SECRET || "change-me-in-production", + JWT_EXPIRES: process.env.JWT_EXPIRES || "7d", + DATABASE_URL: process.env.DATABASE_URL, + REDIS_URL: process.env.REDIS_URL, +}; diff --git a/backend/src/constants/initialData.js b/backend/src/constants/initialData.js new file mode 100644 index 0000000..8336b14 --- /dev/null +++ b/backend/src/constants/initialData.js @@ -0,0 +1,30 @@ +export const INITIAL_DATA = { + eigenVermogen: { + doel: 500000, + categories: [ + { id: "c1", naam: "Aandelen", color: "#3b82f6", items: [] }, + { id: "c2", naam: "Crypto", color: "#8b5cf6", items: [] }, + { id: "c3", naam: "Commodities", color: "#f59e0b", items: [] }, + { id: "c4", naam: "Vastgoed", color: "#10b981", items: [] }, + { id: "c5", naam: "Crowdfunding", color: "#06b6d4", items: [] }, + { id: "c6", naam: "Te Besteden", color: "#64748b", items: [] }, + ], + }, + schuld: { + initieleSchuld: 303000, + schuldHistory: [], + blokken: [ + { id: "s1", naam: "Hypotheek", color: "#ef4444", items: [ + { id: "s1i1", naam: "Leningdeel 1", waarde: 0 }, + { id: "s1i2", naam: "Leningdeel 2", waarde: 0 }, + { id: "s1i3", naam: "Leningdeel 3", waarde: 0 }, + { id: "s1i4", naam: "Leningdeel 4", waarde: 0 }, + ]}, + { id: "s2", naam: "Belastingdienst", color: "#f97316", items: [ + { id: "s2i1", naam: "Kinderopvangtoeslag", waarde: 0 }, + { id: "s2i2", naam: "Sub-categorie 2", waarde: 0 }, + ]}, + ], + }, + evVoortgang: [], +}; diff --git a/backend/src/db.js b/backend/src/db.js new file mode 100644 index 0000000..0fd6abb --- /dev/null +++ b/backend/src/db.js @@ -0,0 +1,33 @@ +import pg from "pg"; +import { config } from "./config/index.js"; + +const { Pool } = pg; + +export const db = new Pool({ + connectionString: config.DATABASE_URL, +}); + +await db.query(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + naam TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + wachtwoord TEXT NOT NULL, + rol TEXT NOT NULL DEFAULT 'Viewer', + actief BOOLEAN NOT NULL DEFAULT true, + aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS user_data ( + user_id TEXT PRIMARY KEY, + data TEXT NOT NULL + ); +`); + +await db.query(` + ALTER TABLE users ADD COLUMN IF NOT EXISTS aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW(); +`); + +await db.query(` + ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT; +`); diff --git a/backend/src/logger.js b/backend/src/logger.js new file mode 100644 index 0000000..a83eaee --- /dev/null +++ b/backend/src/logger.js @@ -0,0 +1,9 @@ +import pino from "pino"; +import { config } from "./config/index.js"; + +export const logger = pino({ + level: config.NODE_ENV === "production" ? "info" : "debug", + ...(config.NODE_ENV !== "production" && { + transport: { target: "pino-pretty", options: { colorize: true } }, + }), +}); diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..56f1991 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,22 @@ +import jwt from "jsonwebtoken"; +import { config } from "../config/index.js"; + +export function authenticate(req, res, next) { + const header = req.headers.authorization; + if (!header?.startsWith("Bearer ")) { + return res.status(401).json({ error: "Niet ingelogd." }); + } + try { + req.user = jwt.verify(header.slice(7), config.JWT_SECRET); + next(); + } catch { + res.status(401).json({ error: "Sessie verlopen. Log opnieuw in." }); + } +} + +export function requireAdmin(req, res, next) { + if (req.user?.rol !== "Admin") { + return res.status(403).json({ error: "Geen toegang." }); + } + next(); +} diff --git a/backend/src/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js new file mode 100644 index 0000000..3cc1666 --- /dev/null +++ b/backend/src/middleware/errorHandler.js @@ -0,0 +1,6 @@ +export function errorHandler(err, _req, res, _next) { + const status = err.status || err.statusCode || 500; + const message = status < 500 ? err.message : "Er is een interne fout opgetreden."; + if (status >= 500) console.error("[ERROR]", err); + res.status(status).json({ error: message }); +} diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..a5999b2 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,135 @@ +import { Router } from "express"; +import jwt from "jsonwebtoken"; +import bcrypt from "bcryptjs"; +import { config } from "../config/index.js"; +import { authenticate } from "../middleware/auth.js"; +import { userStore } from "../stores/userStore.js"; +import { db } from "../db.js"; + +const router = Router(); + +/** POST /api/auth/register */ +router.post("/register", async (req, res, next) => { + try { + const { naam, email, wachtwoord } = req.body; + if (!naam || !email || !wachtwoord) + return res.status(400).json({ error: "Naam, e-mail en wachtwoord zijn verplicht." }); + if (wachtwoord.length < 8) + return res.status(400).json({ error: "Wachtwoord moet minimaal 8 tekens zijn." }); + + const existing = await userStore.findByEmail(email.toLowerCase().trim()); + if (existing) + return res.status(409).json({ error: "E-mailadres is al in gebruik." }); + + const { rows: countRows } = await db.query("SELECT COUNT(*) FROM users"); + const isFirst = parseInt(countRows[0].count) === 0; + + const hash = await bcrypt.hash(wachtwoord, 12); + const user = await userStore.create({ + naam: naam.trim(), + email: email.toLowerCase().trim(), + wachtwoord: hash, + rol: isFirst ? "Admin" : "Viewer", + actief: true, + }); + + const token = jwt.sign( + { sub: user.id, naam: user.naam, email: user.email, rol: user.rol }, + config.JWT_SECRET, + { expiresIn: config.JWT_EXPIRES } + ); + + res.status(201).json({ token, user: { id: user.id, naam: user.naam, email: user.email, rol: user.rol, avatar: null } }); + } catch (err) { next(err); } +}); + +/** POST /api/auth/login */ +router.post("/login", async (req, res, next) => { + try { + const { email, wachtwoord } = req.body; + if (!email || !wachtwoord) + return res.status(400).json({ error: "E-mail en wachtwoord zijn verplicht." }); + + const user = await userStore.findByEmail(email.toLowerCase().trim()); + if (!user || !await bcrypt.compare(wachtwoord, user.wachtwoord)) + return res.status(401).json({ error: "Ongeldig e-mailadres of wachtwoord." }); + + if (!user.actief) + return res.status(403).json({ error: "Account is inactief." }); + + const token = jwt.sign( + { sub: user.id, naam: user.naam, email: user.email, rol: user.rol }, + config.JWT_SECRET, + { expiresIn: config.JWT_EXPIRES } + ); + + const fullUser = await userStore.findById(user.id); + res.json({ token, user: { id: user.id, naam: user.naam, email: user.email, rol: user.rol, avatar: fullUser?.avatar ?? null } }); + } catch (err) { next(err); } +}); + +/** GET /api/auth/me */ +router.get("/me", authenticate, async (req, res, next) => { + try { + const user = await userStore.findById(req.user.sub); + res.json({ user: { id: user.id, naam: user.naam, email: user.email, rol: user.rol, avatar: user?.avatar ?? null } }); + } catch (err) { next(err); } +}); + +/** PATCH /api/auth/me — update naam / email */ +router.patch("/me", authenticate, async (req, res, next) => { + try { + const { naam, email } = req.body; + if (!naam?.trim() || !email?.trim()) + return res.status(400).json({ error: "Naam en e-mail zijn verplicht." }); + + const conflict = await userStore.findByEmail(email.toLowerCase().trim()); + if (conflict && conflict.id !== req.user.sub) + return res.status(409).json({ error: "E-mailadres is al in gebruik." }); + + const updated = await userStore.update(req.user.sub, { naam: naam.trim(), email: email.toLowerCase().trim() }); + const token = jwt.sign( + { sub: updated.id, naam: updated.naam, email: updated.email, rol: updated.rol }, + config.JWT_SECRET, + { expiresIn: config.JWT_EXPIRES } + ); + res.json({ token, user: { id: updated.id, naam: updated.naam, email: updated.email, rol: updated.rol, avatar: updated.avatar ?? null } }); + } catch (err) { next(err); } +}); + +/** POST /api/auth/me/password — wijzig wachtwoord */ +router.post("/me/password", authenticate, async (req, res, next) => { + try { + const { huidig, nieuw: nieuwWw } = req.body; + if (!huidig || !nieuwWw) + return res.status(400).json({ error: "Huidig en nieuw wachtwoord zijn verplicht." }); + if (nieuwWw.length < 8) + return res.status(400).json({ error: "Nieuw wachtwoord moet minimaal 8 tekens zijn." }); + + const user = await userStore.findById(req.user.sub); + if (!user || !await bcrypt.compare(huidig, user.wachtwoord)) + return res.status(401).json({ error: "Huidig wachtwoord is onjuist." }); + + const hash = await bcrypt.hash(nieuwWw, 12); + await userStore.update(req.user.sub, { wachtwoord: hash }); + res.json({ message: "Wachtwoord gewijzigd." }); + } catch (err) { next(err); } +}); + +/** PATCH /api/auth/me/avatar — sla avatar op als base64 */ +router.patch("/me/avatar", authenticate, async (req, res, next) => { + try { + const { avatar } = req.body; + // avatar mag null zijn (verwijderen) of een base64 data-URL + if (avatar !== null && avatar !== undefined && typeof avatar !== "string") + return res.status(400).json({ error: "Ongeldig avatar formaat." }); + + await userStore.update(req.user.sub, { avatar: avatar ?? null }); + res.json({ avatar: avatar ?? null }); + } catch (err) { next(err); } +}); + +/** POST /api/auth/logout */ +router.post("/logout", authenticate, (_req, res) => res.json({ message: "Uitgelogd." })); + +export default router; diff --git a/backend/src/routes/data.js b/backend/src/routes/data.js new file mode 100644 index 0000000..dbbf2d0 --- /dev/null +++ b/backend/src/routes/data.js @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { authenticate } from "../middleware/auth.js"; +import { dataStore } from "../stores/dataStore.js"; + +const router = Router(); +router.use(authenticate); + +router.get("/", async (req, res, next) => { try { res.json(await dataStore.load(req.user.sub)); } catch (e) { next(e); } }); +router.put("/", async (req, res, next) => { try { res.json(await dataStore.save(req.user.sub, req.body)); } catch (e) { next(e); } }); +router.delete("/", async (req, res, next) => { try { await dataStore.reset(req.user.sub); res.json({ message: "Reset." }); } catch (e) { next(e); } }); + +export default router; diff --git a/backend/src/routes/health.js b/backend/src/routes/health.js new file mode 100644 index 0000000..ab1a5ee --- /dev/null +++ b/backend/src/routes/health.js @@ -0,0 +1,7 @@ +import { Router } from "express"; +const router = Router(); +const START = Date.now(); +router.get("/", (_req, res) => { + res.json({ status: "ok", uptime: Math.floor((Date.now() - START) / 1000) }); +}); +export default router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js new file mode 100644 index 0000000..8c6975b --- /dev/null +++ b/backend/src/routes/users.js @@ -0,0 +1,42 @@ +import { Router } from "express"; +import bcrypt from "bcryptjs"; +import { authenticate, requireAdmin } from "../middleware/auth.js"; +import { userStore } from "../stores/userStore.js"; + +const router = Router(); +router.use(authenticate); + +const safe = ({ wachtwoord, ...u }) => ({ ...u, aangemaakt: u.aangemaakt ?? null }); + +router.get("/", requireAdmin, async (_req, res, next) => { + try { res.json((await userStore.list()).map(safe)); } catch (e) { next(e); } +}); + +router.post("/", requireAdmin, async (req, res, next) => { + try { + const { naam, email, wachtwoord, rol = "Viewer" } = req.body; + if (!naam || !email || !wachtwoord) return res.status(400).json({ error: "Naam, e-mail en wachtwoord zijn verplicht." }); + const hash = await bcrypt.hash(wachtwoord, 12); + const u = await userStore.create({ naam, email: email.toLowerCase(), wachtwoord: hash, rol, actief: true }); + res.status(201).json(safe(u)); + } catch (e) { next(e); } +}); + +router.patch("/:id", requireAdmin, async (req, res, next) => { + try { + const patch = { ...req.body }; + if (patch.wachtwoord) { + patch.wachtwoord = await bcrypt.hash(patch.wachtwoord, 12); + } + const u = await userStore.update(req.params.id, patch); + if (!u) return res.status(404).json({ error: "Niet gevonden." }); + res.json(safe(u)); + } catch (e) { next(e); } +}); + +router.delete("/:id", requireAdmin, async (req, res, next) => { + try { await userStore.remove(req.params.id); res.json({ message: "Verwijderd." }); } + catch (e) { next(e); } +}); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js new file mode 100644 index 0000000..40a21af --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,61 @@ +import express from "express"; +import cors from "cors"; +import helmet from "helmet"; +import rateLimit from "express-rate-limit"; +import cookieParser from "cookie-parser"; +import pino from "pino"; +import pinoHttp from "pino-http"; +import { config } from "./config/index.js"; +import authRouter from "./routes/auth.js"; +import dataRouter from "./routes/data.js"; +import usersRouter from "./routes/users.js"; +import healthRouter from "./routes/health.js"; +import { errorHandler } from "./middleware/errorHandler.js"; + +export const logger = pino({ + level: config.NODE_ENV === "production" ? "info" : "debug", + ...(config.NODE_ENV !== "production" && { + transport: { target: "pino-pretty", options: { colorize: true } }, + }), +}); + +const app = express(); + +// ── Security ────────────────────────────────────────────────────────────────── +app.use(helmet()); +app.use(cors({ origin: config.CORS_ORIGIN, credentials: true })); +app.set("trust proxy", 1); + +// ── Rate limiting ───────────────────────────────────────────────────────────── +app.use("/api/auth", rateLimit({ windowMs: 15 * 60 * 1000, max: 20, standardHeaders: true, legacyHeaders: false })); +app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false })); + +// ── Parsing ─────────────────────────────────────────────────────────────────── +app.use(express.json({ limit: "2mb" })); +app.use(cookieParser()); + +// ── Logging ─────────────────────────────────────────────────────────────────── +if (config.NODE_ENV !== "test") { + app.use(pinoHttp({ logger })); +} + +// ── Routes ──────────────────────────────────────────────────────────────────── +app.use("/api/health", healthRouter); +app.use("/api/auth", authRouter); +app.use("/api/data", dataRouter); +app.use("/api/users", usersRouter); + +// ── 404 voor overige /api/* ─────────────────────────────────────────────────── +app.use("/api/*", (_req, res) => res.status(404).json({ error: "Endpoint niet gevonden." })); + +// ── Error handler ───────────────────────────────────────────────────────────── +app.use(errorHandler); + +// ── Start ───────────────────────────────────────────────────────────────────── +if (config.NODE_ENV !== "test") { + app.listen(config.PORT, () => { + logger.info(`VibeFinance API gestart op http://localhost:${config.PORT} [${config.NODE_ENV}]`); + }); +} + +export default app; diff --git a/backend/src/stores/dataStore.js b/backend/src/stores/dataStore.js new file mode 100644 index 0000000..dc0a2c5 --- /dev/null +++ b/backend/src/stores/dataStore.js @@ -0,0 +1,22 @@ +import { db } from "../db.js"; +import { INITIAL_DATA } from "../constants/initialData.js"; + +export const dataStore = { + async load(userId) { + const { rows } = await db.query("SELECT data FROM user_data WHERE user_id = $1", [userId]); + return rows[0] ? JSON.parse(rows[0].data) : structuredClone(INITIAL_DATA); + }, + async save(userId, payload) { + await db.query( + "INSERT INTO user_data (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2", + [userId, JSON.stringify(payload)] + ); + return payload; + }, + async reset(userId) { + await db.query( + "INSERT INTO user_data (user_id, data) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET data = $2", + [userId, JSON.stringify(structuredClone(INITIAL_DATA))] + ); + }, +}; diff --git a/backend/src/stores/userStore.js b/backend/src/stores/userStore.js new file mode 100644 index 0000000..ee52cae --- /dev/null +++ b/backend/src/stores/userStore.js @@ -0,0 +1,43 @@ +/** + * PostgreSQL-backed user store. + */ +import { db } from "../db.js"; + +const toUser = (row) => row ? { ...row } : null; + +export const userStore = { + async list() { + const { rows } = await db.query("SELECT * FROM users"); + return rows.map(toUser); + }, + async findByEmail(email) { + const { rows } = await db.query("SELECT * FROM users WHERE email = $1", [email]); + return toUser(rows[0]); + }, + async findById(id) { + const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [id]); + return toUser(rows[0]); + }, + async create(data) { + const u = { id: `u${Date.now()}`, ...data }; + await db.query( + "INSERT INTO users (id, naam, email, wachtwoord, rol, actief) VALUES ($1, $2, $3, $4, $5, $6)", + [u.id, u.naam, u.email, u.wachtwoord, u.rol ?? "Viewer", u.actief ?? true] + ); + return u; + }, + async update(id, patch) { + const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [id]); + const user = toUser(rows[0]); + if (!user) return null; + const u = { ...user, ...patch }; + await db.query( + "UPDATE users SET naam=$1, email=$2, wachtwoord=$3, rol=$4, actief=$5, avatar=$6 WHERE id=$7", + [u.naam, u.email, u.wachtwoord, u.rol, u.actief, u.avatar ?? null, id] + ); + return u; + }, + async remove(id) { + await db.query("DELETE FROM users WHERE id = $1", [id]); + }, +}; diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 0000000..a2fc993 --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,176 @@ +# VibeFinance dev script (PowerShell) +# Lokale development cyclus: bouwen, starten, stoppen en optioneel pushen +# naar de dev-registry. Gebruikt altijd de -dev images en tags. +# +# Gebruik: +# .\dev.ps1 # build + herstart containers +# .\dev.ps1 build # alleen lokaal bouwen +# .\dev.ps1 build -Target backend # alleen backend bouwen +# .\dev.ps1 build -Target frontend # alleen frontend bouwen +# .\dev.ps1 up # build + herstart containers +# .\dev.ps1 up -Target backend # alleen backend bouwen + herstart +# .\dev.ps1 up -Target frontend # alleen frontend bouwen + herstart +# .\dev.ps1 down # containers stoppen en volumes verwijderen +# .\dev.ps1 push # multi-platform build + push naar registry +# .\dev.ps1 push -Target backend # alleen backend multi-platform pushen +# .\dev.ps1 push -Target frontend # alleen frontend multi-platform pushen +# +# Vereisten: +# - Docker Desktop actief +# - Voor 'push': ingelogd op registry via: docker login +# - Voor 'push': docker buildx builder actief (docker buildx create --use) +# - root package.json bevat het versienummer (bijv. "1.0.0-dev") + +param( + [ValidateSet("build","up","down","push")] + [string]$Action = "up", + + [ValidateSet("both","frontend","backend")] + [string]$Target = "both" +) + +$ErrorActionPreference = "Stop" + +# ============================================================================= +# PROJECTCONFIGURATIE - pas alleen dit blok aan voor een nieuw project +# ============================================================================= +$appName = "VibeFinance" +$registry = "vibehoogie.duckdns.org/vibe" +$backendImage = "vibefinance-backend" +$frontendImage = "vibefinance-frontend" +$apiUrl = "/api" +# ============================================================================= + +# Versie uit root package.json +$rootPkg = Get-Content (Join-Path $PSScriptRoot "package.json") -Raw | ConvertFrom-Json +$appVersion = $rootPkg.version +$repoUrl = if ($rootPkg.repository) { $rootPkg.repository } else { "" } +$updateCheckUrl = if ($rootPkg.updateCheckUrl) { $rootPkg.updateCheckUrl } else { "" } + +if (-not $appVersion) { + Write-Error "Kon versienummer niet lezen uit package.json" + exit 1 +} + +$env:VERSION = $appVersion +$env:VITE_APP_VERSION = $appVersion +if (-not $env:POSTGRES_PASSWORD) { $env:POSTGRES_PASSWORD = "devpassword" } + +# Versie synchroniseren naar frontend/package.json +$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json" +$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json +$updateUrl = if ($rootPkg.updateCheckUrl) { $rootPkg.updateCheckUrl } else { "" } +if ($frontendPkg.version -ne $appVersion -or $frontendPkg.updateCheckUrl -ne $updateUrl) { + $frontendPkg.version = $appVersion + $frontendPkg.updateCheckUrl = $updateUrl + [System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10)) + Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green +} + +# Registry tags +$backendBase = "$registry/$backendImage" +$frontendBase = "$registry/$frontendImage" + +$backendLatestDev = "$backendBase`:latest-dev" +$frontendLatestDev = "$frontendBase`:latest-dev" +$backendVersionDev = "$backendBase`:$appVersion" +$frontendVersionDev= "$frontendBase`:$appVersion" + +# Pad naar compose file +$devCompose = Join-Path $PSScriptRoot "docker-compose.dev.yml" + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " $appName Dev" -ForegroundColor Cyan +Write-Host " Versie : $appVersion" -ForegroundColor Green +Write-Host " Actie : $Action" -ForegroundColor Green +Write-Host " Target : $Target" -ForegroundColor Green +Write-Host " Registry: $registry" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +# Functies +function Invoke-BackendPush { + Write-Host "Backend bouwen en pushen..." -ForegroundColor White + docker buildx build ` + --platform linux/amd64,linux/arm64 ` + --push ` + -t $backendLatestDev ` + -t $backendVersionDev ` + --build-arg APP_VERSION=$appVersion ` + (Join-Path $PSScriptRoot "backend") + if ($LASTEXITCODE -ne 0) { throw "Backend build mislukt" } + Write-Host "Gepusht: $backendVersionDev" -ForegroundColor Green +} + +function Invoke-FrontendPush { + Write-Host "Frontend bouwen en pushen..." -ForegroundColor White + docker buildx build ` + --platform linux/amd64,linux/arm64 ` + --push ` + -t $frontendLatestDev ` + -t $frontendVersionDev ` + --build-arg APP_VERSION=$appVersion ` + --build-arg VITE_API_URL=$apiUrl ` + --build-arg VITE_REPO_URL=$repoUrl ` + --build-arg VITE_UPDATE_CHECK_URL=$updateCheckUrl ` + (Join-Path $PSScriptRoot "frontend") + if ($LASTEXITCODE -ne 0) { throw "Frontend build mislukt" } + Write-Host "Gepusht: $frontendVersionDev" -ForegroundColor Green +} + +function Invoke-Push { + Write-Host "-- Multi-platform bouwen en pushen -------" -ForegroundColor Cyan + if ($Target -eq "both" -or $Target -eq "backend") { Invoke-BackendPush } + if ($Target -eq "both" -or $Target -eq "frontend") { Invoke-FrontendPush } +} + +# Acties +switch ($Action) { + + "build" { + Write-Host "-- Lokaal bouwen -------------------------" -ForegroundColor Cyan + $services = @(if ($Target -ne "both") { $Target }) + docker compose --progress plain -f $devCompose build @services + if ($LASTEXITCODE -ne 0) { throw "Build mislukt" } + Write-Host "" + Write-Host "Build klaar - $appVersion [$Target]" -ForegroundColor Green + } + + "up" { + if ($Target -eq "both") { + docker compose -f $devCompose down -v + docker compose --progress plain -f $devCompose up --build -d + } else { + docker compose -f $devCompose stop $Target + docker compose --progress plain -f $devCompose up --build -d $Target + } + if ($LASTEXITCODE -ne 0) { throw "Opstarten mislukt" } + Write-Host "" + Write-Host "$appName gestart - $appVersion [$Target]" -ForegroundColor Green + } + + "down" { + docker compose -f $devCompose down -v + Write-Host "" + Write-Host "$appName gestopt" -ForegroundColor Yellow + } + + "push" { + Invoke-Push + Write-Host "" + Write-Host "========================================" -ForegroundColor Cyan + Write-Host " Dev images gepusht: $appVersion [$Target]" -ForegroundColor Green + if ($Target -eq "both" -or $Target -eq "backend") { + Write-Host " $backendVersionDev" -ForegroundColor White + Write-Host " $backendLatestDev" -ForegroundColor White + } + if ($Target -eq "both" -or $Target -eq "frontend") { + Write-Host " $frontendVersionDev" -ForegroundColor White + Write-Host " $frontendLatestDev" -ForegroundColor White + } + Write-Host "========================================" -ForegroundColor Cyan + } +} + +Write-Host "" diff --git a/dev.sh b/dev.sh new file mode 100644 index 0000000..cbd780d --- /dev/null +++ b/dev.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# ── VibeFinance dev wrapper (macOS/Linux) ──────────────────────────────── +# Roept dev.ps1 aan via PowerShell Core (pwsh). +# +# Gebruik: +# ./dev.sh # build + herstart containers +# ./dev.sh build # alleen lokaal bouwen +# ./dev.sh build -Target backend # alleen backend bouwen +# ./dev.sh build -Target frontend # alleen frontend bouwen +# ./dev.sh up # build lokaal + herstart containers +# ./dev.sh up -Target backend # alleen backend bouwen + herstart +# ./dev.sh up -Target frontend # alleen frontend bouwen + herstart +# ./dev.sh down # containers stoppen +# ./dev.sh push # multi-platform build + push +# ./dev.sh push -Target backend # alleen backend pushen +# ./dev.sh push -Target frontend # alleen frontend pushen +# +# Vereisten: +# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell +# - Docker Desktop actief +# - Voor 'push': docker buildx builder actief: docker buildx create --use +# - Voor 'push': ingelogd op registry via: docker login 10.0.3.108:3000 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if ! command -v pwsh &>/dev/null; then + echo "Fout: pwsh niet gevonden. Installeer via: brew install powershell" >&2 + exit 1 +fi + +ACTION="${1:-up}" +shift || true + +pwsh -NoProfile -ExecutionPolicy Bypass \ + -File "$SCRIPT_DIR/dev.ps1" \ + -Action "$ACTION" \ + "$@" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..509c82a --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,88 @@ +## +## Standalone dev stack — lokaal bouwen en testen +## +## docker compose -f docker-compose.dev.yml up --build +## + +services: + + postgres: + image: postgres:18-alpine + container_name: vibefinance-postgres-dev + environment: + POSTGRES_DB: ${POSTGRES_DB:-vibefinance} + POSTGRES_USER: ${POSTGRES_USER:-vibefinance} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpassword} + volumes: + - postgres_dev_data:/var/lib/postgresql + networks: + - dev + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vibefinance"] + interval: 10s + timeout: 5s + retries: 5 + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + target: development + args: + VITE_APP_VERSION: ${VITE_APP_VERSION:-dev} + image: vibefinance/frontend:${VERSION:-latest-dev} + container_name: vibefinance-frontend-dev + ports: + - "80:5173" + volumes: + - ./frontend:/app + - /app/node_modules + networks: + - dev + depends_on: + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:5173/"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + + backend: + build: + context: ./backend + dockerfile: Dockerfile + target: development + image: vibefinance/backend:${VERSION:-latest-dev} + container_name: vibefinance-backend-dev + expose: + - "3001" + volumes: + - ./backend:/app + - /app/node_modules + environment: + NODE_ENV: development + PORT: 3001 + JWT_SECRET: dev-secret-change-in-production + JWT_EXPIRES: 7d + CORS_ORIGIN: http://localhost + DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-vibefinance} + networks: + - dev + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 15s + +volumes: + postgres_dev_data: + +networks: + dev: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..20fd3ad --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +# VibeFinance — docker-compose voor deployment via pre-built images +# +# Zie README.md § Quick Start voor volledige instructies. +# +# Stappen: +# 1. cp .env.example .env (vul JWT_SECRET, CORS_ORIGIN en DB_PASSWORD in) +# 2. docker compose pull +# 3. docker compose up -d + +services: + + postgres: + image: postgres:18-alpine + container_name: vibefinance_postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-vibefinance} + POSTGRES_USER: ${POSTGRES_USER:-vibefinance} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - vibefinance_pgdata:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vibefinance"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - vibefinance_net + + backend: + image: vibehoogie.duckdns.org/vibe/vibefinance-backend:latest + container_name: vibefinance_backend + restart: unless-stopped + env_file: .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-vibefinance} + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/health | grep -q status || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + depends_on: + postgres: + condition: service_healthy + networks: + - vibefinance_net + + frontend: + image: vibehoogie.duckdns.org/vibe/vibefinance-frontend:latest + container_name: vibefinance_frontend + restart: unless-stopped + ports: + - "${FRONTEND_PORT:-3400}:80" + depends_on: + backend: + condition: service_healthy + networks: + - vibefinance_net + +volumes: + vibefinance_pgdata: + +networks: + vibefinance_net: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..fcf0cee --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,42 @@ +# ╔══════════════════════════════════════════════╗ +# ║ Frontend Dockerfile – multi-stage ║ +# ╚══════════════════════════════════════════════╝ + +# ── Stage 1: deps ──────────────────────────────── +FROM node:22-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install && npm cache clean --force + +# ── Stage 2: development (hot reload) ──────────── +FROM deps AS development +ARG VITE_APP_VERSION=dev +ENV VITE_APP_VERSION=$VITE_APP_VERSION +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] + +# ── Stage 3: build ─────────────────────────────── +FROM deps AS builder +ARG VITE_API_URL=/api +ARG VITE_APP_VERSION=dev +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_APP_VERSION=$VITE_APP_VERSION +COPY . . +RUN npm run build + +# ── Stage 4: production (nginx static) ─────────── +FROM nginx:1.27-alpine AS production +LABEL maintainer="VibeFinance" + +# Kopieer de Vite build output +COPY --from=builder /app/dist /usr/share/nginx/html + +# Nginx config voor SPA (history-mode fallback) +COPY nginx-spa.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..36c42b8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + + VibeFinance + + + + + +
+ + + diff --git a/frontend/nginx-spa.conf b/frontend/nginx-spa.conf new file mode 100644 index 0000000..3fc4265 --- /dev/null +++ b/frontend/nginx-spa.conf @@ -0,0 +1,32 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + # Gzip voor statische assets + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; + + # Cache-control: assets met hash in naam → lang; index.html → nooit + location ~* \.(js|css|woff2?|ttf|eot|svg|png|jpg|ico|webp)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Proxy API-calls naar de backend + location /api/ { + proxy_pass http://backend:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SPA fallback – stuur alle routes naar index.html + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e1839d1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4934 @@ +{ + "name": "vibefinance-frontend", + "version": "0.0.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vibefinance-frontend", + "version": "0.0.3", + "dependencies": { + "jszip": "^3.10.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.0", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@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.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "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/@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/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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.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/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/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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "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.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "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/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "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-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.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "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", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/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/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "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/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "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/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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/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/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-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.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "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-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-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-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "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/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.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "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-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "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==", + "dev": true, + "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.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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "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/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/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.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/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/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "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/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/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/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/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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-push-apply/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/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/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "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-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "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/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/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "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/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/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/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-builtin-type/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/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.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "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/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0a6e51c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "vibefinance-frontend", + "version": "0.1.5-dev", + "updateCheckUrl": "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint src --ext .js,.jsx" + }, + "dependencies": { + "jszip": "^3.10.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.0", + "vite": "^5.4.0" + } +} \ No newline at end of file diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..d57f17b --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/frontend/public/icons/vibefinance-logo-wordmark.svg b/frontend/public/icons/vibefinance-logo-wordmark.svg new file mode 100644 index 0000000..ea54e02 --- /dev/null +++ b/frontend/public/icons/vibefinance-logo-wordmark.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + VibeFinance + + diff --git a/frontend/public/icons/vibefinance-logo.svg b/frontend/public/icons/vibefinance-logo.svg new file mode 100644 index 0000000..eb60312 --- /dev/null +++ b/frontend/public/icons/vibefinance-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f75a672 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,28 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body, #root { + height: 100%; + width: 100%; +} + +input[type="number"]::-webkit-inner-spin-button { + opacity: 0.4; +} + +.nav-tabs { + display: flex; + gap: 2px; + overflow-x: auto; +} + +.nav-tabs::-webkit-scrollbar { + display: none; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..478170c --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,306 @@ +import "./App.css"; +import { AppProvider, useApp } from "./context/AppContext.jsx"; +import { useEffect, useRef, useState } from "react"; +import LoginPage from "./components/LoginPage.jsx"; +import NavBar from "./components/NavBar.jsx"; +import GebruikersBeheer from "./components/GebruikersBeheer.jsx"; +import DashboardTab from "./pages/DashboardTab.jsx"; +import EigenVermogenTab from "./pages/EigenVermogenTab.jsx"; +import SchuldTab from "./pages/SchuldTab.jsx"; +import VoortgangTab from "./pages/VoortgangTab.jsx"; +import { RED, PURPLE_LIGHT } from "./constants/index.js"; +import { fmt } from "./utils/format.js"; +import { version, updateCheckUrl } from "../package.json"; +import ProfielPopup from "./components/ProfielPopup.jsx"; +import InstellingenModal from "./components/InstellingenModal.jsx"; + +function initials(naam = "") { + const parts = naam.trim().split(/\s+/); + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return naam.slice(0, 2).toUpperCase(); +} + +const PAGES = [DashboardTab, EigenVermogenTab, SchuldTab, VoortgangTab]; + +function AppInner() { + const { loggedIn, login, logout, locked, tab, showUsers, setShowUsers, showInstellingen, setShowInstellingen, loading, T, currentUser, avatar, idleMinutes, totEV } = useApp(); + const [profielOpen, setProfielOpen] = useState(false); + const [searchOpen, setSearchOpen] = useState(false); + const [nieuweVersie, setNieuweVersie] = useState(null); + const [bellOpen, setBellOpen] = useState(false); + const searchRef = useRef(null); + + useEffect(() => { + if (!updateCheckUrl) return; + fetch(updateCheckUrl) + .then(r => r.json()) + .then(data => { + const latest = data?.tag_name?.replace(/^v/, ""); + if (latest && latest !== version) setNieuweVersie(latest); + }) + .catch(() => {}); + }, []); + + const idleTimer = useRef(null); + const IDLE_MS = (idleMinutes ?? 30) * 60 * 1000; + const [sessionVerlopen, setSessionVerlopen] = useState(false); + + useEffect(() => { + if (!loggedIn) return; + const reset = () => { + clearTimeout(idleTimer.current); + idleTimer.current = setTimeout(() => { + logout(); + setSessionVerlopen(true); + }, IDLE_MS); + }; + const events = ["mousemove", "mousedown", "keydown", "touchstart", "scroll"]; + events.forEach((e) => window.addEventListener(e, reset, { passive: true })); + reset(); + return () => { + events.forEach((e) => window.removeEventListener(e, reset)); + clearTimeout(idleTimer.current); + }; + }, [loggedIn]); + + useEffect(() => { + const handler = (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setSearchOpen(true); + setTimeout(() => searchRef.current?.focus(), 50); + } + if (e.key === "Escape") setSearchOpen(false); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, []); + + if (!loggedIn) { + if (sessionVerlopen) { + return ( +
+
+
⏱
+
Sessie verlopen
+
+ Je bent automatisch uitgelogd na 30 minuten inactiviteit. Klik hieronder om opnieuw in te loggen. +
+ +
+
+ ); + } + return ; + } + + if (loading) { + return ( +
+
+ +
Data laden…
+
+ ); + } + + const ActivePage = PAGES[tab] ?? DashboardTab; + + return ( +
+ + + + + {/* Hoofdinhoud */} +
+ {/* Topbalk */} +
+
+ + {/* Notificatiebel */} +
+ + + {/* Badge */} + {nieuweVersie && ( +
1
+ )} + + {/* Popup */} + {bellOpen && nieuweVersie && ( + <> +
setBellOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 190 }} /> +
+
+ Meldingen +
+
+
+ + + + +
+
+
+ Nieuwe versie beschikbaar +
+
+ v{nieuweVersie} is uitgebracht. Je gebruikt momenteel v{version}. +
+
+
+
+ + )} +
+ + + {/* Gebruikersprofiel */} +
+ + {profielOpen && setProfielOpen(false)} />} +
+
+ + {/* Vergrendeld banner */} + {locked && ( +
+ 🔒 + + Vergrendeld — ga naar Gebruikers om te ontgrendelen + +
+ )} + + {/* Modals */} + {showUsers && setShowUsers(false)} />} + {showInstellingen && setShowInstellingen(false)} />} + + {/* Actieve pagina */} +
+ +
+
+
+ ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/frontend/src/components/GebruikersBeheer.jsx b/frontend/src/components/GebruikersBeheer.jsx new file mode 100644 index 0000000..c6ffdb2 --- /dev/null +++ b/frontend/src/components/GebruikersBeheer.jsx @@ -0,0 +1,377 @@ +import { useState, useEffect, useRef } from "react"; +import { RED, GREEN } from "../constants/index.js"; + +const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)"; +const TEAL_COLOR = "#a855f7"; +const TEAL_BG = "rgba(124,58,237,0.15)"; +const TEAL_BORDER = "rgba(124,58,237,0.35)"; +import { useApp } from "../context/AppContext.jsx"; + +const getToken = () => localStorage.getItem("vf_token"); +const authHdrs = () => ({ "Content-Type": "application/json", Authorization: `Bearer ${getToken()}` }); + +async function apiFetch(path, options = {}) { + const res = await fetch(`/api${path}`, { ...options, headers: { ...authHdrs(), ...(options.headers || {}) } }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || `HTTP ${res.status}`); + } + return res.json(); +} + +function fmtDatum(iso) { + if (!iso) return "—"; + const d = new Date(iso); + return `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`; +} + +function RolSelect({ value, onChange, opties, T, darkMode }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + return ( +
+ + {open && ( +
+ {opties.map((o) => ( +
{ onChange(o); setOpen(false); }} style={{ + padding: "9px 12px", fontSize: 13, cursor: "pointer", + color: o === value ? TEAL_COLOR : T.text, + background: o === value ? TEAL_BG : "transparent", + }} + onMouseEnter={(e) => e.currentTarget.style.background = "rgba(14,221,212,0.10)"} + onMouseLeave={(e) => e.currentTarget.style.background = o === value ? TEAL_BG : "transparent"} + > + {o} +
+ ))} +
+ )} +
+ ); +} + +export default function GebruikersBeheer({ onClose }) { + const { currentUser, darkMode, T } = useApp(); + + const [users, setUsers] = useState([]); + const [loadErr, setLoadErr] = useState(""); + const [showAdd, setShowAdd] = useState(false); + const [editUser, setEditUser] = useState(null); // user object dat bewerkt wordt + const [nieuwNaam, setNieuwNaam] = useState(""); + const [nieuwEmail, setNieuwEmail] = useState(""); + const [nieuwWw, setNieuwWw] = useState(""); + const [nieuwRol, setNieuwRol] = useState("Viewer"); + const [editNaam, setEditNaam] = useState(""); + const [editEmail, setEditEmail] = useState(""); + const [editWw, setEditWw] = useState(""); + const [err, setErr] = useState(""); + const [saving, setSaving] = useState(false); + + const rollen = ["Admin", "Bewerker", "Viewer"]; + + useEffect(() => { + apiFetch("/users") + .then(setUsers) + .catch((e) => setLoadErr(e.message)); + }, []); + + const is = { + background: T.inputBg, border: `1px solid ${T.inputBorder}`, + borderRadius: 8, color: T.text, padding: "8px 10px", + fontSize: 13, outline: "none", boxSizing: "border-box", width: "100%", + }; + + const addUser = async () => { + if (!nieuwNaam.trim() || !nieuwEmail.trim() || !nieuwWw.trim()) { + setErr("Naam, e-mail en wachtwoord zijn verplicht."); + return; + } + setSaving(true); + try { + const u = await apiFetch("/users", { + method: "POST", + body: JSON.stringify({ naam: nieuwNaam, email: nieuwEmail, wachtwoord: nieuwWw, rol: nieuwRol }), + }); + setUsers((prev) => [...prev, u]); + setNieuwNaam(""); setNieuwEmail(""); setNieuwWw(""); setErr(""); + setShowAdd(false); + } catch (e) { + setErr(e.message); + } finally { + setSaving(false); + } + }; + + const openEdit = (u) => { + setEditUser(u); + setEditNaam(u.naam); + setEditEmail(u.email); + setEditWw(""); + setErr(""); + }; + + const saveEdit = async () => { + if (!editNaam.trim() || !editEmail.trim()) { setErr("Naam en e-mail zijn verplicht."); return; } + setSaving(true); + try { + const patch = { naam: editNaam.trim(), email: editEmail.trim() }; + if (editWw) patch.wachtwoord = editWw; + const updated = await apiFetch(`/users/${editUser.id}`, { + method: "PATCH", + body: JSON.stringify(patch), + }); + setUsers((prev) => prev.map((x) => x.id === editUser.id ? { ...x, ...updated } : x)); + setEditUser(null); setErr(""); + } catch (e) { setErr(e.message); } + finally { setSaving(false); } + }; + + const toggleActief = async (u) => { + try { + const updated = await apiFetch(`/users/${u.id}`, { + method: "PATCH", + body: JSON.stringify({ actief: !u.actief }), + }); + setUsers((prev) => prev.map((x) => x.id === u.id ? { ...x, ...updated } : x)); + } catch (e) { setErr(e.message); } + }; + + const removeUser = async (id) => { + try { + await apiFetch(`/users/${id}`, { method: "DELETE" }); + setUsers((prev) => prev.filter((u) => u.id !== id)); + } catch (e) { setErr(e.message); } + }; + + const th = { + padding: "8px 12px", fontSize: 11, color: T.muted, fontWeight: 600, + textAlign: "left", borderBottom: `1px solid ${T.border}`, + background: T.card, textTransform: "uppercase", letterSpacing: "0.05em", + }; + const td = { padding: "10px 12px", fontSize: 13, color: T.text, borderBottom: `1px solid ${T.border}` }; + + return ( +
+
e.stopPropagation()} + style={{ + background: T.card, border: `1px solid ${T.border}`, + borderRadius: 16, width: "100%", maxWidth: 700, + maxHeight: "90vh", display: "flex", flexDirection: "column", + boxShadow: "0 16px 48px rgba(0,0,0,0.4)", + overflow: "hidden", + }} + > + {/* Header */} +
+
+ Gebruikersbeheer + + {users.length} gebruiker{users.length !== 1 ? "s" : ""} + +
+ +
+ +
+ {loadErr && ( +
{loadErr}
+ )} + + {/* Tabel */} +
+ + + + + + + + + + + + {users.map((u) => { + const initials = u.naam.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2); + const isSelf = u.id === currentUser?.id || u.email === currentUser?.email; + return ( + + + + + + + + ); + })} + +
NaamE-mailAangemaaktAdmin
+
+
+ {initials} +
+ {u.naam} + {isSelf && ( + jij + )} +
+
{u.email}{fmtDatum(u.aangemaakt)} + {u.rol === "Admin" + ? ⭐ + : — + } + +
+ + {!isSelf && ( + <> + + + + )} +
+
+
+ + {err && ( +
+ {err} +
+ )} + + {/* Bewerken modal */} + {editUser && ( +
+
+ {editUser.naam} bewerken +
+
+ setEditNaam(e.target.value)} style={is} /> + setEditEmail(e.target.value)} style={is} /> + setEditWw(e.target.value)} style={{ ...is, gridColumn: "1 / -1" }} /> +
+
+ + +
+
+ )} + + {/* Nieuwe gebruiker */} + {showAdd ? ( +
+
+ Nieuwe gebruiker +
+
+ setNieuwNaam(e.target.value)} style={is} /> + setNieuwEmail(e.target.value)} style={is} /> + setNieuwWw(e.target.value)} style={is} /> + +
+
+ + +
+
+ ) : ( + + )} +
+ +
+
+ ); +} diff --git a/frontend/src/components/InstellingenModal.jsx b/frontend/src/components/InstellingenModal.jsx new file mode 100644 index 0000000..004a2f8 --- /dev/null +++ b/frontend/src/components/InstellingenModal.jsx @@ -0,0 +1,112 @@ +import { useApp } from "../context/AppContext.jsx"; + +const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)"; + +const BACKUP_ICON = ( + + + + +); + +const RESTORE_ICON = ( + + + + +); + +function ActionRow({ icon, title, description, onClick }) { + return ( +
+
{icon}
+
+
{title}
+
{description}
+
+
+ ); +} + +export default function InstellingenModal({ onClose }) { + const { T, darkMode, setDarkMode, doBackup, doRestore } = useApp(); + + return ( +
+
e.stopPropagation()} + style={{ + background: T.card, border: `1px solid ${T.border}`, + borderRadius: 16, padding: "22px 22px 8px", + width: 360, boxShadow: "0 16px 48px rgba(0,0,0,0.4)", + color: T.text, + }} + > + {/* Header */} +
+ Instellingen + +
+ + {/* Dark mode rij */} +
+ Dark mode +
setDarkMode((d) => !d)} + style={{ + width: 44, height: 24, borderRadius: 12, cursor: "pointer", + background: darkMode ? TEAL : "#334155", + position: "relative", transition: "background 0.2s", flexShrink: 0, + }} + > +
+
+
+ +
+ + {/* Acties */} +
+ { doBackup(); onClose(); }} + /> +
+ { doRestore(); onClose(); }} + /> +
+
+
+ ); +} diff --git a/frontend/src/components/LoginBackground.jsx b/frontend/src/components/LoginBackground.jsx new file mode 100644 index 0000000..6d89d13 --- /dev/null +++ b/frontend/src/components/LoginBackground.jsx @@ -0,0 +1,220 @@ +/** + * Merkeigen achtergrond voor de loginpagina. + * + * Vertelt in één beeld waar de app over gaat: het eigen vermogen (paars/groen) + * stijgt, de schuld (oranje) daalt en de twee lijnen kruisen elkaar. Daarnaast + * de categorie-donut, maandstaafjes en zwevende munten — allemaal in de + * huisstijl-paars. Volledig SVG, dus scherp op elk formaat en zonder asset. + */ + +// Eigen Vermogen: stijgend. Schuld: dalend. Ze kruisen rond x = 320. +const EV_LINE = "M 30 612 C 110 596, 150 560, 215 528 S 300 470, 340 430 S 430 336, 500 286 S 560 250, 600 232"; +const EV_AREA = `${EV_LINE} L 600 760 L 30 760 Z`; +const DEBT_LINE = "M 30 286 C 110 306, 150 340, 215 372 S 300 430, 340 466 S 430 542, 500 578 S 560 602, 600 616"; +const EV_DOTS = [[215, 528], [340, 430], [500, 286]]; +const BARS = [26, 38, 34, 52, 47, 68, 62, 84, 96]; + +export default function LoginBackground() { + return ( + + ); +} diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx new file mode 100644 index 0000000..b56a5a8 --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,378 @@ +import { useState } from "react"; +import { RED } from "../constants/index.js"; + +const MailIcon = () => ( + + + + +); +const LockIcon = () => ( + + + + +); +const PersonIcon = () => ( + + + + +); + +function EyeBtn({ show, onToggle }) { + return ( + + ); +} + +function FloatingInput({ label, type = "text", value, onChange, onKeyDown, icon, rightElement }) { + const [focused, setFocused] = useState(false); + return ( +
+
+ +
+ {icon && ( + + {icon} + + )} + setFocused(true)} onBlur={() => setFocused(false)} + style={{ + flex: 1, background: "transparent", border: "none", outline: "none", + color: "#f1f5f9", fontSize: 14, + fontFamily: "'Poppins', system-ui, sans-serif", + }} + /> + {rightElement} +
+
+
+ ); +} + +function MiniBarChart() { + const bars = [38, 52, 45, 68, 74, 61, 89]; + const months = ["nov", "dec", "jan", "feb", "mrt", "apr", "mei"]; + return ( +
+ {bars.map((h, i) => ( +
+
+ {months[i]} +
+ ))} +
+ ); +} + +export default function LoginPage({ onLogin }) { + const [mode, setMode] = useState("login"); + const [naam, setNaam] = useState(""); + const [email, setEmail] = useState(""); + const [wachtwoord, setWachtwoord] = useState(""); + const [herhaalWw, setHerhaalWw] = useState(""); + const [showPass, setShowPass] = useState(false); + const [showPass2, setShowPass2] = useState(false); + const [err, setErr] = useState(""); + const [loading, setLoading] = useState(false); + + const switchMode = (m) => { setMode(m); setNaam(""); setEmail(""); setWachtwoord(""); setHerhaalWw(""); setErr(""); }; + + const handleSubmit = async () => { + setErr(""); + if (mode === "register") { + if (!naam.trim()) { setErr("Vul je naam in."); return; } + if (wachtwoord.length < 8) { setErr("Wachtwoord moet minimaal 8 tekens zijn."); return; } + if (!/[A-Z]/.test(wachtwoord)) { setErr("Wachtwoord moet minimaal één hoofdletter bevatten."); return; } + if (!/[0-9]/.test(wachtwoord)) { setErr("Wachtwoord moet minimaal één cijfer bevatten."); return; } + if (wachtwoord !== herhaalWw) { setErr("Wachtwoorden komen niet overeen."); return; } + } + if (!email || !wachtwoord) { setErr("Vul e-mail en wachtwoord in."); return; } + setLoading(true); + try { + const url = mode === "login" ? "/api/auth/login" : "/api/auth/register"; + const body = mode === "login" ? { email, wachtwoord } : { naam, email, wachtwoord }; + const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); + const data = await res.json(); + if (!res.ok) { setErr(data.error || "Er is een fout opgetreden."); return; } + localStorage.setItem("vf_token", data.token); + onLogin(data.user); + } catch { + setErr("Kan geen verbinding maken met de server."); + } finally { + setLoading(false); + } + }; + + const onEnter = (e) => e.key === "Enter" && handleSubmit(); + + return ( +
+ {/* Floating card — larger */} +
+ + {/* LEFT: Form panel */} +
+ {/* Logo */} +
+
+ + + + +
+ VibeFinance +
+ + {/* Heading */} +
+

+ Welkom terug! +

+

+ {mode === "login" + ? "Log in om je budgetoverzicht te bekijken." + : "Maak een account aan om je vermogen bij te houden."} +

+
+ + {/* Tab switcher */} +
+ {[["login", "Inloggen"], ["register", "Registreren"]].map(([m, lbl]) => ( + + ))} +
+ + {/* Form fields */} +
+ {mode === "register" && ( + } + onChange={(e) => { setNaam(e.target.value); setErr(""); }} + onKeyDown={onEnter} + /> + )} + } + onChange={(e) => { setEmail(e.target.value); setErr(""); }} + onKeyDown={onEnter} + /> + } + onChange={(e) => { setWachtwoord(e.target.value); setErr(""); }} + onKeyDown={onEnter} + rightElement={ setShowPass(s => !s)} />} + /> + {mode === "register" && ( + <> + } + onChange={(e) => { setHerhaalWw(e.target.value); setErr(""); }} + onKeyDown={onEnter} + rightElement={ setShowPass2(s => !s)} />} + /> +
+ Min. 8 tekens · één hoofdletter · één cijfer +
+ + )} +
+ + {err && ( +
{err}
+ )} + + {/* Submit */} + +
+ + {/* RIGHT: Purple mockup panel */} +
+ {/* Decorative circles */} +
+
+
+
+ + {/* Top badge */} +
+ ✦ + Jouw vermogen in één oogopslag +
+ + {/* Main glassmorphism card */} +
+ {/* Card header */} +
+
+
Vermogensoverzicht
+
The Road naar € 500.000
+
+
Mei 2026
+
+ + {/* KPI rows */} + {[ + { label: "Eigen Vermogen", val: "€ 445.500", color: "#10b981", icon: "↑" }, + { label: "Schuld", val: "€ 54.500", color: "#f97316", icon: "↓" }, + { label: "Winst", val: "€ 142.500", color: "#a855f7", icon: "↑" }, + ].map(({ label, val, color, icon }) => ( +
+
+
{icon}
+ {label} +
+ {val} +
+ ))} + + {/* Progress toward goal */} +
+
+ Voortgang naar doel + 89% +
+
+
+
+
+ € 0 + € 500.000 +
+
+ + {/* Mini bar chart */} +
+
Groei per maand
+ +
+
+ + {/* Floating badges */} +
+
+
ROI
+
+47,03%
+
+
+
Doel
+
€ 500.000
+
+
+ + {/* Bottom tagline */} +
+ Beheer je vermogen, volg je groei en bereik je financiële doelen — allemaal op één plek. +
+
+ +
+
+ ); +} diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx new file mode 100644 index 0000000..f636630 --- /dev/null +++ b/frontend/src/components/NavBar.jsx @@ -0,0 +1,286 @@ +import { useState, useEffect } from "react"; +import { TABS } from "../constants/index.js"; +import { version, updateCheckUrl } from "../../package.json"; +import { useApp } from "../context/AppContext.jsx"; + +const TAB_ICONS = [ + + + + + + , + + + + , + + + + + , + + + + + , +]; + +const GEBRUIKERS_ICON = ( + + + + + + +); + +const INSTELLINGEN_ICON = ( + + + +); + +const LOGOUT_ICON = ( + + + + + +); + +const INFO_ICON = ( + + + + + +); + +const MOON_ICON = ( + + + +); + +function initials(naam = "") { + const parts = naam.trim().split(/\s+/); + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return naam.slice(0, 2).toUpperCase(); +} + +function SectionLabel({ children }) { + return ( +
+ {children} +
+ ); +} + +const ACTIVE_BG = "#7c3aed"; +const ACTIVE_SHADOW = "0 4px 14px rgba(124,58,237,0.4)"; + +function NavItem({ active, onClick, icon, label, T, danger }) { + const [hovered, setHovered] = useState(false); + const isLit = active || hovered; + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + display: "flex", alignItems: "center", gap: 10, + padding: "11px 12px", borderRadius: 9, cursor: "pointer", marginBottom: 4, + background: active + ? ACTIVE_BG + : hovered ? "rgba(139,92,246,0.08)" : "transparent", + color: active ? "#fff" : hovered ? T.text : (danger ? "#ef4444" : T.muted), + fontWeight: active ? 600 : 400, + fontSize: 15, + boxShadow: active ? ACTIVE_SHADOW : "none", + transition: "all 0.15s", + }} + > + {icon} + {label} +
+ ); +} + +function OverPopup({ onClose, nieuweVersie, T }) { + return ( + <> + {/* Overlay */} +
+ {/* Modal */} +
e.stopPropagation()} style={{ + background: T.card, + border: `1px solid ${T.border}`, + borderRadius: 16, padding: "28px 28px 24px", width: 300, + boxShadow: "0 24px 60px rgba(0,0,0,0.5)", + position: "relative", + }}> + {/* Sluitknop */} + + + {/* Logo + naam */} +
+
+ + + + +
+
+
VibeFinance
+
+
+ +
+ +
+ Versie + + v{version} + +
+ + {nieuweVersie && ( +
+ Nieuw beschikbaar + + v{nieuweVersie} + +
+ )} + +
+
© {new Date().getFullYear()} VibeFinance
+
+
+ + ); +} + +function DarkModeToggle({ darkMode, setDarkMode, T }) { + return ( +
setDarkMode(!darkMode)} + style={{ + display: "flex", alignItems: "center", gap: 10, + padding: "11px 12px", borderRadius: 9, cursor: "pointer", marginBottom: 4, + color: T.muted, fontSize: 13.5, fontWeight: 500, + transition: "background 0.12s", + }} + onMouseEnter={e => e.currentTarget.style.background = "rgba(255,255,255,0.04)"} + onMouseLeave={e => e.currentTarget.style.background = "transparent"} + > + {MOON_ICON} + Dark Mode + {/* Toggle pill */} +
+
+
+
+ ); +} + +export default function NavBar() { + const { tab, setTab, setShowUsers, setShowInstellingen, T, logout, darkMode, setDarkMode, currentUser, avatar } = useApp(); + const [nieuweVersie, setNieuweVersie] = useState(null); + const [overOpen, setOverOpen] = useState(false); + + useEffect(() => { + if (!updateCheckUrl) return; + fetch(updateCheckUrl) + .then(r => r.json()) + .then(data => { + const latest = data?.tag_name?.replace(/^v/, ""); + if (latest && latest !== version) setNieuweVersie(latest); + }) + .catch(() => {}); + }, []); + + return ( +
+ + {/* Logo */} +
+
+ + + + +
+ VibeFinance +
+ + {/* Nav */} + + + {/* Uitloggen */} +
+ +
+
+ ); +} diff --git a/frontend/src/components/ProfielPopup.jsx b/frontend/src/components/ProfielPopup.jsx new file mode 100644 index 0000000..c1d2037 --- /dev/null +++ b/frontend/src/components/ProfielPopup.jsx @@ -0,0 +1,265 @@ +import { useState } from "react"; +import { useApp } from "../context/AppContext.jsx"; +import { RED } from "../constants/index.js"; + +const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)"; +const TEAL_SHADOW = "0 4px 14px rgba(124,58,237,0.4)"; + +function initials(naam = "") { + const parts = naam.trim().split(/\s+/); + if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return naam.slice(0, 2).toUpperCase(); +} + +const LOCK_ICON = ( + + + + +); + +const LOGOUT_ICON = ( + + + + + +); + +const CHEVRON = ( + + + +); + +const BACK_ICON = ( + + + +); + +function InputField({ label, type = "text", value, onChange, T }) { + return ( +
+ + +
+ ); +} + +export default function ProfielPopup({ onClose }) { + const { currentUser, updateProfile, changePassword, logout, T, avatar, setAvatar } = useApp(); + + const [view, setView] = useState("main"); + + const [naam, setNaam] = useState(currentUser?.naam || ""); + const [email, setEmail] = useState(currentUser?.email || ""); + const [profielErr, setProfielErr] = useState(""); + const [profielOk, setProfielOk] = useState(false); + const [profielBusy, setProfielBusy] = useState(false); + + const [huidig, setHuidig] = useState(""); + const [nieuw, setNieuw] = useState(""); + const [bevestig, setBevestig] = useState(""); + const [pwErr, setPwErr] = useState(""); + const [pwOk, setPwOk] = useState(false); + const [pwBusy, setPwBusy] = useState(false); + + const saveProfiel = async () => { + setProfielErr(""); setProfielOk(false); + if (!naam.trim() || !email.trim()) { setProfielErr("Naam en e-mail zijn verplicht."); return; } + setProfielBusy(true); + try { await updateProfile(naam.trim(), email.trim()); setProfielOk(true); } + catch (err) { setProfielErr(err.message || "Opslaan mislukt."); } + finally { setProfielBusy(false); } + }; + + const saveWachtwoord = async () => { + setPwErr(""); setPwOk(false); + if (!huidig || !nieuw || !bevestig) { setPwErr("Vul alle velden in."); return; } + if (nieuw !== bevestig) { setPwErr("Nieuwe wachtwoorden komen niet overeen."); return; } + if (nieuw.length < 8) { setPwErr("Minimaal 8 tekens."); return; } + setPwBusy(true); + try { await changePassword(huidig, nieuw); setPwOk(true); setHuidig(""); setNieuw(""); setBevestig(""); } + catch (err) { setPwErr(err.message || "Wijzigen mislukt."); } + finally { setPwBusy(false); } + }; + + const popupStyle = { + position: "fixed", top: 64, right: 16, zIndex: 199, + background: T.card, border: `1px solid ${T.border}`, + borderRadius: 16, width: 340, + boxShadow: "0 16px 48px rgba(0,0,0,0.45)", + overflow: "hidden", + }; + + const headerStyle = { + display: "flex", alignItems: "center", justifyContent: "center", + padding: "16px 16px 14px", position: "relative", + borderBottom: `1px solid ${T.border}`, + }; + + const closeBtn = ( + + ); + + const backBtn = (label) => ( + + ); + + const actionRow = (icon, label, onClick, color) => ( +
+ {icon} + {label} + {!color && {CHEVRON}} +
+ ); + + return ( + <> +
+ +
+ + {/* ── Hoofdscherm ── */} + {view === "main" && ( + <> +
+ Mijn account + {closeBtn} +
+ + {/* Avatar + naam + email */} +
+
+ {avatar + ? avatar + : initials(currentUser?.naam) + } +
+
{currentUser?.naam}
+
{currentUser?.email}
+ +
+ +
+ {actionRow(LOCK_ICON, "Wachtwoord wijzigen", () => setView("wachtwoord"))} + + )} + + {/* ── Profiel bewerken ── */} + {view === "profiel" && ( + <> +
+ {backBtn()} + Profiel bewerken + {closeBtn} +
+
+ {/* Avatar — klikbaar om foto te uploaden */} +
+ +
+ + {avatar && ( + + )} + { setNaam(e.target.value); setProfielOk(false); }} T={T} /> + { setEmail(e.target.value); setProfielOk(false); }} T={T} /> + {profielErr &&
{profielErr}
} + {profielOk &&
Profiel opgeslagen.
} + +
+ + )} + + {/* ── Wachtwoord wijzigen ── */} + {view === "wachtwoord" && ( + <> +
+ {backBtn()} + Wachtwoord wijzigen + {closeBtn} +
+
+ { setHuidig(e.target.value); setPwOk(false); }} T={T} /> + { setNieuw(e.target.value); setPwOk(false); }} T={T} /> + { setBevestig(e.target.value); setPwOk(false); }} T={T} /> + {pwErr &&
{pwErr}
} + {pwOk &&
Wachtwoord gewijzigd.
} + +
+ + )} + +
+ + ); +} diff --git a/frontend/src/components/ui/index.jsx b/frontend/src/components/ui/index.jsx new file mode 100644 index 0000000..6fcf7d3 --- /dev/null +++ b/frontend/src/components/ui/index.jsx @@ -0,0 +1,498 @@ +import { useRef, useState, useCallback, useContext, createContext } from "react"; +import { PURPLE, PURPLE_LIGHT, EMOJI_OPTIONS } from "../../constants/index.js"; + +const DragContext = createContext(null); + +// ── VCard ───────────────────────────────────────────────────────────────────── +export function VCard({ children, style = {} }) { + return ( +
+ {children} +
+ ); +} + +// ── VBadge ──────────────────────────────────────────────────────────────────── +export function VBadge({ label, color = PURPLE }) { + return ( + + {label} + + ); +} + +// ── TabBtn ──────────────────────────────────────────────────────────────────── +export function TabBtn({ active, onClick, children, darkMode = true }) { + return ( + + ); +} + +// ── VProgressBar ────────────────────────────────────────────────────────────── +export function VProgressBar({ value, max, color = PURPLE }) { + const p = Math.min((value / (max || 1)) * 100, 100); + return ( +
+
+
+ ); +} + +// ── DonutChart ──────────────────────────────────────────────────────────────── +export function DonutChart({ segments, total, size = 120, label, centerVal }) { + const strokeW = size * 0.155; + const r = (size - strokeW) / 2 - 1; + const cx = size / 2, cy = size / 2; + const circ = 2 * Math.PI * r; + const active = segments.filter((s) => s.val > 0); + const GAP = active.length > 1 ? circ * 0.022 : 0; + let off = 0; + const slices = active.map((s) => { + const raw = (s.val / total) * circ; + const dash = Math.max(raw - GAP, 2); + const sl = { ...s, dash, offset: off }; + off += raw; + return sl; + }); + const glowId = `dgl${size}`; + return ( + + + + + + + + {/* Dark inner fill */} + + + {/* Track */} + + + {/* Glow pass */} + {slices.map((s, i) => ( + + ))} + + {/* Crisp segments */} + {slices.map((s, i) => ( + + ))} + + {/* Center label */} + {label && ( + + {label} + + )} + {/* Center value */} + {centerVal && ( + + {centerVal} + + )} + + ); +} + +// ── SvgLineChart ────────────────────────────────────────────────────────────── +export function SvgLineChart({ pts, lines, T, H = 180 }) { + if (pts.length < 2) { + return ( +
+ Minimaal 2 datapunten nodig +
+ ); + } + const W = 580, pl = 64, pr = 14, pt2 = 16, pb = 30; + const iW = W - pl - pr, iH = H - pt2 - pb; + const allVals = lines.flatMap((l) => pts.map((p) => p[l.key])); + const mn = Math.min(0, ...allVals); + const mx = Math.max(...allVals) * 1.08 || 1; + const rng = mx - mn || 1; + const xp = (i) => pl + (i / (pts.length - 1)) * iW; + const yp = (v) => pt2 + (1 - (v - mn) / rng) * iH; + const gc = T.bg === "#0f1117" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)"; + + + return ( +
+ + + {lines.map((l) => ( + + + + + ))} + + {[0, 0.25, 0.5, 0.75, 1].map((f) => { + const gy = pt2 + (1 - f) * iH; + const v = mn + f * rng; + return ( + + + + {new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v)} + + + ); + })} + {lines.map((l) => { + const xs = pts.map((_, i) => xp(i)); + const ys = pts.map((p) => yp(p[l.key])); + const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" "); + const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`; + return ( + + + + {xs.map((x, i) => ( + + ))} + + ); + })} + {pts.map((p, i) => { + const [y, m, d] = (p.datum || "").split("-"); + const label = d ? `${d}-${m}-${y}` : p.datum; + return ( + + {label} + + ); + })} + +
+ {lines.map((l) => ( +
+
+
+ {l.label} +
+ ))} +
+
+ ); +} + +// ── EthIcon ─────────────────────────────────────────────────────────────────── +export function EthIcon({ size = 18, color = "#3b82f6" }) { + const w = size * 0.6, h = size; + return ( + + + + + + + + + ); +} + +// ── BtcIcon ─────────────────────────────────────────────────────────────────── +export function BtcIcon({ size = 18, color = "#f59e0b" }) { + return ( + + + ₿ + + ); +} + +// ── GoldIcon ────────────────────────────────────────────────────────────────── +export function GoldIcon({ size = 24 }) { + return ( + + {/* Bovenkant */} + + {/* Voorkant */} + + {/* Rechterkant */} + + {/* Linkeronderkant */} + + {/* Onderkant schaduw */} + + {/* Streep highlight bovenkant */} + + {/* GOLD tekst */} + GOLD + + ); +} + +// ── Modal primitieven ───────────────────────────────────────────────────────── +export function Overlay({ children, onClose, zIndex = 400 }) { + return ( +
{ if (e.target === e.currentTarget) onClose(); }} + style={{ + position: "fixed", inset: 0, + background: "rgba(0,0,0,0.75)", + zIndex, + }} + > + {children} +
+ ); +} + +export function ModalBox({ children, maxWidth = 480 }) { + const ref = useRef(null); + const [pos, setPos] = useState(null); + const dragState = useRef(null); + + const startDrag = useCallback((e) => { + if (e.button !== 0) return; + e.preventDefault(); + const rect = ref.current.getBoundingClientRect(); + dragState.current = { + startX: e.clientX, + startY: e.clientY, + startLeft: rect.left, + startTop: rect.top, + }; + + const onMove = (e) => { + if (!dragState.current) return; + setPos({ + x: dragState.current.startLeft + (e.clientX - dragState.current.startX), + y: dragState.current.startTop + (e.clientY - dragState.current.startY), + }); + }; + + const onUp = () => { + dragState.current = null; + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }, []); + + const posStyle = pos + ? { left: pos.x, top: pos.y, transform: "none" } + : { left: "50%", top: "50%", transform: "translate(-50%, -50%)" }; + + return ( + +
+ {children} +
+
+ ); +} + +export function ModalHeader({ icon, iconBg, title, subtitle, subtitleColor, onClose, headerBg, T }) { + const startDrag = useContext(DragContext); + return ( +
+
+
+ {icon} +
+
+
{title}
+
{subtitle}
+
+
+ +
+ ); +} + +export function ModalFooter({ children, T }) { + return ( +
+ {children} +
+ ); +} + +// ── AppIcon — rendert emoji-string of speciale SVG-iconen ──────────────────── +export function AppIcon({ value, fallback = "🏷", size = 20 }) { + if (value === "__BTC__") return ; + if (value === "__ETH__") return ; + if (value === "__GOLD__") return ; + if (value) return {value}; + return {fallback}; +} + +// ── EmojiPicker ─────────────────────────────────────────────────────────────── +const SPECIAL_ICONS = [ + { key: "__BTC__", node: }, + { key: "__ETH__", node: }, + { key: "__GOLD__", node: }, +]; + +export function EmojiPicker({ selected, T, accentColor, onSelect, onClear }) { + const ac = accentColor || "#f59e0b"; + const btnStyle = (active) => ({ + width: 34, height: 34, borderRadius: 8, + border: `2px solid ${active ? ac : T.border}`, + background: active ? `${ac}22` : "transparent", + cursor: "pointer", fontSize: 18, + display: "flex", alignItems: "center", justifyContent: "center", + }); + return ( +
+
+ {selected && ( + + )} + {SPECIAL_ICONS.map(({ key, node }) => ( + + ))} + {EMOJI_OPTIONS.map((e) => ( + + ))} +
+
+ ); +} + +// ── ConfirmDeleteDialog ─────────────────────────────────────────────────────── +export function ConfirmDeleteDialog({ naam, T, onCancel, onConfirm }) { + const RED = "#ef4444"; + return ( +
+
+ Weet je zeker dat je {naam} wilt verwijderen? +
+
+ + +
+
+ ); +} + +// ── EmptyState ──────────────────────────────────────────────────────────────── +export function EmptyState({ icon, text, hint, onClick, locked, T }) { + return ( +
+
{icon}
+
{text}
+ {!locked && hint && ( +
{hint}
+ )} +
+ ); +} diff --git a/frontend/src/constants/index.js b/frontend/src/constants/index.js new file mode 100644 index 0000000..19ce772 --- /dev/null +++ b/frontend/src/constants/index.js @@ -0,0 +1,76 @@ +// ── Kleuren ────────────────────────────────────────────────────────────────── +export const PURPLE = "#7c3aed"; +export const PURPLE_LIGHT = "#8b5cf6"; +export const GREEN = "#10b981"; +export const RED = "#ef4444"; +export const GOLD = "#f59e0b"; +export const BLUE = "#3b82f6"; + +export const CAT_COLORS = [ + "#3b82f6", "#8b5cf6", "#f59e0b", "#10b981", + "#06b6d4", "#f97316", "#ec4899", "#84cc16", +]; + +// ── Emoji picker opties ─────────────────────────────────────────────────────── +export const EMOJI_OPTIONS = [ + "📈","📉","💰","💵","🏠","🏡","🏦","💎","🪙","🥇","🥈","🥉", + "📊","💹","🌍","🚀","⚡","🔋","🛢","🌾","☕","🍷","🎯","🎲","🎪", + "🏋","🏄","🚗","✈️","🛳","🎸","🎓","💡","🔑","🌱", + "🌊","🔥","❄️","🌈","⭐","🦁","🐉","🦋","🐝","🍀","🌻","💫","✨", + "💸","💝","🌐","🔗","🏅","🪙","💼","📌", +]; + +// ── Navigatie tabs ──────────────────────────────────────────────────────────── +export const TABS = [ + "Dashboard", + "Eigen Vermogen", + "Schuld", + "Voortgang Schuld & EV", +]; + +// ── Categorie-sleutels (EV voortgang tabel) ─────────────────────────────────── +export const CAT_KEYS = [ + { key: "aandelen", label: "Aandelen", color: "#3b82f6" }, + { key: "crypto", label: "Crypto", color: "#8b5cf6" }, + { key: "commodities", label: "Commodities", color: "#f59e0b" }, + { key: "vastgoed", label: "Vastgoed", color: "#10b981" }, + { key: "crowdfunding", label: "Crowdfunding", color: "#06b6d4" }, + { key: "teBesteden", label: "Te Besteden", color: "#64748b" }, +]; + +// ── Demo gebruikers (alleen frontend – backend heeft eigen store) ───────────── +export const DEMO_USERS = [ + { id: "u1", email: "admin@vibefinance.nl", wachtwoord: "Admin123!", naam: "Beheerder", rol: "Admin" }, + { id: "u2", email: "gebruiker@vibefinance.nl", wachtwoord: "Vibe2024!", naam: "Gebruiker", rol: "Viewer" }, +]; + +// ── Initiële app-data (fallback als backend niet bereikbaar is) ─────────────── +export const INITIAL_DATA = { + eigenVermogen: { + doel: 500000, + categories: [ + { id: "c1", naam: "Aandelen", color: "#3b82f6", items: [] }, + { id: "c2", naam: "Crypto", color: "#8b5cf6", items: [] }, + { id: "c3", naam: "Commodities", color: "#f59e0b", items: [] }, + { id: "c4", naam: "Vastgoed", color: "#10b981", items: [] }, + { id: "c5", naam: "Crowdfunding", color: "#06b6d4", items: [] }, + { id: "c6", naam: "Te Besteden", color: "#64748b", items: [] }, + ], + }, + schuld: { + initieleSchuld: 303000, + schuldHistory: [], + blokken: [ + { id: "s1", naam: "Hypotheek", color: "#ef4444", items: [ + { id: "s1i1", naam: "Leningdeel 1", waarde: 0 }, + { id: "s1i2", naam: "Leningdeel 2", waarde: 0 }, + { id: "s1i3", naam: "Leningdeel 3", waarde: 0 }, + { id: "s1i4", naam: "Leningdeel 4", waarde: 0 }, + ]}, + { id: "s2", naam: "Belastingdienst", color: "#f97316", items: [ + { id: "s2i1", naam: "Kinderopvangtoeslag", waarde: 0 }, + { id: "s2i2", naam: "Sub-categorie 2", waarde: 0 }, + ]}, + ], + }, +}; diff --git a/frontend/src/context/AppContext.jsx b/frontend/src/context/AppContext.jsx new file mode 100644 index 0000000..0c49b88 --- /dev/null +++ b/frontend/src/context/AppContext.jsx @@ -0,0 +1,226 @@ +import { createContext, useContext, useState, useMemo, useRef } from "react"; +import JSZip from "jszip"; +import { INITIAL_DATA } from "../constants/index.js"; +import { migrateData } from "../utils/storage.js"; +import { useTheme } from "../hooks/useTheme.js"; + +const AppContext = createContext(null); + +// ── API helpers ─────────────────────────────────────────────────────────────── +const getToken = () => localStorage.getItem("vf_token"); +const authHdrs = () => ({ "Content-Type": "application/json", Authorization: `Bearer ${getToken()}` }); + +async function apiFetch(path, options = {}) { + const res = await fetch(`/api${path}`, { ...options, headers: { ...authHdrs(), ...(options.headers || {}) } }); + if (!res.ok) throw new Error(`API ${path} → ${res.status}`); + return res.json(); +} + +// ── Provider ────────────────────────────────────────────────────────────────── +export function AppProvider({ children }) { + // ── Auth ────────────────────────────────────────────────────── + const [loggedIn, setLoggedIn] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(false); + const [avatar, setAvatarRaw] = useState(null); + + const setAvatar = async (base64) => { + setAvatarRaw(base64); + try { + await apiFetch("/auth/me/avatar", { method: "PATCH", body: JSON.stringify({ avatar: base64 ?? null }) }); + } catch (err) { + console.error("Avatar opslaan mislukt:", err); + } + }; + + // ── UI ──────────────────────────────────────────────────────── + const [darkMode, setDarkMode] = useState(true); + const [locked, setLocked] = useState(false); + const [tab, setTab] = useState(0); + const [menuOpen, setMenuOpen] = useState(false); + const [showUsers, setShowUsers] = useState(false); + const [showInstellingen, setShowInstellingen] = useState(false); + const [idleMinutes, setIdleMinutes] = useState(30); + + // ── Financiële data ─────────────────────────────────────────── + const [data, setDataRaw] = useState(INITIAL_DATA); + const [evRows, setEvRowsRaw] = useState([]); + const [evVoortgangHistory, setEvVoortgangHistoryRaw] = useState([]); + const saveTimer = useRef(null); + + const saveToApi = (newData, newEvRows, newEvVH) => { + if (saveTimer.current) clearTimeout(saveTimer.current); + saveTimer.current = setTimeout(async () => { + try { + await apiFetch("/data", { + method: "PUT", + body: JSON.stringify({ ...newData, evVoortgang: newEvRows, evVoortgangHistory: newEvVH }), + }); + } catch (err) { + console.error("Opslaan mislukt:", err); + } + }, 800); + }; + + const setData = (next) => { + const d = typeof next === "function" ? next(data) : next; + setDataRaw(d); + saveToApi(d, evRows, evVoortgangHistory); + }; + + const setEvRows = (rows) => { + const r = typeof rows === "function" ? rows(evRows) : rows; + setEvRowsRaw(r); + saveToApi(data, r, evVoortgangHistory); + }; + + const setEvVoortgangHistory = (history) => { + const h = typeof history === "function" ? history(evVoortgangHistory) : history; + setEvVoortgangHistoryRaw(h); + saveToApi(data, evRows, h); + }; + + // ── Gebruikers ──────────────────────────────────────────────── + const [users, setUsersRaw] = useState(() => { + try { return JSON.parse(localStorage.getItem("wbjw_users")) || []; } catch { return []; } + }); + const setUsers = (u) => { localStorage.setItem("wbjw_users", JSON.stringify(u)); setUsersRaw(u); }; + + // ── Thema ───────────────────────────────────────────────────── + const T = useTheme(darkMode); + + // ── Afgeleide waarden ───────────────────────────────────────── + const ev = data.eigenVermogen; + const schuld = data.schuld; + const cats = ev?.categories || []; + const schuldBlokken = schuld?.blokken || []; + const schuldHistory = schuld?.schuldHistory || []; + + const totEV = useMemo( + () => cats.reduce((a, c) => a + c.items.reduce((b, i) => b + (i.waarde || 0), 0), 0), + [cats] + ); + const totSchuld = useMemo( + () => schuldBlokken.reduce((a, b) => a + b.items.reduce((c, i) => c + (i.waarde || 0), 0), 0), + [schuldBlokken] + ); + const catTotals = useMemo( + () => cats.map((c) => ({ ...c, tot: c.items.reduce((a, i) => a + (i.waarde || 0), 0) })), + [cats] + ); + + // ── Auth handlers ───────────────────────────────────────────── + const login = async (user) => { + setCurrentUser(user); + setLoggedIn(true); + setAvatarRaw(user.avatar ?? null); + setLoading(true); + try { + const apiData = await apiFetch("/data"); + const migrated = migrateData({ + eigenVermogen: apiData.eigenVermogen, + schuld: apiData.schuld, + }); + setDataRaw(migrated); + setEvRowsRaw(apiData.evVoortgang || []); + setEvVoortgangHistoryRaw(apiData.evVoortgangHistory || []); + } catch (err) { + console.error("Data laden mislukt:", err); + } finally { + setLoading(false); + } + }; + + const updateProfile = async (naam, email) => { + const res = await apiFetch("/auth/me", { method: "PATCH", body: JSON.stringify({ naam, email }) }); + localStorage.setItem("vf_token", res.token); + setCurrentUser(res.user); + }; + + const changePassword = async (huidig, nieuw) => { + await apiFetch("/auth/me/password", { method: "POST", body: JSON.stringify({ huidig, nieuw }) }); + }; + + const logout = () => { + setCurrentUser(null); + setAvatarRaw(null); + setLoggedIn(false); + setDataRaw(INITIAL_DATA); + setEvRowsRaw([]); + setEvVoortgangHistoryRaw([]); + localStorage.removeItem("vf_token"); + if (saveTimer.current) clearTimeout(saveTimer.current); + }; + + // ── Backup / restore ────────────────────────────────────────── + const doBackup = async () => { + const zip = new JSZip(); + const datum = new Date().toISOString().slice(0, 10); + zip.file("data.json", JSON.stringify({ ...data, evVoortgang: evRows, evVoortgangHistory }, null, 2)); + zip.file("meta.json", JSON.stringify({ versie: 1, datum, app: "vibefinance", gebruiker: currentUser?.email }, null, 2)); + const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `vibefinance_backup_${datum}.zip`; + a.click(); + URL.revokeObjectURL(url); + }; + + const doRestore = () => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + try { + const zip = await JSZip.loadAsync(file); + const entry = zip.file("data.json"); + if (!entry) throw new Error("Geen data.json gevonden in backup."); + const parsed = JSON.parse(await entry.async("string")); + const migrated = migrateData({ eigenVermogen: parsed.eigenVermogen, schuld: parsed.schuld }); + const newEvRows = parsed.evVoortgang || []; + const newEvVH = parsed.evVoortgangHistory || []; + setDataRaw(migrated); + setEvRowsRaw(newEvRows); + setEvVoortgangHistoryRaw(newEvVH); + await apiFetch("/data", { + method: "PUT", + body: JSON.stringify({ ...migrated, evVoortgang: newEvRows, evVoortgangHistory: newEvVH }), + }); + } catch (err) { alert("Ongeldig of beschadigd backup-bestand: " + err.message); } + }; + input.click(); + }; + + return ( + + {children} + + ); +} + +export const useApp = () => { + const ctx = useContext(AppContext); + if (!ctx) throw new Error("useApp must be used inside "); + return ctx; +}; diff --git a/frontend/src/hooks/useTheme.js b/frontend/src/hooks/useTheme.js new file mode 100644 index 0000000..7d29eb7 --- /dev/null +++ b/frontend/src/hooks/useTheme.js @@ -0,0 +1,23 @@ +/** + * Geeft een thema-object (T) terug op basis van darkMode. + * Wordt gebruikt door alle componenten om consistent kleuren te krijgen. + */ +export function useTheme(darkMode) { + return { + // Exacte Finex kleuren (dark) / licht alternatief + bg: darkMode ? "#241A39" : "#eef1f8", + card: darkMode ? "rgba(22,23,27,0.30)" : "#ffffff", + sidebar: darkMode ? "#16171B" : "#f8faff", + border: darkMode ? "rgba(139,92,246,0.15)" : "rgba(0,0,0,0.07)", + muted: darkMode ? "#8b8fa8" : "#94a3b8", + text: darkMode ? "#ffffff" : "#0f172a", + subtext: darkMode ? "#a0aec0" : "#64748b", + inputBg: darkMode ? "rgba(139,92,246,0.08)" : "#f1f5ff", + inputBorder:darkMode ? "rgba(139,92,246,0.2)" : "#e2e8f0", + contentBg: darkMode ? "#241A39" : "#eef1f8", + shadow: darkMode ? "0 4px 24px rgba(124,58,237,0.25)" : "0 4px 20px rgba(0,0,0,0.07)", + cardShadow: darkMode ? "0 4px 20px rgba(124,58,237,0.2)" : "0 2px 8px rgba(0,0,0,0.06)", + radius: "14px", + font: "'Poppins', system-ui, sans-serif", + }; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..0daebe8 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; + +createRoot(document.getElementById("root")).render( + + + +); diff --git a/frontend/src/pages/DashboardTab.jsx b/frontend/src/pages/DashboardTab.jsx new file mode 100644 index 0000000..db33a85 --- /dev/null +++ b/frontend/src/pages/DashboardTab.jsx @@ -0,0 +1,258 @@ +import { useState } from "react"; +import { useApp } from "../context/AppContext.jsx"; +import { fmt, pct, fmtDatum } from "../utils/format.js"; +import { GREEN, RED } from "../constants/index.js"; + +const TEAL = "#a855f7"; +import { VCard, VBadge } from "../components/ui/index.jsx"; + +export default function DashboardTab() { + const { ev, totEV, evRows, cats, T, darkMode, currentUser } = useApp(); + + const hour = new Date().getHours(); + const dagdeel = hour < 12 ? "Goedemorgen" : hour < 18 ? "Goedemiddag" : "Goedenavond"; + const rawNaam = currentUser?.naam?.trim().split(/\s+/)[0] || ""; + const voornaam = rawNaam ? rawNaam.charAt(0).toUpperCase() + rawNaam.slice(1) : ""; + + const catSum = (row) => + cats.reduce((a, c) => a + (row[c.id] || 0), 0); + + const startEV = evRows.length > 0 ? (evRows[evRows.length - 1]?.nettoWaarde || 0) : 0; + const extraFiat = evRows.length > 1 + ? evRows.slice(0, -1).reduce((acc, row) => acc + catSum(row), 0) : 0; + const totaalFiat = startEV + extraFiat; + const winst = totEV - totaalFiat; + const roi = totaalFiat > 0 ? winst / totaalFiat : 0; + const doel = ev.doel || 1; + const progressPct = Math.min(totEV / doel, 1); + const milestones = [0, 0.25, 0.5, 0.75, 1].map((f) => f * doel); + const kpis = [ + { label: "Start eigen vermogen", val: fmt(startEV), accent: TEAL}, + { label: "Extra geïnvesteerde fiat", val: fmt(extraFiat), accent: TEAL}, + { label: "Totaal geïnvesteerde fiat", val: fmt(totaalFiat), accent: TEAL}, + { label: "Totaal eigen vermogen", val: fmt(totEV), accent: GREEN }, + { label: "Winst op investering", val: fmt(winst), accent: winst >= 0 ? GREEN : RED }, + { label: "ROI %", val: totaalFiat > 0 ? pct(roi, 2) : "—", accent: roi >= 0 ? GREEN : RED }, + ]; + + // Minigrafiek data + const vData = [...evRows].sort((a, b) => a.datum.localeCompare(b.datum)); + + return ( +
+
+
+ Hi{voornaam ? ` ${voornaam},` : ","} +
+

+ {dagdeel} 👋 +

+
+ The Road naar {fmt(doel)} +
+
+ + {/* KPI grid */} +
+ {kpis.map(({ label, val, accent }) => ( + +
{label}
+
{val}
+
+ ))} +
+ + {/* Doel progressie */} + +
+
+ 🎯 + Doel & Bijbehorende Progressie + +
+ {pct(progressPct)} behaald +
+
+
+ {[0.25, 0.5, 0.75].map((f) => ( +
+ ))} +
+
+ {milestones.map((m) => ( + {fmt(m)} + ))} +
+ + + {/* EV Grafiek */} + +
Ontwikkeling van Eigen Vermogen
+ {vData.length < 2 ? ( +
+
📈
+
+ Leg minimaal 2 voortgangsmomenten vast in het Eigen Vermogen tabblad om de grafiek te tonen. +
+
+ ) : ( + + )} +
+ +
+ ); +} + +// ── Smooth Catmull-Rom bezier path ────────────────────────────────────────── +function crPath(xs, ys, t = 0.4) { + if (xs.length < 2) return ""; + let d = `M ${xs[0]},${ys[0]}`; + for (let i = 0; i < xs.length - 1; i++) { + const p0 = i > 0 ? [xs[i-1], ys[i-1]] : [xs[i], ys[i]]; + const p1 = [xs[i], ys[i]]; + const p2 = [xs[i+1], ys[i+1]]; + const p3 = i < xs.length - 2 ? [xs[i+2], ys[i+2]] : [xs[i+1], ys[i+1]]; + const cp1x = (p1[0] + (p2[0] - p0[0]) * t).toFixed(2); + const cp1y = (p1[1] + (p2[1] - p0[1]) * t).toFixed(2); + const cp2x = (p2[0] - (p3[0] - p1[0]) * t).toFixed(2); + const cp2y = (p2[1] - (p3[1] - p1[1]) * t).toFixed(2); + d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p2[0]},${p2[1]}`; + } + return d; +} + +// ── Mini inline SVG grafiek ──────────────────────────────────────────────────── +function MiniLineChart({ vData, darkMode }) { + const [hovered, setHovered] = useState(null); + const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 52; + const vals = vData.map((d) => d.nettoWaarde || 0); + const mn = Math.min(...vals) * 0.95; + const mx = Math.max(...vals) * 1.05; + const rng = mx - mn || 1; + const iW = W - pl - pr, iH = H - pt - pb; + const xs = vData.map((_, i) => pl + (i / Math.max(vData.length - 1, 1)) * iW); + const ys = vals.map((v) => pt + (1 - (v - mn) / rng) * iH); + const linePath = crPath(xs, ys); + const areaPath = `${linePath} L ${xs[xs.length-1]},${pt+iH} L ${xs[0]},${pt+iH} Z`; + const gc = darkMode ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)"; + const lc = darkMode ? "#6b7280" : "#94a3b8"; + + const handleMouseMove = (e) => { + const rect = e.currentTarget.getBoundingClientRect(); + const svgX = ((e.clientX - rect.left) / rect.width) * W; + const dists = xs.map((x) => Math.abs(x - svgX)); + setHovered(dists.indexOf(Math.min(...dists))); + }; + + const h = hovered; + const tipW = 114, tipH = 46; + const tipX = h !== null ? Math.min(Math.max(xs[h] - tipW / 2, pl), W - tipW - 4) : 0; + const tipY = h !== null ? (ys[h] - tipH - 14 < pt ? ys[h] + 14 : ys[h] - tipH - 14) : 0; + + return ( + setHovered(null)} + > + + + + + + + + + + + + + + + {/* Gestippelde gridlijnen */} + {[0, 0.25, 0.5, 0.75, 1].map((f) => { + const gy = pt + (1 - f) * iH; + return ( + + + + {fmt(mn + f * rng)} + + + ); + })} + + {/* Vlakkleur met gradient + fade */} + + + {/* Lijn */} + + + {/* Verticale hover-lijn */} + {h !== null && ( + + )} + + {/* Hover punt */} + {h !== null && ( + + )} + + {/* Tooltip */} + {h !== null && ( + + + + {fmtDatum(vData[h].datum)} + + + {fmt(vals[h])} + + + )} + + {/* X-as labels */} + {xs.map((x, i) => ( + + {fmtDatum(vData[i].datum)} + + ))} + + ); +} diff --git a/frontend/src/pages/EigenVermogenTab.jsx b/frontend/src/pages/EigenVermogenTab.jsx new file mode 100644 index 0000000..8f27bcd --- /dev/null +++ b/frontend/src/pages/EigenVermogenTab.jsx @@ -0,0 +1,653 @@ +import { useState } from "react"; +import { useApp } from "../context/AppContext.jsx"; +import { fmt, fmtDatum } from "../utils/format.js"; +import { PURPLE, PURPLE_LIGHT, GREEN, RED, CAT_COLORS } from "../constants/index.js"; +import { + VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter, + EmojiPicker, AppIcon, ConfirmDeleteDialog, EmptyState, +} from "../components/ui/index.jsx"; + +// ── AanpassenPopup (EV categorie) ───────────────────────────────────────────── +function AanpassenPopup({ cat, onClose }) { + const { data, setData, ev, cats, darkMode, T } = useApp(); + const [items, setItems] = useState(cat.items.map((i) => ({ ...i }))); + const [catNaam, setCatNaam] = useState(cat.naam); + const [catColor, setCatColor] = useState(cat.color); + const [catIcon, setCatIcon] = useState(cat.icon || ""); + const [showEmoji, setShowEmoji] = useState(false); + const [nieuwNaam, setNieuwNaam] = useState(""); + const [nieuwVal, setNieuwVal] = useState(""); + const [err, setErr] = useState(""); + const [confirmDel, setConfirmDel] = useState(false); + + const is = inputStyle(T); + + const updateItem = (id, field, val) => + setItems((p) => p.map((i) => i.id !== id ? i : { ...i, [field]: val })); + const removeItemLocal = (id) => { + if (items.length <= 1) return; + setItems((p) => p.filter((i) => i.id !== id)); + }; + const addNieuw = () => { + if (!nieuwNaam.trim()) { setErr("Naam is verplicht."); return; } + setItems((p) => [...p, { id: `i${Date.now()}`, naam: nieuwNaam.trim(), waarde: parseFloat(nieuwVal) || 0 }]); + setNieuwNaam(""); setNieuwVal(""); setErr(""); + }; + const handleSave = () => { + const updated = { + ...data, + eigenVermogen: { + ...ev, + categories: cats.map((c) => + c.id !== cat.id ? c : { + ...c, + naam: catNaam.trim() || c.naam, + color: catColor, + icon: catIcon, + items: items.map((i) => ({ ...i, waarde: parseFloat(i.waarde) || 0 })), + } + ), + }, + }; + setData(updated); onClose(); + }; + const handleDelete = () => { + setData({ ...data, eigenVermogen: { ...ev, categories: cats.filter((c) => c.id !== cat.id) } }); + onClose(); + }; + + return ( + + + +
+ {/* Naam + emoji */} + +
+
+ + setCatNaam(e.target.value)} + style={{ ...is, flex: 1, borderLeft: `3px solid ${cat.color}` }} + placeholder="Naam categorie" /> +
+ {showEmoji && ( + { setCatIcon(e); setShowEmoji(false); }} + onClear={() => { setCatIcon(""); setShowEmoji(false); }} + /> + )} +
+ + {/* Kleur */} + +
+ {CAT_COLORS.map((c) => ( +
+ + {/* Bestaande items */} + + {items.map((item) => ( +
+ updateItem(item.id, "naam", e.target.value)} + style={{ ...is, flex: 1 }} placeholder="Naam" /> + updateItem(item.id, "waarde", v)} is={is} T={T} /> + {items.length > 1 && ( + + )} +
+ ))} + + {/* Nieuw item */} +
+ +
+ { setNieuwNaam(e.target.value); setErr(""); }} + placeholder="Naam" style={{ ...is, flex: 1 }} /> + + +
+ {err && } +
+
+ + + {confirmDel ? ( + setConfirmDel(false)} + onConfirm={handleDelete} + /> + ) : ( + <> + + + + + )} + +
+
+ ); +} + +// ── CatKaart ────────────────────────────────────────────────────────────────── +function CatKaart({ cat }) { + const { locked, T } = useApp(); + const [showAanpassen, setShowAanpassen] = useState(false); + + return ( + + {showAanpassen && setShowAanpassen(false)} />} +
+ + {cat.icon && } + {cat.naam} + + {!locked && ( + + )} +
+ {cat.items.map((item) => ( +
+ + {item.naam} + + + {fmt(item.waarde)} + +
+ ))} +
+ + {fmt(cat.items.reduce((a, i) => a + (i.waarde || 0), 0))} + +
+
+ ); +} + +// ── EVVoortgangTabel ────────────────────────────────────────────────────────── +function EVVoortgangTabel() { + const { evRows, setEvRows, cats, locked, darkMode, T } = useApp(); + + const empty = () => ({ + datum: "", nettoWaarde: "", opmerkingen: "", + ...cats.reduce((a, c) => ({ ...a, [c.id]: "" }), {}), + }); + + const [showModal, setShowModal] = useState(false); + const [input, setInput] = useState(empty); + const [editDatum, setEditDatum] = useState(null); + const [err, setErr] = useState(""); + const is = inputStyle(T); + + const openModal = () => { setInput(empty()); setEditDatum(null); setErr(""); setShowModal(true); }; + const openEdit = (row) => { + setInput({ ...row, opmerkingen: row.opmerkingen || "" }); + setEditDatum(row.datum); setErr(""); setShowModal(true); + }; + const closeModal = () => { setShowModal(false); setErr(""); setEditDatum(null); }; + + const addRow = () => { + if (!input.datum) { setErr("Datum is verplicht."); return; } + const parsedCats = cats.reduce((a, c) => ({ ...a, [c.id]: parseFloat(input[c.id]) || 0 }), {}); + const hasValue = cats.some((c) => parsedCats[c.id] > 0); + if (!hasValue) { setErr("Vul minimaal één bedrag in."); return; } + const row = { + ...input, + ...parsedCats, + nettoWaarde: parseFloat(input.nettoWaarde) || 0, + }; + const updated = [...evRows.filter((r) => r.datum !== row.datum), row] + .sort((a, b) => b.datum.localeCompare(a.datum)); + setEvRows(updated); closeModal(); + }; + + const removeRow = (datum) => setEvRows(evRows.filter((r) => r.datum !== datum)); + + const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", background: T.card, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` }; + const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` }; + + return ( +
+ {/* Modal */} + {showModal && ( + + + +
+ + setInput((s) => ({ ...s, datum: e.target.value }))} + style={{ ...is, width: "100%", marginBottom: 16 }} /> + + +
+ {cats.map((c) => ( +
+ {c.naam} + setInput((s) => ({ ...s, [c.id]: v }))} + is={is} T={T} + /> +
+ ))} +
+ + + setInput((s) => ({ ...s, nettoWaarde: v }))} + is={is} T={T} + /> +
+ + + setInput((s) => ({ ...s, opmerkingen: e.target.value }))} + placeholder="Optionele notitie..." style={{ ...is, width: "100%" }} /> + + {err && } +
+ + + + + + + )} + +
+
Geïnvesteerde Fiat
+ {!locked && ( + + )} +
+ + {evRows.length > 0 ? ( +
+
+ + + + + {cats.map((c) => )} + + + + {!locked && } + + + + {evRows.map((row, i) => { + const vorigeNW = i < evRows.length - 1 ? (evRows[i + 1].nettoWaarde || 0) : null; + const verschil = vorigeNW !== null ? (row.nettoWaarde || 0) - vorigeNW : null; + const vColor = verschil === null ? T.muted : verschil > 0 ? GREEN : verschil < 0 ? RED : T.muted; + const vLabel = verschil === null ? "—" : `${verschil >= 0 ? "+" : ""}${fmt(verschil)}`; + return ( + + + {cats.map((c) => )} + + + + {!locked && ( + + )} + + ); + })} + +
Datum{c.naam}Netto WaardeVerschilOpmerkingen
{fmtDatum(row.datum)}{fmt(row[c.id] || 0)}{fmt(row.nettoWaarde || 0)}{vLabel}{row.opmerkingen || "—"} +
+ + +
+
+
+
+ ) : ( + + )} +
+ ); +} + +// ── DoelProgressie ──────────────────────────────────────────────────────────── +function DoelProgressie({ ev, data, setData, totVerdeling, locked, T }) { + const [editing, setEditing] = useState(false); + const [val, setVal] = useState(""); + + const doel = ev.doel || 1; + const progressPct = Math.min(totVerdeling / doel, 1); + + const openEdit = () => { setVal(String(doel)); setEditing(true); }; + const save = () => { + const n = parseFloat(val); + if (n > 0) setData({ ...data, eigenVermogen: { ...ev, doel: n } }); + setEditing(false); + }; + + return ( + +
+
+ 🎯 + Gewenst doel EV +
+
+ {fmt(totVerdeling)} + {editing ? ( +
+ / + setVal(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") setEditing(false); }} + style={{ + width: 130, padding: "4px 8px", borderRadius: 8, fontSize: 13, + background: T.inputBg, border: `1px solid ${PURPLE}`, color: T.text, outline: "none", + }} + /> + +
+ ) : ( + <> + / {fmt(doel)} + {!locked && ( + + )} + + )} + {Math.round(progressPct * 100)}% +
+
+
+
+
+ + ); +} + +// ── EigenVermogenTab (pagina) ───────────────────────────────────────────────── +export default function EigenVermogenTab() { + const { cats, catTotals, ev, data, setData, locked, darkMode, T, evRows } = useApp(); + const [showAddCat, setShowAddCat] = useState(false); + const [newNaam, setNewNaam] = useState(""); + const [newColor, setNewColor] = useState(CAT_COLORS[0]); + + const totVerdeling = catTotals.reduce((a, c) => a + c.tot, 0); + const sumCat = (id) => evRows.reduce((a, r) => a + (r[id] || 0), 0); + const totGeinv = cats.reduce((a, c) => a + sumCat(c.id), 0); + + const iStyle2 = inputStyle(T); + + const addCat = () => { + if (!newNaam.trim()) return; + setData({ + ...data, + eigenVermogen: { + ...ev, + categories: [...cats, { + id: `c${Date.now()}`, naam: newNaam, color: newColor, + items: [{ id: `i${Date.now()}`, naam: "Item 1", waarde: 0 }], + }], + }, + }); + setNewNaam(""); setShowAddCat(false); + }; + + const summaryBlocks = [ + { + title: "Verdeling Eigen Vermogen", accent: GREEN, + rows: catTotals.map((c) => ({ label: c.naam, value: fmt(c.tot), color: c.color })), + total: fmt(totVerdeling), totalColor: GREEN, + }, + { + title: "Geïnvesteerde Fiat", accent: PURPLE_LIGHT, + rows: cats.map((c) => ({ label: c.naam, value: evRows.length > 0 ? fmt(sumCat(c.id)) : "—", color: c.color })), + total: evRows.length > 0 ? fmt(totGeinv) : "—", totalColor: PURPLE_LIGHT, + }, + { + title: "Rendement", accent: "#f59e0b", + rows: cats.map((c) => { + const ev2 = catTotals.find((ct) => ct.id === c.id)?.tot ?? 0; + const g = sumCat(c.id); + const r = g > 0 ? (ev2 - g) / g : null; + return { label: c.naam, value: r === null ? "—" : `${(r * 100).toFixed(1)}%`, color: r === null ? T.muted : r >= 0 ? GREEN : RED }; + }), + total: (() => { + const et = totVerdeling; + const gt = totGeinv; + return gt > 0 ? `${(((et - gt) / gt) * 100).toFixed(1)}%` : "—"; + })(), + totalColor: (() => { + return totGeinv > 0 ? (totVerdeling >= totGeinv ? GREEN : RED) : T.muted; + })(), + }, + ]; + + return ( +
+

Eigen Vermogen

+ + {/* Doel progressie */} + + + {/* Summary blokken */} +
+ {summaryBlocks.map((block) => ( +
+
{block.title}
+ {block.rows.map(({ label, value, color }) => ( +
+ {label} + {value} +
+ ))} +
+ Totaal + {block.total} +
+
+ ))} + + {totVerdeling > 0 && ( +
+ ({ val: c.tot, color: c.color }))} total={totVerdeling} size={200} label="Verdeling" centerVal={fmt(totVerdeling)} /> +
+ {catTotals.map((c) => ( +
+
+ {c.naam} +
+ ))} +
+
+ )} +
+ + {/* Nieuwe categorie */} + {!locked && ( +
+ +
+ )} + {showAddCat && ( + setShowAddCat(false)}> + + setShowAddCat(false)} headerBg={darkMode ? `${PURPLE}08` : "#f8f5ff"} T={T} /> +
+
Naam *
+ setNewNaam(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addCat()} + style={{ ...iStyle2, marginBottom: 16, width: "100%", boxSizing: "border-box" }} /> +
Kleur
+
+ {CAT_COLORS.map((c) => ( +
+
+ + + + +
+
+ )} + + {/* Categorie kaarten */} +
+ {cats.map((cat) => )} +
+ + {/* Voortgang tabel */} + + + +
+ ); +} + +// ── Gedeelde kleine helpers ──────────────────────────────────────────────────── +function inputStyle(T) { + return { + background: T.inputBg, border: `1px solid ${T.inputBorder}`, + borderRadius: 8, color: T.text, padding: "8px 10px", + fontSize: 13, outline: "none", boxSizing: "border-box", + }; +} +function FieldLabel({ label, T }) { + return ( +
{label}
+ ); +} +function EuroInput({ value, onChange, is, T }) { + return ( +
+ € + onChange(e.target.value)} + placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} /> +
+ ); +} +function ErrMsg({ msg }) { + return ( +
{msg}
+ ); +} +const primaryBtn = { padding: "10px 20px", background: PURPLE, border: "none", borderRadius: 8, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 }; +const editBtn = { padding: "2px 10px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 600 }; +const deleteSmBtn = { padding: "2px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 12 }; +const trashBtn = { padding: "10px 14px", background: "rgba(239,68,68,0.08)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 10, color: RED, cursor: "pointer", fontWeight: 600, fontSize: 13 }; +const addBtn = { padding: "6px 14px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 8, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 }; +const cancelBtn = (T) => ({ padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 }); diff --git a/frontend/src/pages/SchuldTab.jsx b/frontend/src/pages/SchuldTab.jsx new file mode 100644 index 0000000..5f05223 --- /dev/null +++ b/frontend/src/pages/SchuldTab.jsx @@ -0,0 +1,452 @@ +import { useState } from "react"; +import { useApp } from "../context/AppContext.jsx"; +import { fmt, pct } from "../utils/format.js"; +import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js"; +import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, EmojiPicker, AppIcon, ConfirmDeleteDialog } from "../components/ui/index.jsx"; + +// ── SchuldAanpassenPopup ────────────────────────────────────────────────────── +function SchuldAanpassenPopup({ blok, onClose }) { + const { data, setData, schuld, schuldBlokken, darkMode, T } = useApp(); + const [items, setItems] = useState(blok.items.map((i) => ({ ...i }))); + const [blokNaam, setBlokNaam] = useState(blok.naam); + const [blokIcon, setBlokIcon] = useState(blok.icon || ""); + const [showEmoji, setShowEmoji] = useState(false); + const [nieuwNaam, setNieuwNaam] = useState(""); + const [nieuwVal, setNieuwVal] = useState(""); + const [nieuwErr, setNieuwErr] = useState(""); + const [confirmDel, setConfirmDel] = useState(false); + + const is = inputStyle(T); + + const updateItem = (id, field, val) => + setItems((p) => p.map((i) => i.id !== id ? i : { ...i, [field]: val })); + const removeItemLocal = (id) => { + if (items.length <= 1) return; + setItems((p) => p.filter((i) => i.id !== id)); + }; + const addNieuw = () => { + if (!nieuwNaam.trim()) { setNieuwErr("Naam is verplicht."); setTimeout(() => setNieuwErr(""), 3000); return; } + setItems((p) => [...p, { id: `si${Date.now()}`, naam: nieuwNaam.trim(), waarde: parseFloat(nieuwVal) || 0 }]); + setNieuwNaam(""); setNieuwVal(""); setNieuwErr(""); + }; + const handleSave = () => { + setData({ + ...data, + schuld: { + ...schuld, + blokken: schuldBlokken.map((b) => + b.id !== blok.id ? b : { + ...b, naam: blokNaam.trim() || b.naam, icon: blokIcon, + items: items.map((i) => ({ ...i, waarde: parseFloat(i.waarde) || 0 })), + } + ), + }, + }); + onClose(); + }; + const handleDelete = () => { + setData({ ...data, schuld: { ...schuld, blokken: schuldBlokken.filter((b) => b.id !== blok.id) } }); + onClose(); + }; + + return ( + + + +
+ {/* Naam + emoji */} + +
+
+ + setBlokNaam(e.target.value)} + style={{ ...is, flex: 1, borderLeft: `3px solid ${blok.color}` }} + placeholder="Naam categorie" /> +
+ {showEmoji && ( + { setBlokIcon(e); setShowEmoji(false); }} + onClear={() => { setBlokIcon(""); setShowEmoji(false); }} + /> + )} +
+ + {/* Items */} + + {items.map((item) => ( +
+ updateItem(item.id, "naam", e.target.value)} + style={{ ...is, flex: 1 }} placeholder="Naam" /> + updateItem(item.id, "waarde", v)} is={is} T={T} /> + {items.length > 1 && ( + + )} +
+ ))} + + {/* Nieuw item */} +
+ +
+ { setNieuwNaam(e.target.value); setNieuwErr(""); }} + placeholder="Naam" style={{ ...is, flex: 1 }} /> + + +
+ {nieuwErr && } +
+
+ + {/* Footer */} +
+ {confirmDel ? ( + setConfirmDel(false)} + onConfirm={handleDelete} + /> + ) : ( +
+ + + +
+ )} +
+
+
+ ); +} + +// ── SchuldKaart ─────────────────────────────────────────────────────────────── +function SchuldKaart({ blok }) { + const { locked, T } = useApp(); + const [showAanpassen, setShowAanpassen] = useState(false); + const subtot = blok.items.reduce((a, i) => a + (i.waarde || 0), 0); + + return ( + + {showAanpassen && setShowAanpassen(false)} />} +
+ + {blok.icon && } + {blok.naam} + + {!locked && ( + + )} +
+ {blok.items.map((item) => ( +
+ + {item.naam} + + + {fmt(item.waarde)} + +
+ ))} +
+ {fmt(subtot)} +
+
+ ); +} + +// ── AflossingProgressie ─────────────────────────────────────────────────────── +function AflossingProgressie() { + const { data, setData, schuld, totSchuld, locked, darkMode, T } = useApp(); + const init = schuld.initieleSchuld || 0; + const betaald = Math.min(Math.max(init - totSchuld, 0), init); + const aflossingPct = init > 0 ? betaald / init : 0; + const [editOpen, setEditOpen] = useState(false); + const [editVal, setEditVal] = useState(schuld.initieleSchuld || ""); + + const saveInitiele = () => { + setData({ ...data, schuld: { ...schuld, initieleSchuld: parseFloat(editVal) || 0 } }); + setEditOpen(false); + }; + + return ( + + {editOpen && ( + setEditOpen(false)}> + + setEditOpen(false)} + headerBg={darkMode ? `${RED}11` : `${RED}09`} + T={T} + /> +
+
Bedrag (€)
+
+ € + setEditVal(e.target.value)} autoFocus + style={{ background: T.inputBg, border: `1px solid ${T.inputBorder}`, borderRadius: 10, color: T.text, padding: "10px 12px 10px 28px", fontSize: 14, outline: "none", width: "100%", boxSizing: "border-box" }} /> +
+
+
+ + +
+
+
+ )} + +
+ Aflossing progressie +
+ Initiële schuld: + {fmt(schuld.initieleSchuld)} + {!locked && ( + + )} +
+
+
+
+
+
+ €0 + {pct(aflossingPct)} afgelost + {fmt(init)} +
+ + ); +} + +// ── NieuweCategoriepopup ────────────────────────────────────────────────────── +function NieuweCategoriepopup({ onSave, onClose, T, darkMode }) { + const is = { background: T.inputBg, border: `1px solid ${T.inputBorder}`, borderRadius: 8, color: T.text, padding: "10px 12px", fontSize: 13, outline: "none", boxSizing: "border-box", width: "100%" }; + const COLORS = ["#ef4444","#f97316","#eab308","#a855f7","#06b6d4","#10b981","#3b82f6","#ec4899","#6b7280"]; + const [naam, setNaam] = useState(""); + const [color, setColor] = useState(COLORS[0]); + + const save = () => { + if (!naam.trim()) return; + onSave({ naam: naam.trim(), color }); + onClose(); + }; + + return ( + + + +
+
Naam *
+ setNaam(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && save()} + placeholder="Bijv. Hypotheek" style={{ ...is, marginBottom: 16 }} /> +
Kleur
+
+ {COLORS.map((c) => ( +
+
+
+ + +
+
+
+ ); +} + +// ── SchuldTab (pagina) ──────────────────────────────────────────────────────── +export default function SchuldTab() { + const { data, setData, schuld, schuldBlokken, locked, darkMode, T } = useApp(); + const [showNieuweCategorie, setShowNieuweCategorie] = useState(false); + + const schuldCats = schuldBlokken.map((b) => ({ + ...b, subtot: b.items.reduce((a, i) => a + (i.waarde || 0), 0), + })); + const donutTotal = schuldCats.reduce((a, b) => a + b.subtot, 0); + + + const addSchuldBlok = ({ naam, color }) => { + setData({ + ...data, + schuld: { + ...schuld, + blokken: [...schuldBlokken, { + id: `s${Date.now()}`, naam, color, + items: [{ id: `si${Date.now()}`, naam: "Item 1", waarde: 0 }], + }], + }, + }); + }; + + return ( +
+

Schuld

+ + + +
+ + {schuldCats.length > 0 && ( +
+ {/* Verdeling lijst */} +
+
+ Verdeling Schuld +
+ {schuldCats.map((b) => ( +
+ {b.naam} + {fmt(b.subtot)} +
+ ))} +
+ Totaal + {fmt(donutTotal)} +
+
+ + {/* Donut */} +
+ ({ val: b.subtot, color: b.color }))} + total={donutTotal} size={200} label="Totaal schuld" centerVal={fmt(donutTotal)} + /> +
+ {schuldCats.map((b) => ( +
+
+ {b.naam} +
+ ))} +
+
+
+ )} + + {!locked && ( + + )} + {showNieuweCategorie && ( + setShowNieuweCategorie(false)} + T={T} + darkMode={darkMode} + /> + )} + +
+ {schuldCats.map((blok) => )} +
+ +
+ ); +} + +// ── kleine helpers ──────────────────────────────────────────────────────────── +function inputStyle(T) { + return { + background: T.inputBg, border: `1px solid ${T.inputBorder}`, + borderRadius: 8, color: T.text, padding: "8px 10px", + fontSize: 13, outline: "none", boxSizing: "border-box", + }; +} +function FieldLabel({ label, T }) { + return ( +
+ {label} +
+ ); +} +function EuroInput({ value, onChange, is, T }) { + return ( +
+ € + onChange(e.target.value)} + placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} /> +
+ ); +} +function ErrMsg({ msg }) { + return
{msg}
; +} +const deleteSmBtn = { padding: "4px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 13, flexShrink: 0 }; +const trashBtn = { padding: "10px 14px", background: "rgba(239,68,68,0.08)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 10, color: RED, cursor: "pointer", fontWeight: 600, fontSize: 13 }; +const addBtn = { padding: "6px 14px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 8, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 }; +const cancelBtn = (T) => ({ padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 }); diff --git a/frontend/src/pages/VoortgangTab.jsx b/frontend/src/pages/VoortgangTab.jsx new file mode 100644 index 0000000..fc62f00 --- /dev/null +++ b/frontend/src/pages/VoortgangTab.jsx @@ -0,0 +1,438 @@ +import { useState } from "react"; +import { useApp } from "../context/AppContext.jsx"; +import { fmt, fmtDatum } from "../utils/format.js"; +import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js"; +import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx"; + +// ── Inline SVG line chart ───────────────────────────────────────────────────── +function SvgLineChart({ pts, lines, T, H = 200 }) { + const [hovered, setHovered] = useState(null); // { lineKey, ptIdx } + + if (pts.length < 2) { + return ( +
+ Minimaal 2 datapunten nodig +
+ ); + } + const W = 700, pl = 64, pr = 16, pt2 = 16, pb = 62; + const iW = W - pl - pr, iH = H - pt2 - pb; + const allVals = lines.flatMap((l) => pts.map((p) => p[l.key] ?? 0)); + const dataMin = Math.min(...allVals); + const dataMax = Math.max(...allVals); + const padding = (dataMax - dataMin) * 0.08 || 1; + const mn = dataMin - padding; + const mx = dataMax + padding; + const rng = mx - mn || 1; + const xp = (i) => pl + (i / (pts.length - 1)) * iW; + const yp = (v) => pt2 + (1 - ((v ?? 0) - mn) / rng) * iH; + const gc = T.bg === "#0f1117" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)"; + const darkMode = T.bg === "#0f1117"; + + return ( +
+ + + {lines.map((l) => ( + + + + + ))} + + {[0, 0.25, 0.5, 0.75, 1].map((f) => { + const gy = pt2 + (1 - f) * iH; + return ( + + + + {fmt(mn + f * rng)} + + + ); + })} + {lines.map((l) => { + const validPts = pts.map((p, i) => ({ ...p, _i: i })).filter((p) => p[l.key] !== null); + if (validPts.length < 2) return null; + const xs = validPts.map((p) => xp(p._i)); + const ys = validPts.map((p) => yp(p[l.key])); + const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" "); + const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`; + return ( + + + + {validPts.map((p, i) => { + const x = xs[i], y = ys[i]; + const isHov = hovered?.lineKey === l.key && hovered?.ptIdx === i; + const tipW = 90, tipH = 22; + const tipX = Math.min(Math.max(x - tipW / 2, pl), W - tipW - 4); + const tipY = y - tipH - 8 < pt2 ? y + 10 : y - tipH - 8; + return ( + setHovered({ lineKey: l.key, ptIdx: i })} + onMouseLeave={() => setHovered(null)} + style={{ cursor: "pointer" }} + > + + + {isHov && ( + + + + {fmt(p[l.key])} + + + )} + + ); + })} + + ); + })} + {pts.map((p, i) => ( + + {fmtDatum(p.datum)} + + ))} + +
+ {lines.map((l) => ( +
+
+
+ {l.label} +
+ ))} +
+
+ ); +} + +// ── VoortgangModal ──────────────────────────────────────────────────────────── +function VoortgangModal({ onSave, onClose, T, darkMode, initialData }) { + const is = { background: T.inputBg, border: `1px solid ${T.inputBorder}`, borderRadius: 8, color: T.text, padding: "8px 10px", fontSize: 13, outline: "none", boxSizing: "border-box" }; + const isEdit = !!initialData; + const [datum, setDatum] = useState(initialData?.datum || ""); + const [ev, setEv] = useState(initialData?.ev != null ? String(initialData.ev) : ""); + const [schuld, setSchuld] = useState(initialData?.schuld != null ? String(initialData.schuld) : ""); + const [opmerkingen, setOpmerkingen] = useState(initialData?.opmerkingen || ""); + const [err, setErr] = useState(""); + + const save = () => { + if (!datum) { setErr("Vul een datum in."); setTimeout(() => setErr(""), 3000); return; } + if (!ev && !schuld) { setErr("Vul minimaal Eigen Vermogen of Schuld in."); setTimeout(() => setErr(""), 3000); return; } + onSave({ + datum, + ev: ev ? parseFloat(ev) : null, + schuld: schuld ? parseFloat(schuld) : null, + opmerkingen, + }); + onClose(); + }; + + return ( + + + +
+ + setDatum(e.target.value)} + style={{ ...is, width: "100%", marginBottom: 16 }} /> + + +
+ € + setEv(e.target.value)} + placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} /> +
+ + +
+ € + setSchuld(e.target.value)} + placeholder="0" style={{ ...is, paddingLeft: 22, width: "100%" }} /> +
+ + + setOpmerkingen(e.target.value)} + placeholder="Optionele notitie..." + style={{ ...is, width: "100%" }} /> + + {err && ( +
+ {err} +
+ )} +
+ + + + +
+
+ ); +} + +function VLabel({ label, T }) { + return ( +
+ {label} +
+ ); +} + +// ── VoortgangTab (pagina) ───────────────────────────────────────────────────── +export default function VoortgangTab() { + const [showAdd, setShowAdd] = useState(false); + const [editRow, setEditRow] = useState(null); + const { + totEV, totSchuld, catTotals, schuldBlokken, schuldHistory, + evVoortgangHistory, setEvVoortgangHistory, + data, setData, schuld, locked, darkMode, T, + } = useApp(); + + const nettoWaarde = totEV - totSchuld; + + const schuldCatsDash = schuldBlokken.map((b) => ({ + naam: b.naam, color: b.color, + subtot: b.items.reduce((a, i) => a + (i.waarde || 0), 0), + })); + + const combined = [...new Set([ + ...evVoortgangHistory.map((r) => r.datum), + ...schuldHistory.map((s) => s.datum), + ])].sort((a, b) => a.localeCompare(b)).map((d) => { + const evRow = evVoortgangHistory.find((r) => r.datum === d); + const schRow = schuldHistory.find((s) => s.datum === d); + const evVal = evRow ? (evRow.ev || 0) : null; + const schVal = schRow ? schRow.schuld : null; + return { + datum: d, + ev: evVal, + schuld: schVal, + netto: evVal !== null && schVal !== null ? evVal - schVal : null, + opmerkingen: evRow?.opmerkingen || "", + }; + }); + + const timeline = combined; + const voortgangRows = [...combined].reverse(); + + const addVoortgang = ({ datum, ev: evVal, schuld: schuldVal, opmerkingen }) => { + if (evVal !== null) { + const newRow = { datum, ev: evVal, opmerkingen: opmerkingen || "" }; + const updated = [...evVoortgangHistory.filter((r) => r.datum !== datum), newRow] + .sort((a, b) => b.datum.localeCompare(a.datum)); + setEvVoortgangHistory(updated); + } + if (schuldVal !== null) { + const updated = [...schuldHistory.filter((s) => s.datum !== datum), { datum, schuld: schuldVal }] + .sort((a, b) => b.datum.localeCompare(a.datum)); + setData({ ...data, schuld: { ...schuld, schuldHistory: updated } }); + } + }; + + const removeVoortgang = (datum) => { + setEvVoortgangHistory(evVoortgangHistory.filter((r) => r.datum !== datum)); + setData({ ...data, schuld: { ...schuld, schuldHistory: schuldHistory.filter((s) => s.datum !== datum) } }); + }; + + const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", background: T.card, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` }; + const td = { padding: "7px 10px", fontSize: 12, color: T.text, borderBottom: `1px solid ${T.border}` }; + + return ( +
+
+

Voortgang Schuld & EV

+
+ + {/* Hero netto */} + +
+
+
Mijn Nettowaarde
+
= 0 ? GREEN : RED }}>{fmt(nettoWaarde)}
+
+
+
+
Eigen Vermogen
+
{fmt(totEV)}
+
+
+
Totale Schuld
+
{fmt(totSchuld)}
+
+
+
+
+ + {/* KPI kaarten */} +
+ {/* Schuld verdeling */} + +
Verdeling Schuld
+ {schuldCatsDash.map((b) => ( +
+
+
+ {b.naam} +
+ {fmt(b.subtot)} +
+ ))} +
+ Totaal schuld + {fmt(totSchuld)} +
+ + + {/* EV verdeling */} + +
Verdeling Eigen Vermogen
+ {catTotals.map((c) => ( +
+
+
+ {c.naam} +
+ {fmt(c.tot)} +
+ ))} +
+ Totaal EV + {fmt(totEV)} +
+ + + {/* Rendement */} + +
Rendement Eigen Vermogen
+
+
Bedrag
+
—
+
+
+
Percentage
+
—
+
+
+ + {/* Donut */} + + +
+
+
+ Eigen Vermogen +
+
+
+ Schuld +
+
+ +
+ + {/* Grafieken */} + +
Ontwikkeling Schuld en EV
+ +
+ + +
Ontwikkeling Mijn Nettowaarde
+ +
+ + {/* Voortgang tabel */} + +
+
Voortgang overzicht
+ {!locked && ( + + )} +
+ {showAdd && !locked && ( + setShowAdd(false)} T={T} darkMode={darkMode} /> + )} + {editRow && !locked && ( + setEditRow(null)} T={T} darkMode={darkMode} initialData={editRow} /> + )} + {voortgangRows.length > 0 ? ( +
+ + + + + + + + + {!locked && } + + + + {voortgangRows.map((row, i) => ( + + + + + + + {!locked && ( + + )} + + ))} + +
DatumEigen VermogenSchuldNettowaardeOpmerkingen
{fmtDatum(row.datum)}{row.ev !== null ? fmt(row.ev) : "—"}{row.schuld !== null ? fmt(row.schuld) : "—"}= 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}{row.opmerkingen || "—"} + + +
+
+ ) : ( +
+ Nog geen gecombineerde datapunten. Voeg schuld- en EV-momenten toe. +
+ )} +
+ +
+ ); +} diff --git a/frontend/src/utils/format.js b/frontend/src/utils/format.js new file mode 100644 index 0000000..523235e --- /dev/null +++ b/frontend/src/utils/format.js @@ -0,0 +1,33 @@ +/** Euro formatter (nl-NL, geen decimalen) */ +export const fmt = (n) => + new Intl.NumberFormat("nl-NL", { + style: "currency", currency: "EUR", maximumFractionDigits: 0, + }).format(n || 0); + +/** Percentage formatter */ +export const pct = (n, d = 1) => `${((n || 0) * 100).toFixed(d)}%`; + +/** ISO datum (YYYY-MM-DD) → Nederlandse notatie (DD-MM-YYYY) */ +export const fmtDatum = (s) => { + if (!s) return s; + const [y, m, d] = s.split("-"); + return `${d}-${m}-${y}`; +}; + +/** Crypto-bedrag formatter (4 decimalen standaard) */ +export const fmtCrypto = (n, d = 4) => (parseFloat(n) || 0).toFixed(d); + +/** + * Bedrag in USD of EUR opmaken afhankelijk van geselecteerde valuta. + * @param {number} amount + * @param {"USD"|"EUR"} currency + * @param {number} usdEurRate + */ +export function fmtMoney(amount, currency = "EUR", usdEurRate = 0.92) { + const v = currency === "EUR" ? amount * usdEurRate : amount; + return new Intl.NumberFormat("nl-NL", { + style: "currency", + currency, + maximumFractionDigits: 0, + }).format(v || 0); +} diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js new file mode 100644 index 0000000..9b7f60d --- /dev/null +++ b/frontend/src/utils/index.js @@ -0,0 +1,2 @@ +export * from "./format.js"; +export * from "./storage.js"; diff --git a/frontend/src/utils/storage.js b/frontend/src/utils/storage.js new file mode 100644 index 0000000..c0c6536 --- /dev/null +++ b/frontend/src/utils/storage.js @@ -0,0 +1,75 @@ +const KEYS = { + data: "wbjw_data", + evRows: "ev_voortgang_rows", + users: "wbjw_users", + exitplan: "exitplan_data", +}; + +export function loadFromStorage(key) { + try { + const raw = localStorage.getItem(KEYS[key] ?? key); + return raw ? JSON.parse(raw) : null; + } catch { return null; } +} + +export function saveToStorage(key, value) { + try { localStorage.setItem(KEYS[key] ?? key, JSON.stringify(value)); } catch {} +} + +export function clearStorage(key) { + try { localStorage.removeItem(KEYS[key] ?? key); } catch {} +} + +/** Exporteer alle data als JSON-backup blob */ +export function buildBackup() { + return { + wbjw_data: localStorage.getItem(KEYS.data), + ev_voortgang_rows: localStorage.getItem(KEYS.evRows), + wbjw_users: localStorage.getItem(KEYS.users), + exitplan_data: localStorage.getItem(KEYS.exitplan), + datum: new Date().toISOString(), + versie: "2.0", + }; +} + +/** Herstel een backup-object naar localStorage */ +export function restoreBackup(backup) { + if (backup.wbjw_data) localStorage.setItem(KEYS.data, backup.wbjw_data); + if (backup.ev_voortgang_rows) localStorage.setItem(KEYS.evRows, backup.ev_voortgang_rows); + if (backup.wbjw_users) localStorage.setItem(KEYS.users, backup.wbjw_users); + if (backup.exitplan_data) localStorage.setItem(KEYS.exitplan, backup.exitplan_data); +} + +/** Migreer oud schuld-formaat naar blokken-formaat */ +export function migrateData(parsed) { + if (parsed.schuld && !parsed.schuld.blokken) { + const h = parsed.schuld.hypotheek || {}; + const b = parsed.schuld.belastingdienst || {}; + const ex = parsed.schuld.extra || []; + parsed.schuld.blokken = [ + { + id: "s1", naam: "Hypotheek", color: "#ef4444", + items: Object.entries({ + deel1: "Leningdeel 1", deel2: "Leningdeel 2", + deel3: "Leningdeel 3", deel4: "Leningdeel 4", + deel5: "Sub-categorie 5", + }).map(([k, n], i) => ({ id: `s1i${i + 1}`, naam: n, waarde: h[k] || 0 })), + }, + { + id: "s2", naam: "Belastingdienst", color: "#f97316", + items: Object.entries({ + sub1: "Kinderopvangtoeslag", sub2: "Sub-categorie 2", + sub3: "Sub-categorie 3", sub4: "Sub-categorie 4", + sub5: "Sub-categorie 5", + }).map(([k, n], i) => ({ id: `s2i${i + 1}`, naam: n, waarde: b[k] || 0 })), + }, + ...ex.map((cat, ci) => ({ ...cat, id: `s${ci + 3}` })), + ]; + delete parsed.schuld.hypotheek; + delete parsed.schuld.belastingdienst; + delete parsed.schuld.extra; + } + if (!parsed.schuld.schuldHistory) parsed.schuld.schuldHistory = []; + if (!parsed.schuld.initieleSchuld) parsed.schuld.initieleSchuld = 303000; + return parsed; +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..7def53e --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,41 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8")); + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + // In dev: proxy /api naar de backend zodat je geen CORS-issues hebt + proxy: { + "/api": { + target: "http://backend:3001", + changeOrigin: true, + secure: false, + }, + }, + }, + build: { + outDir: "dist", + sourcemap: false, // zet op true voor staged deploys + chunkSizeWarningLimit: 800, + rollupOptions: { + output: { + // Splits vendor chunks voor betere caching + manualChunks: { + react: ["react", "react-dom"], + }, + }, + }, + }, + define: { + // VITE_API_URL wordt ingebakken tijdens build + __API_URL__: JSON.stringify(process.env.VITE_API_URL ?? "/api"), + __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION ?? pkg.version), + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..082c73a --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "vibefinance", + "version": "0.1.5-dev", + "description": "VibeFinance — persoonlijk vermogensbeheer", + "private": true, + "repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance", + "updateCheckUrl": "https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest" +} \ No newline at end of file diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..0f27070 --- /dev/null +++ b/release.ps1 @@ -0,0 +1,208 @@ +# ── VibeFinance release script (PowerShell) ────────────────────────────── +# Productie release: bouwt multi-platform, pusht naar registry, +# maakt een Git-tag aan en pusht naar origin. +# +# Gebruik: +# .\release.ps1 # bouw + push images + Git-tag +# .\release.ps1 -NoBuild # sla docker build over, alleen Git-tag +# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren +# .\release.ps1 -Force # sla conflictcontrole over, overschrijf bestaande tags +# +# Vereisten: +# - Docker Desktop actief +# - Ingelogd op de registry: docker login +# - docker buildx builder actief: docker buildx create --use +# - Git geïnstalleerd en working directory is de repo root +# - root package.json bevat een schoon versienummer ZONDER -dev suffix + +param( + [switch]$NoBuild, + [switch]$DryRun, + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +# ════════════════════════════════════════════════════════════════════════════ +# ── PROJECTCONFIGURATIE — pas alleen dit blok aan voor een nieuw project ── +# ════════════════════════════════════════════════════════════════════════════ +$appName = "VibeFinance" +$registry = "vibehoogie.duckdns.org/vibe" +$backendImage = "vibefinance-backend" +$frontendImage = "vibefinance-frontend" +$apiUrl = "/api" +# ════════════════════════════════════════════════════════════════════════════ + +# ── Versie uit root package.json ─────────────────────────────────────────── +$rootPkg = Get-Content (Join-Path $PSScriptRoot "package.json") -Raw | ConvertFrom-Json +$appVersion = $rootPkg.version + +if (-not $appVersion) { + Write-Error "Kon versienummer niet lezen uit package.json" + exit 1 +} + +if ($appVersion -match "-dev$") { + Write-Error "Versienummer '$appVersion' bevat '-dev' suffix. Verwijder de suffix voor een productie-release." + exit 1 +} + +# ── Versie synchroniseren naar frontend/package.json ────────────────────── +$frontendPkgPath = Join-Path $PSScriptRoot "frontend/package.json" +$frontendPkg = Get-Content $frontendPkgPath -Raw | ConvertFrom-Json +if ($frontendPkg.version -ne $appVersion) { + $frontendPkg.version = $appVersion + [System.IO.File]::WriteAllText($frontendPkgPath, ($frontendPkg | ConvertTo-Json -Depth 10)) + Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green +} + +$gitTag = "$appVersion" + +# ── Registry tags ────────────────────────────────────────────────────────── +$backendBase = "$registry/$backendImage" +$frontendBase = "$registry/$frontendImage" + +$backendVersionTag = "$backendBase`:$appVersion" +$frontendVersionTag = "$frontendBase`:$appVersion" +$backendLatestTag = "$backendBase`:latest" +$frontendLatestTag = "$frontendBase`:latest" + +# ── Helpers ──────────────────────────────────────────────────────────────── +function Invoke-Cmd { + param([string[]]$Cmd) + $display = $Cmd -join " " + if ($DryRun) { + Write-Host "[DRY-RUN] $display" -ForegroundColor Yellow + return "" + } + Write-Host "> $display" -ForegroundColor DarkGray + $output = & $Cmd[0] $Cmd[1..($Cmd.Length - 1)] + if ($LASTEXITCODE -ne 0) { throw "Commando mislukt: $display" } + return $output +} + +function Write-Step { + param([string]$Label) + Write-Host "" + Write-Host "── $Label" -ForegroundColor Cyan +} + +# ── Samenvatting ─────────────────────────────────────────────────────────── +Write-Host "" +Write-Host "════════════════════════════════════════" -ForegroundColor Cyan +Write-Host " $appName Release" -ForegroundColor Cyan +Write-Host " Versie : $appVersion" -ForegroundColor Green +Write-Host " Git-tag : $gitTag" -ForegroundColor Green +Write-Host " Registry: $registry" -ForegroundColor Green +if ($DryRun) { Write-Host " Mode : DRY-RUN (geen wijzigingen)" -ForegroundColor Yellow } +if ($NoBuild) { Write-Host " Mode : SKIP BUILD" -ForegroundColor Yellow } +if ($Force) { Write-Host " Mode : FORCE (conflictcontrole overgeslagen)" -ForegroundColor Red } +Write-Host "════════════════════════════════════════" -ForegroundColor Cyan + +# ── Stap 1: Conflictcontrole ────────────────────────────────────────────── +Write-Step "[1/3] Conflictcontrole" + +if (-not $DryRun -and -not $Force) { + $localTag = git tag --list $gitTag + if ($localTag) { + Write-Error "Git-tag '$gitTag' bestaat al lokaal. Verhoog het versienummer in package.json of gebruik -Force." + exit 1 + } + + $remoteTags = git ls-remote --tags origin "refs/tags/$gitTag" + if ($remoteTags) { + Write-Error "Git-tag '$gitTag' bestaat al op origin. Verhoog het versienummer in package.json of gebruik -Force." + exit 1 + } + + Write-Host "Controleer registry: $backendVersionTag" -ForegroundColor DarkGray + $ErrorActionPreference = "SilentlyContinue" + docker manifest inspect $backendVersionTag 2>$null + $backendExists = $LASTEXITCODE -eq 0 + $ErrorActionPreference = "Stop" + if ($backendExists) { + Write-Error "Registry-tag '$backendVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force." + exit 1 + } + + Write-Host "Controleer registry: $frontendVersionTag" -ForegroundColor DarkGray + $ErrorActionPreference = "SilentlyContinue" + docker manifest inspect $frontendVersionTag 2>$null + $frontendExists = $LASTEXITCODE -eq 0 + $ErrorActionPreference = "Stop" + if ($frontendExists) { + Write-Error "Registry-tag '$frontendVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force." + exit 1 + } + + Write-Host "Geen conflicten gevonden." -ForegroundColor Green +} elseif ($Force) { + Write-Host "Conflictcontrole overgeslagen (-Force)" -ForegroundColor Yellow +} else { + Write-Host "[DRY-RUN] Conflictcontrole overgeslagen" -ForegroundColor Yellow +} + +# ── Stap 2: Bouwen en pushen ────────────────────────────────────────────── +if (-not $NoBuild) { + Write-Step "[2/3] Multi-platform bouwen en pushen" + + Write-Host "Backend bouwen en pushen..." -ForegroundColor White + Invoke-Cmd @( + "docker", "buildx", "build", + "--platform", "linux/amd64,linux/arm64", + "--push", + "-t", $backendVersionTag, + "-t", $backendLatestTag, + "--build-arg", "APP_VERSION=$appVersion", + (Join-Path $PSScriptRoot "backend") + ) + + Write-Host "Frontend bouwen en pushen..." -ForegroundColor White + Invoke-Cmd @( + "docker", "buildx", "build", + "--platform", "linux/amd64,linux/arm64", + "--push", + "-t", $frontendVersionTag, + "-t", $frontendLatestTag, + "--build-arg", "APP_VERSION=$appVersion", + "--build-arg", "VITE_API_URL=$apiUrl", + (Join-Path $PSScriptRoot "frontend") + ) +} else { + Write-Step "[2/3] Bouwen overgeslagen (-NoBuild)" +} + +# ── Stap 3: Git-tag aanmaken en pushen ──────────────────────────────────── +Write-Step "[3/3] Git-tag aanmaken en pushen" + +if ($Force) { + Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow + $ErrorActionPreference = "SilentlyContinue" + git tag -d $gitTag 2>$null | Out-Null + $ErrorActionPreference = "Stop" + Write-Host "Bestaande remote tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow + $ErrorActionPreference = "SilentlyContinue" + git push origin ":refs/tags/$gitTag" 2>$null | Out-Null + $ErrorActionPreference = "Stop" +} + +Write-Host "Git-tag aanmaken: $gitTag" -ForegroundColor White +Invoke-Cmd @("git", "tag", "-a", $gitTag, "-m", "Release $gitTag") + +Write-Host "Git-tag pushen naar origin" -ForegroundColor White +Invoke-Cmd @("git", "push", "origin", "refs/tags/$gitTag") + +# ── Klaar ───────────────────────────────────────────────────────────────── +Write-Host "" +Write-Host "════════════════════════════════════════" -ForegroundColor Cyan +Write-Host " Gepubliceerd: $appVersion" -ForegroundColor Green +Write-Host "" +Write-Host " Registry images:" -ForegroundColor Green +Write-Host " $backendVersionTag" -ForegroundColor White +Write-Host " $backendLatestTag" -ForegroundColor White +Write-Host " $frontendVersionTag" -ForegroundColor White +Write-Host " $frontendLatestTag" -ForegroundColor White +Write-Host "" +Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green +Write-Host "════════════════════════════════════════" -ForegroundColor Cyan +Write-Host "" diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..36474d2 --- /dev/null +++ b/release.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# ── VibeFinance release wrapper (macOS/Linux) ──────────────────────────── +# Roept release.ps1 aan via PowerShell Core (pwsh). +# +# Gebruik: +# ./release.sh # bouw + push images + Git-tag +# ./release.sh -NoBuild # sla docker build over, alleen Git-tag +# ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren +# ./release.sh -Force # sla conflictcontrole over, overschrijf bestaande tags +# +# Combinaties: +# ./release.sh -Force -NoBuild +# ./release.sh -DryRun -Force +# +# Vereisten: +# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell +# - Docker Desktop actief +# - docker buildx builder actief: docker buildx create --use +# - Ingelogd op de registry: docker login vibetea.vldn.net +# - Git geïnstalleerd +# - root package.json bevat versienummer ZONDER -dev suffix + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if ! command -v pwsh &>/dev/null; then + echo "Fout: pwsh niet gevonden. Installeer via: brew install powershell" >&2 + exit 1 +fi + +# Geef alle argumenten door aan het .ps1 script +pwsh -NoProfile -ExecutionPolicy Bypass \ + -File "$SCRIPT_DIR/release.ps1" \ + "$@"