commit 2b72f306ffde55572775f79214af22eb783e6ed0 Author: jhoogie Date: Thu Apr 16 10:22:13 2026 +0200 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6d14580 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# ============================================================================== +# VibeFinance — Configuratie +# Kopieer dit bestand naar .env en vul de verplichte waarden in: +# cp .env.example .env +# +# Commit .env NOOIT naar Git — het staat al in .gitignore +# ============================================================================== + +# ── Database ─────────────────────────────────────────────────────────────── +POSTGRES_USER=vibefinance +POSTGRES_PASSWORD=verander_dit_wachtwoord +# POSTGRES_DB=vibefinance # optioneel — standaard vibefinance + +# ── Backend ──────────────────────────────────────────────────────────────── +# Genereer een veilige waarde met: openssl rand -hex 64 +JWT_SECRET=verander_dit_naar_een_zeer_lange_willekeurige_string_minimaal_64_tekens +JWT_EXPIRES=7d + +# ── URLs ─────────────────────────────────────────────────────────────────── +CORS_ORIGIN=https://jouwdomein.nl + +# ── Poorten ──────────────────────────────────────────────────────────────── +FRONTEND_PORT=3300 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96c2b65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Environment — commit NOOIT secrets +.env +.env.local + +# Node +node_modules/ +npm-debug.log* + +# Vite build output +frontend/dist/ + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ + +# Claude +.claude/ diff --git a/BEHEER.md b/BEHEER.md new file mode 100644 index 0000000..7eeffb7 --- /dev/null +++ b/BEHEER.md @@ -0,0 +1,162 @@ +# 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 +``` + +--- + +## 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 + +``` +10.0.3.108:3000/vibe/vibefinance-backend +10.0.3.108: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 10.0.3.108:3000 +``` + +--- + +## Update-notificaties + +De app vergelijkt de draaiende versie met de laatste release op Gitea. Configuratie staat in `package.json`: + +```json +"repository": "https://10.0.3.108:3000/vibe/VibeFinance", +"updateCheckUrl": "https://10.0.3.108: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..a3bb6d1 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,52 @@ +# Changelog + +## [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..772b789 --- /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.0.3-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 binnen de container +│ ├── 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 +- 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/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..810d192 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.env* +*.log +.DS_Store 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..50bc5ff --- /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 = "10.0.3.108:3000/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..ec625ef --- /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:16-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/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U vibefinance"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - vibefinance_net + + backend: + image: 10.0.3.108:3000/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: 10.0.3.108:3000/vibe/vibefinance-frontend:latest + container_name: vibefinance_frontend + restart: unless-stopped + ports: + - "${FRONTEND_PORT:-3300}:80" + depends_on: + backend: + condition: service_healthy + networks: + - vibefinance_net + +volumes: + vibefinance_pgdata: + +networks: + vibefinance_net: + driver: bridge diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..12bb4bb --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.env* +*.log +.DS_Store 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..660e319 --- /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..3dcb6d7 --- /dev/null +++ b/frontend/nginx-spa.conf @@ -0,0 +1,22 @@ +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; + } + + # 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..52159a2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "vibefinance-frontend", + "version": "0.0.5-dev", + "updateCheckUrl": "https://10.0.3.108:3000/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/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..89201cf --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,21 @@ +* { + box-sizing: border-box; +} + +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..069ac30 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,186 @@ +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, PURPLE_LIGHT } from "./constants/index.js"; +import ProfielPopup from "./components/ProfielPopup.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, loading, T, darkMode, setDarkMode, currentUser, avatar } = useApp(); + const [profielOpen, setProfielOpen] = useState(false); + + const idleTimer = useRef(null); + const IDLE_MS = 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]); + + 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 rechts */} +
+
+ + {profielOpen && setProfielOpen(false)} />} +
+ +
+ + {/* Vergrendeld banner */} + {locked && ( +
+ 🔒 + + Vergrendeld — ga naar Gebruikers om te ontgrendelen + +
+ )} + + {/* Gebruikersbeheer modal */} + {showUsers && setShowUsers(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..c8fe6b6 --- /dev/null +++ b/frontend/src/components/GebruikersBeheer.jsx @@ -0,0 +1,355 @@ +import { useState, useEffect, useRef } from "react"; +import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js"; +import { Overlay, ModalBox, ModalHeader } from "./ui/index.jsx"; +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 ? PURPLE_LIGHT : T.text, + background: o === value ? `${PURPLE}22` : "transparent", + }} + onMouseEnter={(e) => e.currentTarget.style.background = `${PURPLE}18`} + onMouseLeave={(e) => e.currentTarget.style.background = o === value ? `${PURPLE}22` : "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 ( + + + + +
+ {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/LoginPage.jsx b/frontend/src/components/LoginPage.jsx new file mode 100644 index 0000000..21c045b --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,297 @@ +import { useState } from "react"; +import { PURPLE, PURPLE_LIGHT, RED } from "../constants/index.js"; + +export default function LoginPage({ onLogin }) { + const [mode, setMode] = useState("login"); + const [naam, setNaam] = useState(""); + const [email, setEmail] = useState(""); + const [wachtwoord, setWachtwoord] = useState(""); + const [showPass, setShowPass] = useState(false); + const [err, setErr] = useState(""); + const [loading, setLoading] = useState(false); + + const switchMode = (m) => { setMode(m); setNaam(""); setEmail(""); setWachtwoord(""); setErr(""); }; + + const handleSubmit = async () => { + setErr(""); + if (mode === "register" && !naam.trim()) { setErr("Vul je naam in."); 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 iStyle = { + width: "100%", + background: "rgba(255,255,255,0.06)", + border: "1px solid rgba(255,255,255,0.1)", + borderRadius: 8, color: "#f1f5f9", + padding: "11px 14px", fontSize: 13, + outline: "none", boxSizing: "border-box", + }; + + return ( +
+ {/* Achtergrond gloeden */} +
+
+ + {/* Links: formulier */} +
+ {/* Logo */} +
+
+ + + + +
+ VibeFinance +
+ + {/* Heading */} +
+

+ {mode === "login" ? "Welkom terug!" : "Account aanmaken"} +

+

+ {mode === "login" + ? "Voer je gegevens in om in te loggen op je dashboard." + : "Maak een account aan om je vermogen bij te houden."} +

+
+ + {/* Formulier */} +
+ {mode === "register" && ( +
+ { setNaam(e.target.value); setErr(""); }} + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + style={iStyle} /> +
+ )} +
+ { setEmail(e.target.value); setErr(""); }} + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + style={iStyle} /> +
+
+
+ { setWachtwoord(e.target.value); setErr(""); }} + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + style={{ ...iStyle, paddingRight: 42 }} /> + +
+
+
+ + {err && ( +
{err}
+ )} + + {/* Submit knop */} + + + + {/* Switch mode */} +
+ {mode === "login" ? ( + + Nog geen account?{" "} + + + ) : ( + + Al een account?{" "} + + + )} +
+
+ + {/* Rechts: app mockup — flat, groot, licht zwevend */} +
+
+ {/* Browser chrome */} +
+
+
+
+
vibefinance.app
+
+ + {/* App */} +
+ + {/* Sidebar */} +
+
+ +
+ {[ + { icon: , active: true }, + { icon: , active: false }, + { icon: , active: false }, + { icon: , active: false }, + ].map((item, i) => ( +
+ {item.icon} +
+ ))} +
+
+ +
+
+
+ +
+
+ + {/* Inhoud */} +
+ {/* Topbalk */} +
+
JH
+ +
🌙
+
+ + {/* Paginatitel */} +
+
The Road naar € 500.000
+
Persoonlijk vermogensdashboard
+
+ + {/* 6 KPI kaarten */} +
+ {[ + { label: "Start Eigen Vermogen", val: "€ 0", color: PURPLE }, + { label: "Extra Geïnv. Fiat", val: "€ 303.000", color: PURPLE }, + { label: "Totaal Geïnv. Fiat", val: "€ 303.000", color: PURPLE }, + { label: "Totaal Eigen Vermogen", val: "€ 445.500", color: "#10b981" }, + { label: "Winst op Investering", val: "€ 142.500", color: "#10b981" }, + { label: "ROI %", val: "+47,03%", color: "#10b981" }, + ].map((k) => ( +
+
{k.label}
+
{k.val}
+
+ ))} +
+ + {/* Progressie balk */} +
+
+
+ 🎯 + Doel & Bijbehorende Progressie + € 500.000 +
+ 89,10% behaald +
+
+
+ {[25, 50, 75].map(p =>
)} +
+
+ {["€ 0", "€ 125k", "€ 250k", "€ 375k", "€ 500k"].map(m => ( + {m} + ))} +
+
+ + {/* Grafiek */} +
+
Ontwikkeling van Eigen Vermogen
+ + + + + + + + {[0,1,2,3,4].map(i => )} + {["500k","375k","250k","125k","0"].map((l, i) => ( + {l} + ))} + + + {[[44,112],[114,102],[184,90],[254,74],[324,56],[394,38],[464,22],[534,10],[558,7]].map(([x,y],i) => ( + + ))} + {["jan","feb","mrt","apr","mei","jun","jul","aug","sep"].map((m, i) => ( + {m} + ))} + +
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx new file mode 100644 index 0000000..4e7c1cd --- /dev/null +++ b/frontend/src/components/NavBar.jsx @@ -0,0 +1,290 @@ +import { useState, useEffect } from "react"; +import { PURPLE, PURPLE_LIGHT, TABS } from "../constants/index.js"; +import { version, updateCheckUrl } from "../../package.json"; +import { useApp } from "../context/AppContext.jsx"; + +const TAB_ICONS = [ + + + + + + , + + + + , + + + + + , + + + + + , +]; + +const GEBRUIKERS_ICON = ( + + + + + + +); + +const DATA_ICON = ( + + + + +); + +const LOGOUT_ICON = ( + + + + + +); + +function Tooltip({ label, collapsed }) { + if (!collapsed) return null; + return ( +
{label}
+ ); +} + +function SidebarItem({ active, onClick, icon, label, T, collapsed }) { + const [hovered, setHovered] = useState(false); + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + title={collapsed ? label : undefined} + style={{ + position: "relative", + display: "flex", alignItems: "center", + gap: collapsed ? 0 : 10, + padding: "9px 12px", borderRadius: 8, cursor: "pointer", marginBottom: 6, + justifyContent: collapsed ? "center" : "flex-start", + background: active ? `${PURPLE}18` : "transparent", + color: active ? PURPLE_LIGHT : T.muted, + fontWeight: active ? 600 : 500, + fontSize: 13, + transition: "background 0.15s, color 0.15s", + }}> + {icon} + {!collapsed && {label}} + {hovered && } +
+ ); +} + +export default function NavBar() { + const { + tab, setTab, + setShowUsers, + T, + logout, + doBackup, doRestore, + } = useApp(); + + const [nieuweVersie, setNieuweVersie] = useState(null); + const [dataOpen, setDataOpen] = useState(false); + const [collapsed, setCollapsed] = useState(false); + + const W = collapsed ? 56 : 220; + + 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 + collapse knop */} + {!collapsed ? ( +
+
+
+ + + + +
+ VibeFinance +
+ +
+ ) : ( +
+ +
+ )} + + + {/* Navigatie */} + + + {/* Onderste sectie */} +
+ + {/* Update melding */} + {nieuweVersie && !collapsed && ( +
+
+ Nieuwe versie: v{nieuweVersie} +
+
Huidig: v{version}
+
+ )} + {nieuweVersie && collapsed && ( +
+
+
+ )} + + {/* Uitloggen */} + {LOGOUT_ICON}} + label="Log out" + T={T} + collapsed={collapsed} + /> +
+
+ ); +} diff --git a/frontend/src/components/ProfielPopup.jsx b/frontend/src/components/ProfielPopup.jsx new file mode 100644 index 0000000..284e4a2 --- /dev/null +++ b/frontend/src/components/ProfielPopup.jsx @@ -0,0 +1,233 @@ +import { useState } from "react"; +import { useApp } from "../context/AppContext.jsx"; +import { PURPLE, PURPLE_LIGHT, RED } from "../constants/index.js"; + +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(); +} + +export default function ProfielPopup({ onClose }) { + const { currentUser, updateProfile, changePassword, logout, T, avatar, setAvatar } = useApp(); + + const [activeTab, setActiveTab] = useState("profiel"); + + const [naam, setNaam] = useState(currentUser?.naam || ""); + const [email, setEmail] = useState(currentUser?.email || ""); + const [profielError, setProfielError] = useState(""); + const [profielSuccess, setProfielSuccess] = useState(false); + const [profielLoading, setProfielLoading] = useState(false); + + const [huidig, setHuidig] = useState(""); + const [nieuw, setNieuw] = useState(""); + const [bevestig, setBevestig] = useState(""); + const [pwError, setPwError] = useState(""); + const [pwSuccess, setPwSuccess] = useState(false); + const [pwLoading, setPwLoading] = useState(false); + + const pIs = { + background: T.inputBg, border: `1px solid ${T.inputBorder}`, + borderRadius: 10, color: T.text, padding: "10px 12px", + fontSize: 13, outline: "none", width: "100%", boxSizing: "border-box", + }; + + const handleSaveProfiel = async () => { + setProfielError(""); setProfielSuccess(false); + if (!naam.trim() || !email.trim()) { setProfielError("Naam en e-mail zijn verplicht."); return; } + setProfielLoading(true); + try { + await updateProfile(naam.trim(), email.trim()); + setProfielSuccess(true); + } catch (err) { + setProfielError(err.message || "Opslaan mislukt."); + } finally { + setProfielLoading(false); + } + }; + + const handleChangePassword = async () => { + setPwError(""); setPwSuccess(false); + if (!huidig || !nieuw || !bevestig) { setPwError("Vul alle velden in."); return; } + if (nieuw !== bevestig) { setPwError("Nieuwe wachtwoorden komen niet overeen."); return; } + if (nieuw.length < 8) { setPwError("Nieuw wachtwoord moet minimaal 8 tekens zijn."); return; } + setPwLoading(true); + try { + await changePassword(huidig, nieuw); + setPwSuccess(true); + setHuidig(""); setNieuw(""); setBevestig(""); + } catch (err) { + setPwError(err.message || "Wachtwoord wijzigen mislukt."); + } finally { + setPwLoading(false); + } + }; + + return ( + <> + {/* Backdrop */} +
+ + {/* Popup */} +
+ {/* Header */} +
+
+
+ {avatar + ? avatar + : initials(currentUser?.naam) + } +
+ +
+
+
+ {currentUser?.naam} +
+
+ {currentUser?.email} +
+
+ +
+ + {/* Tabs */} +
+ {[ + { id: "profiel", label: "👤 Profiel" }, + { id: "wachtwoord", label: "🔑 Wachtwoord" }, + ].map(({ id, label }) => ( + + ))} +
+ + {/* Profiel tab */} + {activeTab === "profiel" && ( +
+ {/* Avatar upload */} +
+
+ {avatar + ? avatar + : initials(currentUser?.naam) + } +
+
+ + {avatar && ( + + )} +
+
+
+ + { setNaam(e.target.value); setProfielSuccess(false); }} style={pIs} /> +
+
+ + { setEmail(e.target.value); setProfielSuccess(false); }} style={pIs} /> +
+ {profielError &&
{profielError}
} + {profielSuccess &&
Profiel opgeslagen.
} + + +
+ )} + + {/* Wachtwoord tab */} + {activeTab === "wachtwoord" && ( +
+ {[ + { label: "Huidig wachtwoord", value: huidig, set: setHuidig }, + { label: "Nieuw wachtwoord", value: nieuw, set: setNieuw }, + { label: "Bevestig nieuw", value: bevestig, set: setBevestig }, + ].map(({ label, value, set }) => ( +
+ + { set(e.target.value); setPwSuccess(false); }} style={pIs} /> +
+ ))} + {pwError &&
{pwError}
} + {pwSuccess &&
Wachtwoord gewijzigd.
} + +
+ )} +
+ + ); +} diff --git a/frontend/src/components/ui/index.jsx b/frontend/src/components/ui/index.jsx new file mode 100644 index 0000000..b39b85e --- /dev/null +++ b/frontend/src/components/ui/index.jsx @@ -0,0 +1,458 @@ +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 }) { + const r = size * 0.38, cx = size / 2, cy = size / 2; + const circ = 2 * Math.PI * r; + let off = 0; + const slices = segments.filter((s) => s.val > 0).map((s) => { + const dash = (s.val / total) * circ; + const sl = { ...s, dash, gap: circ - dash, offset: off }; + off += dash; + return sl; + }); + return ( + + + {slices.map((s, i) => { + const mid = (s.offset + s.dash / 2) / r - Math.PI / 2; + const lx = cx + r * Math.cos(mid); + const ly = cy + r * Math.sin(mid); + return ( + + + {s.dash > circ * 0.06 && ( + + {(s.val / total * 100).toFixed(0)}% + + )} + + ); + })} + + ); +} + +// ── 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..2ad89e8 --- /dev/null +++ b/frontend/src/context/AppContext.jsx @@ -0,0 +1,222 @@ +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); + + // ── 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..f887833 --- /dev/null +++ b/frontend/src/hooks/useTheme.js @@ -0,0 +1,16 @@ +/** + * 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 { + bg: darkMode ? "#0f1117" : "#f1f5f9", + card: darkMode ? "#1a1d27" : "#ffffff", + border: darkMode ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.08)", + muted: darkMode ? "#6b7280" : "#94a3b8", + text: darkMode ? "#f1f5f9" : "#0f172a", + subtext: darkMode ? "#94a3b8" : "#64748b", + inputBg: darkMode ? "rgba(255,255,255,0.05)" : "#f8fafc", + inputBorder: darkMode ? "rgba(255,255,255,0.1)" : "#e2e8f0", + }; +} 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..3e49050 --- /dev/null +++ b/frontend/src/pages/DashboardTab.jsx @@ -0,0 +1,209 @@ +import { useApp } from "../context/AppContext.jsx"; +import { fmt, pct, fmtDatum } from "../utils/format.js"; +import { PURPLE, PURPLE_LIGHT, GREEN, RED } from "../constants/index.js"; +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: PURPLE }, + { label: "Extra geïnvesteerde fiat", val: fmt(extraFiat), accent: PURPLE }, + { label: "Totaal geïnvesteerde fiat", val: fmt(totaalFiat), accent: PURPLE }, + { 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 ( +
+
+
+ {dagdeel}{voornaam ? `, ${voornaam}` : ""} 👋 +
+

+ The Road naar {fmt(doel)} +

+
+ Persoonlijk vermogensdashboard +
+
+ + {/* 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. +
+
+ ) : ( + + )} +
+ +
+ ); +} + +// ── Mini inline SVG grafiek ──────────────────────────────────────────────────── +function MiniLineChart({ vData, darkMode }) { + const [hovered, setHovered] = useState(null); + const W = 700, H = 230, pl = 58, pr = 16, pt = 12, pb = 62; + 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 line = xs.map((x, i) => `${x},${ys[i]}`).join(" "); + const area = `${xs[0]},${pt + iH} ${line} ${xs[xs.length - 1]},${pt + iH}`; + const gc = darkMode ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)"; + const lc = darkMode ? "#6b7280" : "#94a3b8"; + + return ( + + + + + + + + {[0, 0.25, 0.5, 0.75, 1].map((f) => { + const gy = pt + (1 - f) * iH; + return ( + + + + {fmt(mn + f * rng)} + + + ); + })} + + + {xs.map((x, i) => { + const isHov = hovered === i; + const tipW = 90, tipH = 22; + const tipX = Math.min(Math.max(x - tipW / 2, pl), W - tipW - 4); + const tipY = ys[i] - tipH - 8 < pt ? ys[i] + 10 : ys[i] - tipH - 8; + return ( + setHovered(i)} + onMouseLeave={() => setHovered(null)} + style={{ cursor: "pointer" }} + > + {/* Vergroot klikgebied */} + + + + {fmtDatum(vData[i].datum)} + + {/* Tooltip */} + {isHov && ( + + + + {fmt(vals[i])} + + + )} + + ); + })} + + ); +} diff --git a/frontend/src/pages/EigenVermogenTab.jsx b/frontend/src/pages/EigenVermogenTab.jsx new file mode 100644 index 0000000..4b1200f --- /dev/null +++ b/frontend/src/pages/EigenVermogenTab.jsx @@ -0,0 +1,656 @@ +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 && ( +
+
+ Donut verdeling +
+ ({ val: c.tot, color: c.color }))} total={totVerdeling} size={160} /> +
+ {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..58ea387 --- /dev/null +++ b/frontend/src/pages/SchuldTab.jsx @@ -0,0 +1,455 @@ +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 */} +
+
+ Verdeling Schuld +
+ ({ val: b.subtot, color: b.color }))} + total={donutTotal} size={160} + /> +
+ {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..2724177 --- /dev/null +++ b/frontend/src/pages/VoortgangTab.jsx @@ -0,0 +1,439 @@ +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 */} + +
Verdeling Schuld & EV
+ +
+
+
+ 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/vibefinance-logo-wordmark.svg b/frontend/vibefinance-logo-wordmark.svg new file mode 100644 index 0000000..ea54e02 --- /dev/null +++ b/frontend/vibefinance-logo-wordmark.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + VibeFinance + + diff --git a/frontend/vibefinance-logo.svg b/frontend/vibefinance-logo.svg new file mode 100644 index 0000000..eb60312 --- /dev/null +++ b/frontend/vibefinance-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + 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..0371837 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "vibefinance", + "version": "0.1.0", + "description": "VibeFinance — persoonlijk vermogensbeheer", + "private": true, + "repository": "https://10.0.3.108:3000/vibe/VibeFinance", + "updateCheckUrl": "https://10.0.3.108:3000/api/v1/repos/vibe/VibeFinance/releases/latest" +} diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..04e3e34 --- /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 = "10.0.3.108:3000/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" \ + "$@"