From a8d4c158b8a17f1536549952e88835eae1188cea Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 18 May 2026 15:25:38 +0200 Subject: [PATCH] Commit iniziale --- .env.example | 12 + .gitignore | 37 + Caddyfile | 3 + README.md | 334 +++ app/Dockerfile | 36 + app/entrypoint.sh | 16 + app/next-env.d.ts | 5 + app/next.config.js | 5 + app/package-lock.json | 2013 +++++++++++++++++ app/package.json | 31 + app/postcss.config.js | 6 + .../migrations/0001_initial/migration.sql | 315 +++ app/prisma/migrations/migration_lock.toml | 3 + app/prisma/schema.prisma | 257 +++ app/scripts/bootstrap-admin.js | 46 + app/scripts/bootstrap-admin.ts | 46 + app/src/app/account/orders/page.tsx | 125 + app/src/app/account/page.tsx | 80 + app/src/app/admin/admin-users/page.tsx | 192 ++ app/src/app/admin/categories/page.tsx | 188 ++ app/src/app/admin/change-password/page.tsx | 107 + app/src/app/admin/customers/page.tsx | 68 + app/src/app/admin/layout.tsx | 44 + app/src/app/admin/orders/[id]/page.tsx | 223 ++ app/src/app/admin/orders/page.tsx | 126 ++ app/src/app/admin/page.tsx | 158 ++ app/src/app/admin/product-types/page.tsx | 193 ++ app/src/app/admin/products/[id]/page.tsx | 306 +++ app/src/app/admin/products/page.tsx | 161 ++ app/src/app/admin/reviews/page.tsx | 137 ++ app/src/app/admin/settings/page.tsx | 101 + app/src/app/api/admin/categories/route.ts | 78 + app/src/app/api/admin/dashboard/route.ts | 47 + app/src/app/api/admin/orders/[id]/route.ts | 69 + app/src/app/api/admin/orders/route.ts | 40 + app/src/app/api/admin/product-types/route.ts | 79 + app/src/app/api/admin/products/[id]/route.ts | 107 + app/src/app/api/admin/products/route.ts | 97 + app/src/app/api/admin/reviews/route.ts | 65 + app/src/app/api/admin/settings/route.ts | 50 + app/src/app/api/admin/users/route.ts | 90 + app/src/app/api/auth/change-password/route.ts | 42 + app/src/app/api/auth/login/route.ts | 81 + app/src/app/api/auth/logout/route.ts | 13 + app/src/app/api/auth/register/route.ts | 54 + app/src/app/api/cart/route.ts | 69 + app/src/app/api/checkout/route.ts | 125 + app/src/app/api/orders/route.ts | 21 + app/src/app/api/products/[slug]/route.ts | 31 + app/src/app/api/products/route.ts | 56 + app/src/app/api/reviews/route.ts | 61 + app/src/app/api/webhooks/stripe/route.ts | 124 + app/src/app/cart/page.tsx | 174 ++ app/src/app/checkout/page.tsx | 135 ++ app/src/app/globals.css | 3 + app/src/app/layout.tsx | 21 + app/src/app/login/page.tsx | 108 + app/src/app/page.tsx | 98 + app/src/app/products/[slug]/page.tsx | 306 +++ app/src/app/products/page.tsx | 154 ++ app/src/app/register/page.tsx | 105 + app/src/components/admin/AdminNav.tsx | 69 + app/src/components/storefront/Navbar.tsx | 90 + app/src/components/storefront/ProductCard.tsx | 45 + app/src/components/ui/Alert.tsx | 20 + app/src/components/ui/Badge.tsx | 22 + app/src/components/ui/Button.tsx | 50 + app/src/components/ui/Card.tsx | 28 + app/src/components/ui/Input.tsx | 84 + app/src/lib/auth.ts | 103 + app/src/lib/email.ts | 70 + app/src/lib/prisma.ts | 13 + app/src/lib/stripe.ts | 39 + app/src/lib/validate.ts | 115 + app/src/middleware.ts | 12 + app/tailwind.config.js | 12 + app/tsconfig.json | 27 + app/tsconfig.tsbuildinfo | 1 + docker-compose.yml | 53 + 79 files changed, 8730 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Caddyfile create mode 100644 README.md create mode 100644 app/Dockerfile create mode 100644 app/entrypoint.sh create mode 100644 app/next-env.d.ts create mode 100644 app/next.config.js create mode 100644 app/package-lock.json create mode 100644 app/package.json create mode 100644 app/postcss.config.js create mode 100644 app/prisma/migrations/0001_initial/migration.sql create mode 100644 app/prisma/migrations/migration_lock.toml create mode 100644 app/prisma/schema.prisma create mode 100644 app/scripts/bootstrap-admin.js create mode 100644 app/scripts/bootstrap-admin.ts create mode 100644 app/src/app/account/orders/page.tsx create mode 100644 app/src/app/account/page.tsx create mode 100644 app/src/app/admin/admin-users/page.tsx create mode 100644 app/src/app/admin/categories/page.tsx create mode 100644 app/src/app/admin/change-password/page.tsx create mode 100644 app/src/app/admin/customers/page.tsx create mode 100644 app/src/app/admin/layout.tsx create mode 100644 app/src/app/admin/orders/[id]/page.tsx create mode 100644 app/src/app/admin/orders/page.tsx create mode 100644 app/src/app/admin/page.tsx create mode 100644 app/src/app/admin/product-types/page.tsx create mode 100644 app/src/app/admin/products/[id]/page.tsx create mode 100644 app/src/app/admin/products/page.tsx create mode 100644 app/src/app/admin/reviews/page.tsx create mode 100644 app/src/app/admin/settings/page.tsx create mode 100644 app/src/app/api/admin/categories/route.ts create mode 100644 app/src/app/api/admin/dashboard/route.ts create mode 100644 app/src/app/api/admin/orders/[id]/route.ts create mode 100644 app/src/app/api/admin/orders/route.ts create mode 100644 app/src/app/api/admin/product-types/route.ts create mode 100644 app/src/app/api/admin/products/[id]/route.ts create mode 100644 app/src/app/api/admin/products/route.ts create mode 100644 app/src/app/api/admin/reviews/route.ts create mode 100644 app/src/app/api/admin/settings/route.ts create mode 100644 app/src/app/api/admin/users/route.ts create mode 100644 app/src/app/api/auth/change-password/route.ts create mode 100644 app/src/app/api/auth/login/route.ts create mode 100644 app/src/app/api/auth/logout/route.ts create mode 100644 app/src/app/api/auth/register/route.ts create mode 100644 app/src/app/api/cart/route.ts create mode 100644 app/src/app/api/checkout/route.ts create mode 100644 app/src/app/api/orders/route.ts create mode 100644 app/src/app/api/products/[slug]/route.ts create mode 100644 app/src/app/api/products/route.ts create mode 100644 app/src/app/api/reviews/route.ts create mode 100644 app/src/app/api/webhooks/stripe/route.ts create mode 100644 app/src/app/cart/page.tsx create mode 100644 app/src/app/checkout/page.tsx create mode 100644 app/src/app/globals.css create mode 100644 app/src/app/layout.tsx create mode 100644 app/src/app/login/page.tsx create mode 100644 app/src/app/page.tsx create mode 100644 app/src/app/products/[slug]/page.tsx create mode 100644 app/src/app/products/page.tsx create mode 100644 app/src/app/register/page.tsx create mode 100644 app/src/components/admin/AdminNav.tsx create mode 100644 app/src/components/storefront/Navbar.tsx create mode 100644 app/src/components/storefront/ProductCard.tsx create mode 100644 app/src/components/ui/Alert.tsx create mode 100644 app/src/components/ui/Badge.tsx create mode 100644 app/src/components/ui/Button.tsx create mode 100644 app/src/components/ui/Card.tsx create mode 100644 app/src/components/ui/Input.tsx create mode 100644 app/src/lib/auth.ts create mode 100644 app/src/lib/email.ts create mode 100644 app/src/lib/prisma.ts create mode 100644 app/src/lib/stripe.ts create mode 100644 app/src/lib/validate.ts create mode 100644 app/src/middleware.ts create mode 100644 app/tailwind.config.js create mode 100644 app/tsconfig.json create mode 100644 app/tsconfig.tsbuildinfo create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bc49678 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +APP_URL=http://localhost +DATABASE_URL=postgresql://ecommerce:ecommerce_password@db:5432/ecommerce +AUTH_SECRET=dev-secret-change-in-production-32chars +INITIAL_ADMIN_EMAIL=admin@example.com +INITIAL_ADMIN_PASSWORD=Admin1234!test +STRIPE_SECRET_KEY=sk_test_placeholder +STRIPE_WEBHOOK_SECRET=whsec_placeholder +SMTP_HOST=mailpit +SMTP_PORT=1025 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM=noreply@localhost diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5049cf6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Environment +.env +.env.local +.env.*.local + +# Node +app/node_modules/ +node_modules/ + +# Next.js build output +app/.next/ +app/out/ + +# Prisma generated client (rebuilt on npm install) +app/node_modules/.prisma/ + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/settings.json +.idea/ +*.swp +*.swo + +# Docker volumes (dati locali) +pgdata/ +caddy_data/ +caddy_config/ + +# Logs +*.log +npm-debug.log* + +# Uploads / media locali +app/public/uploads/ diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..d3c9a1c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,3 @@ +localhost { + reverse_proxy app:3000 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a00e3d --- /dev/null +++ b/README.md @@ -0,0 +1,334 @@ +# E-commerce Platform + +Piattaforma e-commerce containerizzata, avviabile con un singolo comando. + +**Stack:** Next.js 14 · PostgreSQL 16 · Prisma · Stripe · Caddy · Docker Compose + +--- + +## Avvio in locale (test su localhost) + +### Prerequisiti + +- [Docker Desktop](https://docs.docker.com/get-docker/) installato e avviato +- Porta **80** libera (nessun altro web server in esecuzione) + +### 1. Clona il repository + +```bash +git clone +cd ecommerce-platform +``` + +### 2. Crea il file `.env` + +```bash +cp .env.example .env +``` + +Il file `.env` di default è già configurato per localhost. **Non serve modificare nulla** per i primi test. + +### 3. Avvia la piattaforma + +```bash +docker compose up -d +``` + +Il primo avvio richiede **5–10 minuti**: Docker scarica le immagini, installa le dipendenze npm, compila Next.js, esegue le migrazioni e crea l'utente admin. + +Segui il progresso con: + +```bash +docker compose logs -f app +``` + +La piattaforma è pronta quando vedi: + +``` +✓ Ready in 91ms +``` + +### 4. Apri nel browser + +| URL | Cosa trovi | +|-----|------------| +| http://localhost | Sito pubblico (vetrina) | +| http://localhost/admin | Dashboard amministratore | +| http://localhost:8025 | Mailpit — cattura le email di test | + +### 5. Primo accesso admin + +Vai su **http://localhost/admin** e accedi con: + +``` +Email: admin@example.com +Password: Admin1234!test +``` + +Al primo accesso il sistema ti obbliga a cambiare la password. + +**Requisiti password:** minimo 12 caratteri, almeno una maiuscola, una minuscola, un numero e un simbolo. + +### 6. Configura il negozio (ordine consigliato) + +1. **Product Types** → crea un tipo di prodotto (es. "Prodotto") con gli attributi che vuoi +2. **Categories** → crea le categorie +3. **Products** → crea un prodotto, assegna tipo e categoria, impostalo su **Published** +4. Apri http://localhost → il prodotto appare in homepage + +### 7. Cosa funziona in locale senza configurazione extra + +| Funzionalità | Stato | +|---|---| +| Admin dashboard completa | ✅ | +| Gestione prodotti, categorie, ordini | ✅ | +| Registrazione e login clienti | ✅ | +| Email (reset password, ecc.) | ✅ visibili su http://localhost:8025 | +| Pagamenti Stripe | ⚠️ richiede chiavi reali (vedi sotto) | + +**Per testare i pagamenti Stripe** in locale, inserisci una chiave `sk_test_...` reale nel `.env` (gratuita, modalità test su [dashboard.stripe.com](https://dashboard.stripe.com/test/apikeys)): + +```env +STRIPE_SECRET_KEY=sk_test_la_tua_chiave +``` + +Poi riavvia l'app: + +```bash +docker compose restart app +``` + +### 8. Ferma la piattaforma + +```bash +docker compose down # ferma i container, i dati restano +docker compose down -v # ferma e cancella anche il database +``` + +--- + +## Deploy in produzione + +### Prerequisiti + +- Server Linux con Docker e Docker Compose v2 (qualsiasi VPS o cloud VM) +- Dominio con record **A** puntato all'IP del server +- Porte **80** e **443** aperte nel firewall del server +- Account [Stripe](https://stripe.com) (per i pagamenti reali) +- Account SMTP (per le email: Mailgun, Resend, SendGrid, ecc.) + +### 1. Clona il repository sul server + +```bash +git clone +cd ecommerce-platform +``` + +### 2. Crea e configura `.env` + +```bash +cp .env.example .env +nano .env +``` + +Modifica questi valori: + +```env +# URL pubblico del sito — con https:// +APP_URL=https://tuodominio.com + +# Genera un segreto casuale: openssl rand -hex 32 +AUTH_SECRET=incolla_qui_il_risultato_di_openssl + +# Credenziali del primo admin — cambia subito dopo il primo accesso +INITIAL_ADMIN_EMAIL=admin@tuodominio.com +INITIAL_ADMIN_PASSWORD=UnaPasswordSicura1! + +# Stripe — usa le chiavi live (non sk_test_) +STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx +STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx + +# SMTP reale per le email +SMTP_HOST=smtp.tuoprovider.com +SMTP_PORT=587 +SMTP_USER=user@tuodominio.com +SMTP_PASSWORD=la_tua_password_smtp +SMTP_FROM=noreply@tuodominio.com +``` + +> `DATABASE_URL` non va modificato: usa già il nome del container Docker corretto. + +### 3. Genera `AUTH_SECRET` + +```bash +openssl rand -hex 32 +``` + +Copia l'output nel campo `AUTH_SECRET` nel file `.env`. + +### 4. Configura il dominio nel Caddyfile + +Apri `Caddyfile` e sostituisci `localhost` con il tuo dominio: + +``` +tuodominio.com { + encode gzip zstd + reverse_proxy app:3000 +} +``` + +Caddy ottiene e rinnova automaticamente il certificato HTTPS tramite Let's Encrypt. Non serve nessuna configurazione SSL manuale. + +### 5. Avvia + +```bash +docker compose up -d +``` + +Attendi che l'app sia pronta: + +```bash +docker compose logs -f app +# attendi: ✓ Ready in ...ms +``` + +Il sito è live su `https://tuodominio.com`. + +### 6. Configura il webhook Stripe + +Nel [dashboard Stripe → Webhooks](https://dashboard.stripe.com/webhooks), aggiungi un endpoint: + +``` +URL endpoint: https://tuodominio.com/api/webhooks/stripe + +Eventi da ascoltare: + - checkout.session.completed + - payment_intent.succeeded + - payment_intent.payment_failed +``` + +Copia il **Signing secret** (`whsec_...`) nel `.env` come `STRIPE_WEBHOOK_SECRET`, poi: + +```bash +docker compose restart app +``` + +### 7. Primo accesso e configurazione negozio + +1. Vai su `https://tuodominio.com/admin` +2. Accedi con le credenziali del `.env` +3. Cambia la password quando richiesto +4. Configura il negozio in questo ordine: + - **Settings** → nome negozio, logo, colori + - **Product Types** → tipi di prodotto con attributi personalizzati + - **Categories** → categorie prodotto + - **Products** → aggiungi i tuoi prodotti + - **Admin Users** → aggiungi altri amministratori se necessario + +--- + +## Aggiornamenti + +```bash +git pull +docker compose build app +docker compose up -d +``` + +Le migrazioni del database vengono applicate automaticamente all'avvio. + +--- + +## Backup database + +**Dump manuale:** + +```bash +docker compose exec db pg_dump -U ecommerce ecommerce > backup_$(date +%Y%m%d_%H%M%S).sql +``` + +**Ripristino:** + +```bash +cat backup_XXXXXXXX_XXXXXX.sql | docker compose exec -T db psql -U ecommerce ecommerce +``` + +**Backup automatico giornaliero** (aggiungi al crontab del server con `crontab -e`): + +```bash +0 3 * * * cd /percorso/ecommerce-platform && docker compose exec -T db pg_dump -U ecommerce ecommerce | gzip > /backups/ecommerce_$(date +\%Y\%m\%d).sql.gz +``` + +--- + +## Problemi comuni + +**Il build impiega molto tempo al primo avvio** +È normale. npm install + compilazione Next.js richiedono 5–10 minuti la prima volta. I build successivi sono molto più veloci grazie alla cache Docker. + +**http://localhost non risponde (502 Bad Gateway)** +L'app è ancora in fase di avvio. Aspetta qualche secondo e ricarica. Controlla lo stato con: +```bash +docker compose logs -f app +``` + +**Errore "port 80 already in use"** +Un altro servizio usa la porta 80 (Apache, Nginx, ecc.). Fermalo o cambia la porta nel `docker-compose.yml`. + +**Dimentico la password admin** +Resetta direttamente nel database: +```bash +# genera hash bcrypt per la nuova password +docker compose exec app node -e " +const bcrypt = require('bcryptjs'); +bcrypt.hash('NuovaPassword1!', 12).then(h => console.log(h)); +" +# aggiorna nel db +docker compose exec db psql -U ecommerce ecommerce -c \ + "UPDATE \"User\" SET \"passwordHash\"='HASH_QUI', \"mustChangePassword\"=true WHERE email='admin@example.com';" +``` + +--- + +## Struttura del progetto + +``` +ecommerce-platform/ +├── docker-compose.yml Orchestrazione: db, app, caddy, mailpit +├── Caddyfile Reverse proxy — modifica qui il dominio +├── .env Variabili d'ambiente (non committare) +├── .env.example Template da copiare +└── app/ + ├── Dockerfile Build multi-stage Next.js + ├── entrypoint.sh Sequenza avvio: migrazioni → admin → server + ├── prisma/ + │ └── schema.prisma Schema database completo + ├── scripts/ + │ └── bootstrap-admin.ts Crea il primo OWNER se non esiste + └── src/ + ├── app/ Pagine Next.js (App Router) + │ ├── admin/ Dashboard admin (protetta per ruolo) + │ ├── api/ API routes (auth, prodotti, ordini, webhook) + │ └── (storefront) Homepage, catalogo, carrello, checkout + ├── components/ Componenti React riutilizzabili + └── lib/ Auth, Prisma client, Stripe, email +``` + +--- + +## Variabili d'ambiente + +| Variabile | Descrizione | Locale | Produzione | +|-----------|-------------|:------:|:----------:| +| `APP_URL` | URL pubblico del sito | `http://localhost` | `https://tuodominio.com` | +| `DATABASE_URL` | Connessione PostgreSQL | invariato | invariato | +| `AUTH_SECRET` | Segreto sessioni (min 32 char) | qualsiasi | `openssl rand -hex 32` | +| `INITIAL_ADMIN_EMAIL` | Email primo admin | qualsiasi | la tua email | +| `INITIAL_ADMIN_PASSWORD` | Password primo admin | qualsiasi | sicura | +| `STRIPE_SECRET_KEY` | Chiave Stripe | `sk_test_...` | `sk_live_...` | +| `STRIPE_WEBHOOK_SECRET` | Segreto webhook Stripe | opzionale | obbligatorio | +| `SMTP_HOST` | Server SMTP | `mailpit` | provider reale | +| `SMTP_PORT` | Porta SMTP | `1025` | `587` | +| `SMTP_USER` | Utente SMTP | vuoto | obbligatorio | +| `SMTP_PASSWORD` | Password SMTP | vuoto | obbligatorio | +| `SMTP_FROM` | Mittente email | qualsiasi | `noreply@tuodominio.com` | diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..fc949a0 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,36 @@ +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +FROM node:20-alpine AS builder +WORKDIR /app +RUN apk add --no-cache openssl +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +ENV DATABASE_URL=postgresql://placeholder:placeholder@localhost:5432/placeholder +RUN npx prisma generate +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder /app/node_modules/prisma ./node_modules/prisma +COPY --from=builder /app/node_modules/bcryptjs ./node_modules/bcryptjs +COPY --from=builder /app/node_modules/@prisma/client ./node_modules/@prisma/client +COPY --from=builder /app/scripts ./scripts +COPY entrypoint.sh ./ +RUN chmod +x entrypoint.sh +RUN apk add --no-cache postgresql-client openssl +USER nextjs +EXPOSE 3000 +ENTRYPOINT ["./entrypoint.sh"] diff --git a/app/entrypoint.sh b/app/entrypoint.sh new file mode 100644 index 0000000..8dbfdeb --- /dev/null +++ b/app/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +echo "Waiting for database..." +until pg_isready -h db -p 5432 -U ecommerce; do + sleep 2 +done + +echo "Running migrations..." +node node_modules/prisma/build/index.js migrate deploy + +echo "Bootstrapping admin..." +node scripts/bootstrap-admin.js + +echo "Starting server..." +exec node server.js diff --git a/app/next-env.d.ts b/app/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/app/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/app/next.config.js b/app/next.config.js new file mode 100644 index 0000000..a8d3c99 --- /dev/null +++ b/app/next.config.js @@ -0,0 +1,5 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', +} +module.exports = nextConfig diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..752d938 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,2013 @@ +{ + "name": "ecommerce-platform", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ecommerce-platform", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "@prisma/client": "^5.16.0", + "bcryptjs": "^2.4.3", + "next": "14.2.5", + "nodemailer": "^6.9.14", + "stripe": "^16.0.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/node": "^20", + "@types/nodemailer": "^6.4.15", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "postcss": "^8", + "prisma": "^5.16.0", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@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/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/@next/env": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.23", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.23.tgz", + "integrity": "sha512-aFV3/NsYFLSx9mbb5gtirBSXJnAlrusoKNuPbxsASWc7vrKLmIrTQRpdcxNcSFL3VW2A2XpeLEavwb2qMi6nlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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==", + "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==", + "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/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "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==", + "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.357", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", + "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "dev": true, + "license": "ISC" + }, + "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==", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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", + "peer": true + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "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", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.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/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/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/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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==", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "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==", + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/stripe": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-16.12.0.tgz", + "integrity": "sha512-H7eFVLDxeTNNSn4JTRfL2//LzCbDrMSZ+2q1c7CanVWgK2qIW5TwS+0V7N9KcKZZNpYh/uCqK0PyZh/2UsaAtQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..158cca0 --- /dev/null +++ b/app/package.json @@ -0,0 +1,31 @@ +{ + "name": "ecommerce-platform", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "postinstall": "prisma generate" + }, + "dependencies": { + "next": "14.2.5", + "@prisma/client": "^5.16.0", + "bcryptjs": "^2.4.3", + "stripe": "^16.0.0", + "nodemailer": "^6.9.14", + "zod": "^3.23.8" + }, + "devDependencies": { + "prisma": "^5.16.0", + "@types/bcryptjs": "^2.4.6", + "@types/nodemailer": "^6.4.15", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5", + "tailwindcss": "^3.4.1", + "postcss": "^8", + "autoprefixer": "^10.0.1" + } +} diff --git a/app/postcss.config.js b/app/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/app/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/app/prisma/migrations/0001_initial/migration.sql b/app/prisma/migrations/0001_initial/migration.sql new file mode 100644 index 0000000..bf5190f --- /dev/null +++ b/app/prisma/migrations/0001_initial/migration.sql @@ -0,0 +1,315 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('CUSTOMER', 'ADMIN', 'OWNER'); + +-- CreateEnum +CREATE TYPE "ProductStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); + +-- CreateEnum +CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'PAID', 'CANCELLED', 'REFUNDED', 'FULFILLED'); + +-- CreateEnum +CREATE TYPE "ReviewStatus" AS ENUM ('PENDING', 'APPROVED', 'HIDDEN'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'CUSTOMER', + "name" TEXT, + "mustChangePassword" BOOLEAN NOT NULL DEFAULT false, + "emailVerifiedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "tokenHash" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "tokenHash" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductType" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "schema" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProductType_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "typeId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT NOT NULL, + "basePrice" INTEGER NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'EUR', + "status" "ProductStatus" NOT NULL DEFAULT 'DRAFT', + "attributes" JSONB NOT NULL, + "stock" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductVariant" ( + "id" TEXT NOT NULL, + "productId" TEXT NOT NULL, + "sku" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "stock" INTEGER NOT NULL, + "attributes" JSONB NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProductVariant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Category" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "parentId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductCategory" ( + "productId" TEXT NOT NULL, + "categoryId" TEXT NOT NULL, + + CONSTRAINT "ProductCategory_pkey" PRIMARY KEY ("productId","categoryId") +); + +-- CreateTable +CREATE TABLE "MediaAsset" ( + "id" TEXT NOT NULL, + "productId" TEXT, + "url" TEXT NOT NULL, + "altText" TEXT, + "mimeType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MediaAsset_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Order" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "status" "OrderStatus" NOT NULL DEFAULT 'PENDING', + "currency" TEXT NOT NULL DEFAULT 'EUR', + "subtotal" INTEGER NOT NULL, + "taxTotal" INTEGER NOT NULL DEFAULT 0, + "shippingTotal" INTEGER NOT NULL DEFAULT 0, + "grandTotal" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Order_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OrderItem" ( + "id" TEXT NOT NULL, + "orderId" TEXT NOT NULL, + "productId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "quantity" INTEGER NOT NULL, + "unitPrice" INTEGER NOT NULL, + "totalPrice" INTEGER NOT NULL, + "metadata" JSONB, + + CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Payment" ( + "id" TEXT NOT NULL, + "orderId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerPaymentId" TEXT, + "status" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "currency" TEXT NOT NULL, + "rawPayload" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Payment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Review" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "productId" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "title" TEXT, + "comment" TEXT, + "status" "ReviewStatus" NOT NULL DEFAULT 'PENDING', + "verified" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SiteSettings" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" JSONB NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SiteSettings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Page" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "title" TEXT NOT NULL, + "seoTitle" TEXT, + "seoDesc" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Page_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PageSection" ( + "id" TEXT NOT NULL, + "pageId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL, + "data" JSONB NOT NULL, + "visible" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PageSection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" TEXT NOT NULL, + "userId" TEXT, + "action" TEXT NOT NULL, + "entity" TEXT NOT NULL, + "entityId" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_tokenHash_key" ON "Session"("tokenHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordResetToken_tokenHash_key" ON "PasswordResetToken"("tokenHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductType_slug_key" ON "ProductType"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Product_slug_key" ON "Product"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductVariant_sku_key" ON "ProductVariant"("sku"); + +-- CreateIndex +CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Payment_orderId_key" ON "Payment"("orderId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Payment_providerPaymentId_key" ON "Payment"("providerPaymentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Review_userId_productId_key" ON "Review"("userId", "productId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SiteSettings_key_key" ON "SiteSettings"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "Page_slug_key" ON "Page"("slug"); + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "ProductType"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductVariant" ADD CONSTRAINT "ProductVariant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductCategory" ADD CONSTRAINT "ProductCategory_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductCategory" ADD CONSTRAINT "ProductCategory_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MediaAsset" ADD CONSTRAINT "MediaAsset_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Payment" ADD CONSTRAINT "Payment_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PageSection" ADD CONSTRAINT "PageSection_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/app/prisma/migrations/migration_lock.toml b/app/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/app/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma new file mode 100644 index 0000000..475b2aa --- /dev/null +++ b/app/prisma/schema.prisma @@ -0,0 +1,257 @@ +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum UserRole { + CUSTOMER + ADMIN + OWNER +} + +enum ProductStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +enum OrderStatus { + PENDING + PAID + CANCELLED + REFUNDED + FULFILLED +} + +enum ReviewStatus { + PENDING + APPROVED + HIDDEN +} + +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String + role UserRole @default(CUSTOMER) + name String? + mustChangePassword Boolean @default(false) + emailVerifiedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sessions Session[] + orders Order[] + reviews Review[] + passwordResetTokens PasswordResetToken[] +} + +model Session { + id String @id @default(cuid()) + userId String + tokenHash String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model PasswordResetToken { + id String @id @default(cuid()) + userId String + tokenHash String @unique + expiresAt DateTime + usedAt DateTime? + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model ProductType { + id String @id @default(cuid()) + name String + slug String @unique + schema Json + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + products Product[] +} + +model Product { + id String @id @default(cuid()) + typeId String + title String + slug String @unique + description String + basePrice Int + currency String @default("EUR") + status ProductStatus @default(DRAFT) + attributes Json + stock Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + type ProductType @relation(fields: [typeId], references: [id]) + variants ProductVariant[] + categories ProductCategory[] + images MediaAsset[] + orderItems OrderItem[] + reviews Review[] +} + +model ProductVariant { + id String @id @default(cuid()) + productId String + sku String @unique + price Int + stock Int + attributes Json + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) +} + +model Category { + id String @id @default(cuid()) + name String + slug String @unique + parentId String? + createdAt DateTime @default(now()) + + products ProductCategory[] +} + +model ProductCategory { + productId String + categoryId String + + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + + @@id([productId, categoryId]) +} + +model MediaAsset { + id String @id @default(cuid()) + productId String? + url String + altText String? + mimeType String + size Int + createdAt DateTime @default(now()) + + product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) +} + +model Order { + id String @id @default(cuid()) + userId String + status OrderStatus @default(PENDING) + currency String @default("EUR") + subtotal Int + taxTotal Int @default(0) + shippingTotal Int @default(0) + grandTotal Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id]) + items OrderItem[] + payment Payment? +} + +model OrderItem { + id String @id @default(cuid()) + orderId String + productId String + title String + quantity Int + unitPrice Int + totalPrice Int + metadata Json? + + order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id]) +} + +model Payment { + id String @id @default(cuid()) + orderId String @unique + provider String + providerPaymentId String? @unique + status String + amount Int + currency String + rawPayload Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) +} + +model Review { + id String @id @default(cuid()) + userId String + productId String + rating Int + title String? + comment String? + status ReviewStatus @default(PENDING) + verified Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + + @@unique([userId, productId]) +} + +model SiteSettings { + id String @id @default(cuid()) + key String @unique + value Json + updatedAt DateTime @updatedAt +} + +model Page { + id String @id @default(cuid()) + slug String @unique + title String + seoTitle String? + seoDesc String? + published Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sections PageSection[] +} + +model PageSection { + id String @id @default(cuid()) + pageId String + type String + sortOrder Int + data Json + visible Boolean @default(true) + + page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) +} + +model AuditLog { + id String @id @default(cuid()) + userId String? + action String + entity String + entityId String? + metadata Json? + createdAt DateTime @default(now()) +} diff --git a/app/scripts/bootstrap-admin.js b/app/scripts/bootstrap-admin.js new file mode 100644 index 0000000..aa0938d --- /dev/null +++ b/app/scripts/bootstrap-admin.js @@ -0,0 +1,46 @@ +const { PrismaClient } = require('@prisma/client') +const bcrypt = require('bcryptjs') + +const prisma = new PrismaClient() + +async function main() { + const email = process.env.INITIAL_ADMIN_EMAIL + const password = process.env.INITIAL_ADMIN_PASSWORD + + if (!email || !password) { + console.log('INITIAL_ADMIN_EMAIL or INITIAL_ADMIN_PASSWORD not set, skipping bootstrap') + return + } + + const ownerCount = await prisma.user.count({ + where: { role: 'OWNER' }, + }) + + if (ownerCount > 0) { + console.log('Owner already exists, skipping bootstrap') + return + } + + const passwordHash = await bcrypt.hash(password, 12) + + const admin = await prisma.user.create({ + data: { + email, + passwordHash, + role: 'OWNER', + name: 'Admin', + mustChangePassword: true, + }, + }) + + console.log(`Created owner account: ${admin.email}`) +} + +main() + .catch((e) => { + console.error('Bootstrap failed:', e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/app/scripts/bootstrap-admin.ts b/app/scripts/bootstrap-admin.ts new file mode 100644 index 0000000..8f35269 --- /dev/null +++ b/app/scripts/bootstrap-admin.ts @@ -0,0 +1,46 @@ +import { PrismaClient } from '@prisma/client' +import bcrypt from 'bcryptjs' + +const prisma = new PrismaClient() + +async function main() { + const email = process.env.INITIAL_ADMIN_EMAIL + const password = process.env.INITIAL_ADMIN_PASSWORD + + if (!email || !password) { + console.log('INITIAL_ADMIN_EMAIL or INITIAL_ADMIN_PASSWORD not set, skipping bootstrap') + return + } + + const ownerCount = await prisma.user.count({ + where: { role: 'OWNER' }, + }) + + if (ownerCount > 0) { + console.log('Owner already exists, skipping bootstrap') + return + } + + const passwordHash = await bcrypt.hash(password, 12) + + const admin = await prisma.user.create({ + data: { + email, + passwordHash, + role: 'OWNER', + name: 'Admin', + mustChangePassword: true, + }, + }) + + console.log(`Created owner account: ${admin.email}`) +} + +main() + .catch((e) => { + console.error('Bootstrap failed:', e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/app/src/app/account/orders/page.tsx b/app/src/app/account/orders/page.tsx new file mode 100644 index 0000000..8637469 --- /dev/null +++ b/app/src/app/account/orders/page.tsx @@ -0,0 +1,125 @@ +'use client' + +import { Suspense, useEffect, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Link from 'next/link' +import { Navbar } from '@/components/storefront/Navbar' +import { Badge } from '@/components/ui/Badge' +import { Alert } from '@/components/ui/Alert' + +interface Order { + id: string + status: string + grandTotal: number + currency: string + createdAt: string + items: Array<{ title: string; quantity: number; unitPrice: number }> +} + +const statusVariant: Record = { + PENDING: 'warning', + PAID: 'success', + FULFILLED: 'success', + CANCELLED: 'danger', + REFUNDED: 'info', +} + +export default function OrdersPage() { + return ( + + + + ) +} + +function OrdersContent() { + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) + const router = useRouter() + const searchParams = useSearchParams() + const success = searchParams.get('success') + + useEffect(() => { + const stored = localStorage.getItem('user') + if (!stored) { + router.push('/login?redirect=/account/orders') + return + } + + fetch('/api/orders') + .then((r) => r.json()) + .then((data) => { + setOrders(data.orders || []) + setLoading(false) + }) + .catch(() => setLoading(false)) + }, [router]) + + return ( +
+ +
+
+

My Orders

+ + Back to Account + +
+ + {success && ( + + Payment successful! Your order has been confirmed. + + )} + + {loading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ) : orders.length === 0 ? ( +
+

No orders yet.

+ + Start shopping + +
+ ) : ( +
+ {orders.map((order) => ( +
+
+
+

Order #{order.id.slice(-8).toUpperCase()}

+

+ {new Date(order.createdAt).toLocaleDateString()} +

+
+
+ + {order.status} + +

+ {(order.grandTotal / 100).toFixed(2)} {order.currency} +

+
+
+
+
    + {order.items.map((item, i) => ( +
  • + {item.title} × {item.quantity} —{' '} + {((item.unitPrice * item.quantity) / 100).toFixed(2)} EUR +
  • + ))} +
+
+
+ ))} +
+ )} +
+
+ ) +} diff --git a/app/src/app/account/page.tsx b/app/src/app/account/page.tsx new file mode 100644 index 0000000..35959ab --- /dev/null +++ b/app/src/app/account/page.tsx @@ -0,0 +1,80 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { Navbar } from '@/components/storefront/Navbar' + +interface User { + id: string + email: string + name?: string + role: string +} + +export default function AccountPage() { + const [user, setUser] = useState(null) + const router = useRouter() + + useEffect(() => { + const stored = localStorage.getItem('user') + if (!stored) { + router.push('/login?redirect=/account') + return + } + try { + setUser(JSON.parse(stored)) + } catch { + router.push('/login') + } + }, [router]) + + if (!user) return null + + return ( +
+ +
+

My Account

+ +
+

Profile

+
+
+
Name:
+
{user.name || '—'}
+
+
+
Email:
+
{user.email}
+
+
+
Role:
+
{user.role.toLowerCase()}
+
+
+
+ +
+ +
📦
+

My Orders

+

View your order history

+ + + +
🛍️
+

Shop

+

Browse our products

+ +
+
+
+ ) +} diff --git a/app/src/app/admin/admin-users/page.tsx b/app/src/app/admin/admin-users/page.tsx new file mode 100644 index 0000000..36ddaa6 --- /dev/null +++ b/app/src/app/admin/admin-users/page.tsx @@ -0,0 +1,192 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Badge } from '@/components/ui/Badge' +import { Input, Select } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { Alert } from '@/components/ui/Alert' + +interface AdminUser { + id: string + email: string + name: string | null + role: string + createdAt: string + mustChangePassword: boolean +} + +export default function AdminUsersPage() { + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [showForm, setShowForm] = useState(false) + const [email, setEmail] = useState('') + const [name, setName] = useState('') + const [role, setRole] = useState('ADMIN') + const [password, setPassword] = useState('') + const [creating, setCreating] = useState(false) + + async function loadUsers() { + const res = await fetch('/api/admin/users') + if (res.ok) { + const data = await res.json() + setUsers(data.users || []) + } else { + setError('You do not have permission to manage admin users (Owner role required)') + } + setLoading(false) + } + + useEffect(() => { + loadUsers() + }, []) + + async function handleCreate(e: React.FormEvent) { + e.preventDefault() + setError('') + setCreating(true) + + const res = await fetch('/api/admin/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, name, role, password }), + }) + + const data = await res.json() + + if (res.ok) { + setSuccess('Admin user created!') + setShowForm(false) + setEmail('') + setName('') + setPassword('') + loadUsers() + } else { + setError(data.error || 'Failed to create user') + } + + setCreating(false) + } + + async function deleteUser(id: string) { + if (!confirm('Delete this admin user?')) return + const res = await fetch(`/api/admin/users?id=${id}`, { method: 'DELETE' }) + if (res.ok) { + loadUsers() + } + } + + return ( +
+
+

Admin Users

+ +
+ + {error && {error}} + {success && {success}} + + {showForm && ( +
+

New Admin User

+
+
+ setName(e.target.value)} + required + /> + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+

+ User will be required to change their password on first login. +

+
+ + +
+
+
+ )} + +
+ {loading ? ( +
Loading...
+ ) : users.length === 0 ? ( +
No admin users found.
+ ) : ( + + + + + + + + + + + + + {users.map((u) => ( + + + + + + + + + ))} + +
NameEmailRoleStatusCreatedActions
{u.name || '—'}{u.email} + + {u.role} + + + {u.mustChangePassword ? ( + Must change password + ) : ( + Active + )} + + {new Date(u.createdAt).toLocaleDateString()} + + +
+ )} +
+
+ ) +} diff --git a/app/src/app/admin/categories/page.tsx b/app/src/app/admin/categories/page.tsx new file mode 100644 index 0000000..421fa1f --- /dev/null +++ b/app/src/app/admin/categories/page.tsx @@ -0,0 +1,188 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Input, Select } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { Alert } from '@/components/ui/Alert' + +interface Category { + id: string + name: string + slug: string + parentId: string | null + _count: { products: number } +} + +export default function AdminCategoriesPage() { + const [categories, setCategories] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [showForm, setShowForm] = useState(false) + const [editId, setEditId] = useState(null) + const [name, setName] = useState('') + const [slug, setSlug] = useState('') + const [parentId, setParentId] = useState('') + + async function loadCategories() { + const res = await fetch('/api/admin/categories') + const data = await res.json() + setCategories(data.categories || []) + setLoading(false) + } + + useEffect(() => { + loadCategories() + }, []) + + function generateSlug(n: string) { + return n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + + const url = '/api/admin/categories' + const method = editId ? 'PUT' : 'POST' + const body = editId + ? { id: editId, name, slug, parentId: parentId || null } + : { name, slug, parentId: parentId || null } + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + + if (res.ok) { + setSuccess(editId ? 'Updated!' : 'Created!') + setShowForm(false) + setEditId(null) + setName('') + setSlug('') + setParentId('') + loadCategories() + } else { + const data = await res.json() + setError(data.error || 'Failed') + } + } + + function startEdit(c: Category) { + setEditId(c.id) + setName(c.name) + setSlug(c.slug) + setParentId(c.parentId || '') + setShowForm(true) + } + + async function handleDelete(id: string) { + if (!confirm('Delete this category?')) return + const res = await fetch(`/api/admin/categories?id=${id}`, { method: 'DELETE' }) + if (res.ok) { + loadCategories() + } + } + + return ( +
+
+

Categories

+ +
+ + {error && {error}} + {success && {success}} + + {showForm && ( +
+

{editId ? 'Edit Category' : 'New Category'}

+
+
+ { + setName(e.target.value) + if (!editId) setSlug(generateSlug(e.target.value)) + }} + required + /> + setSlug(e.target.value)} + required + /> +
+ +
+ + +
+
+
+ )} + +
+ {loading ? ( +
Loading...
+ ) : categories.length === 0 ? ( +
No categories yet.
+ ) : ( + + + + + + + + + + + + {categories.map((c) => ( + + + + + + + + ))} + +
NameSlugParentProductsActions
{c.name}{c.slug} + {c.parentId ? categories.find((p) => p.id === c.parentId)?.name || '—' : '—'} + {c._count.products} +
+ + {c._count.products === 0 && ( + + )} +
+
+ )} +
+
+ ) +} diff --git a/app/src/app/admin/change-password/page.tsx b/app/src/app/admin/change-password/page.tsx new file mode 100644 index 0000000..89cd4cc --- /dev/null +++ b/app/src/app/admin/change-password/page.tsx @@ -0,0 +1,107 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { Input } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { Alert } from '@/components/ui/Alert' + +export default function ChangePasswordPage() { + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirm, setConfirm] = useState('') + const [error, setError] = useState('') + const [success, setSuccess] = useState(false) + const [loading, setLoading] = useState(false) + const router = useRouter() + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + + if (newPassword !== confirm) { + setError('New passwords do not match') + return + } + + setLoading(true) + + try { + const response = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.error || 'Failed to change password') + return + } + + setSuccess(true) + // Update localStorage to remove mustChangePassword flag + const userStr = localStorage.getItem('user') + if (userStr) { + const user = JSON.parse(userStr) + user.mustChangePassword = false + localStorage.setItem('user', JSON.stringify(user)) + } + + setTimeout(() => router.push('/admin'), 1500) + } catch { + setError('Something went wrong') + } finally { + setLoading(false) + } + } + + return ( +
+

Change Password

+

+ You must change your password before continuing. +

+ + {error && {error}} + {success && ( + + Password changed successfully! Redirecting... + + )} + +
+ setCurrentPassword(e.target.value)} + required + /> +
+ setNewPassword(e.target.value)} + required + /> +

+ Min 12 chars, uppercase, lowercase, number, symbol. +

+
+ setConfirm(e.target.value)} + required + /> + +
+
+ ) +} diff --git a/app/src/app/admin/customers/page.tsx b/app/src/app/admin/customers/page.tsx new file mode 100644 index 0000000..4ca94fe --- /dev/null +++ b/app/src/app/admin/customers/page.tsx @@ -0,0 +1,68 @@ +import { prisma } from '@/lib/prisma' +import { getCurrentUser } from '@/lib/auth' +import { redirect } from 'next/navigation' + +async function getCustomers() { + return prisma.user.findMany({ + where: { role: 'CUSTOMER' }, + select: { + id: true, + email: true, + name: true, + createdAt: true, + emailVerifiedAt: true, + _count: { select: { orders: true } }, + }, + orderBy: { createdAt: 'desc' }, + }) +} + +export default async function AdminCustomersPage() { + const user = await getCurrentUser() + if (!user || (user.role !== 'ADMIN' && user.role !== 'OWNER')) redirect('/login') + + const customers = await getCustomers() + + return ( +
+

Customers

+ +
+ {customers.length === 0 ? ( +
No customers yet.
+ ) : ( + + + + + + + + + + + + {customers.map((c) => ( + + + + + + + + ))} + +
CustomerEmailOrdersVerifiedJoined
{c.name || '—'}{c.email}{c._count.orders} + {c.emailVerifiedAt ? ( + Verified + ) : ( + Not verified + )} + + {new Date(c.createdAt).toLocaleDateString()} +
+ )} +
+
+ ) +} diff --git a/app/src/app/admin/layout.tsx b/app/src/app/admin/layout.tsx new file mode 100644 index 0000000..2353ff4 --- /dev/null +++ b/app/src/app/admin/layout.tsx @@ -0,0 +1,44 @@ +import { redirect } from 'next/navigation' +import { headers } from 'next/headers' +import { getCurrentUser } from '@/lib/auth' +import { AdminNav } from '@/components/admin/AdminNav' + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + const user = await getCurrentUser() + + if (!user) { + redirect('/login') + } + + if (user.role === 'CUSTOMER') { + return ( +
+
+

Access Denied

+

You do not have permission to access this area.

+
+
+ ) + } + + if (user.mustChangePassword) { + const headersList = await headers() + const pathname = headersList.get('x-pathname') ?? '' + if (!pathname.startsWith('/admin/change-password')) { + redirect('/admin/change-password') + } + } + + return ( +
+ +
+ {children} +
+
+ ) +} diff --git a/app/src/app/admin/orders/[id]/page.tsx b/app/src/app/admin/orders/[id]/page.tsx new file mode 100644 index 0000000..8ff5dc9 --- /dev/null +++ b/app/src/app/admin/orders/[id]/page.tsx @@ -0,0 +1,223 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { Badge } from '@/components/ui/Badge' +import { Select } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { Alert } from '@/components/ui/Alert' + +interface Order { + id: string + status: string + grandTotal: number + currency: string + subtotal: number + taxTotal: number + shippingTotal: number + createdAt: string + user: { email: string; name: string | null } + items: Array<{ + id: string + title: string + quantity: number + unitPrice: number + totalPrice: number + product: { title: string; slug: string } + }> + payment: { + provider: string + status: string + amount: number + providerPaymentId: string | null + } | null +} + +const statusVariant: Record = { + PENDING: 'warning', + PAID: 'success', + FULFILLED: 'success', + CANCELLED: 'danger', + REFUNDED: 'info', +} + +export default function AdminOrderDetailPage() { + const params = useParams() + const router = useRouter() + const [order, setOrder] = useState(null) + const [loading, setLoading] = useState(true) + const [newStatus, setNewStatus] = useState('') + const [updating, setUpdating] = useState(false) + const [message, setMessage] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + fetch(`/api/admin/orders/${params.id}`) + .then((r) => r.json()) + .then((data) => { + setOrder(data.order) + setNewStatus(data.order?.status || '') + setLoading(false) + }) + }, [params.id]) + + async function updateStatus() { + setUpdating(true) + setError('') + setMessage('') + const res = await fetch(`/api/admin/orders/${params.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }), + }) + if (res.ok) { + const data = await res.json() + setOrder((o) => o ? { ...o, status: data.order.status } : o) + setMessage('Status updated!') + } else { + const data = await res.json() + setError(data.error || 'Failed') + } + setUpdating(false) + } + + if (loading) return
Loading...
+ if (!order) return
Order not found
+ + return ( +
+
+

+ Order #{order.id.slice(-8).toUpperCase()} +

+ +
+ + {error && {error}} + {message && {message}} + +
+
+

Customer

+

{order.user.name || '—'}

+

{order.user.email}

+
+ +
+

Status

+
+ + {order.status} + + + {new Date(order.createdAt).toLocaleString()} + +
+
+ + +
+
+
+ +
+
+

Items

+
+ + + + + + + + + + + {order.items.map((item) => ( + + + + + + + ))} + + + + + + + {order.taxTotal > 0 && ( + + + + + )} + {order.shippingTotal > 0 && ( + + + + + )} + + + + + +
ProductQtyUnit PriceTotal
{item.title}{item.quantity} + {(item.unitPrice / 100).toFixed(2)} {order.currency} + + {(item.totalPrice / 100).toFixed(2)} {order.currency} +
Subtotal{(order.subtotal / 100).toFixed(2)} {order.currency}
Tax{(order.taxTotal / 100).toFixed(2)} {order.currency}
Shipping{(order.shippingTotal / 100).toFixed(2)} {order.currency}
Total + {(order.grandTotal / 100).toFixed(2)} {order.currency} +
+
+ + {order.payment && ( +
+

Payment

+
+
+
Provider:
+
{order.payment.provider}
+
+
+
Status:
+
{order.payment.status}
+
+
+
Amount:
+
+ {(order.payment.amount / 100).toFixed(2)} {order.currency} +
+
+ {order.payment.providerPaymentId && ( +
+
Payment ID:
+
{order.payment.providerPaymentId}
+
+ )} +
+
+ )} +
+ ) +} diff --git a/app/src/app/admin/orders/page.tsx b/app/src/app/admin/orders/page.tsx new file mode 100644 index 0000000..ebdc8de --- /dev/null +++ b/app/src/app/admin/orders/page.tsx @@ -0,0 +1,126 @@ +'use client' + +import { useEffect, useState } from 'react' +import Link from 'next/link' +import { useSearchParams } from 'next/navigation' +import { Badge } from '@/components/ui/Badge' + +interface Order { + id: string + status: string + grandTotal: number + currency: string + createdAt: string + user: { email: string; name: string | null } + items: Array<{ title: string; quantity: number }> +} + +const statusVariant: Record = { + PENDING: 'warning', + PAID: 'success', + FULFILLED: 'success', + CANCELLED: 'danger', + REFUNDED: 'info', +} + +export default function AdminOrdersPage() { + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(true) + const [statusFilter, setStatusFilter] = useState('') + const searchParams = useSearchParams() + + useEffect(() => { + const s = searchParams.get('status') || '' + setStatusFilter(s) + }, [searchParams]) + + useEffect(() => { + const params = new URLSearchParams() + if (statusFilter) params.set('status', statusFilter) + + fetch(`/api/admin/orders?${params}`) + .then((r) => r.json()) + .then((data) => { + setOrders(data.orders || []) + setLoading(false) + }) + }, [statusFilter]) + + return ( +
+

Orders

+ +
+ +
+ +
+ {loading ? ( +
Loading...
+ ) : orders.length === 0 ? ( +
No orders found.
+ ) : ( + + + + + + + + + + + + + + {orders.map((order) => ( + + + + + + + + + + ))} + +
OrderCustomerItemsTotalStatusDateActions
+ #{order.id.slice(-8).toUpperCase()} + +

{order.user.name || '—'}

+

{order.user.email}

+
+ {order.items.map((i) => `${i.title} ×${i.quantity}`).join(', ')} + + {(order.grandTotal / 100).toFixed(2)} {order.currency} + + + {order.status} + + + {new Date(order.createdAt).toLocaleDateString()} + + + View + +
+ )} +
+
+ ) +} diff --git a/app/src/app/admin/page.tsx b/app/src/app/admin/page.tsx new file mode 100644 index 0000000..21da4c1 --- /dev/null +++ b/app/src/app/admin/page.tsx @@ -0,0 +1,158 @@ +import { redirect } from 'next/navigation' +import { getCurrentUser } from '@/lib/auth' +import { prisma } from '@/lib/prisma' +import Link from 'next/link' +import { Badge } from '@/components/ui/Badge' + +async function getDashboardData() { + const [totalOrders, pendingOrders, totalRevenue, totalProducts, totalCustomers, recentOrders, pendingReviews] = + await Promise.all([ + prisma.order.count(), + prisma.order.count({ where: { status: 'PENDING' } }), + prisma.order.aggregate({ + where: { status: { in: ['PAID', 'FULFILLED'] } }, + _sum: { grandTotal: true }, + }), + prisma.product.count({ where: { status: { not: 'ARCHIVED' } } }), + prisma.user.count({ where: { role: 'CUSTOMER' } }), + prisma.order.findMany({ + take: 5, + orderBy: { createdAt: 'desc' }, + include: { user: { select: { email: true, name: true } } }, + }), + prisma.review.count({ where: { status: 'PENDING' } }), + ]) + + return { + stats: { + totalOrders, + pendingOrders, + totalRevenue: totalRevenue._sum.grandTotal ?? 0, + totalProducts, + totalCustomers, + pendingReviews, + }, + recentOrders, + } +} + +const statusVariant: Record = { + PENDING: 'warning', + PAID: 'success', + FULFILLED: 'success', + CANCELLED: 'danger', + REFUNDED: 'info', +} + +export default async function AdminDashboardPage() { + const user = await getCurrentUser() + if (!user) redirect('/login') + if (user.mustChangePassword) redirect('/admin/change-password') + + const { stats, recentOrders } = await getDashboardData() + + return ( +
+

Dashboard

+ + {/* Stats */} +
+
+

Total Orders

+

{stats.totalOrders}

+
+
+

Pending Orders

+

{stats.pendingOrders}

+
+
+

Revenue

+

+ {(stats.totalRevenue / 100).toFixed(0)} EUR +

+
+
+

Products

+

{stats.totalProducts}

+
+
+

Customers

+

{stats.totalCustomers}

+
+
+

Pending Reviews

+

{stats.pendingReviews}

+
+
+ + {/* Quick links */} +
+ +

+ New Product

+ + +

View Pending Orders

+ + +

Review Pending Reviews

+ + +

Settings

+ +
+ + {/* Recent orders */} +
+
+

Recent Orders

+ + View all + +
+ {recentOrders.length === 0 ? ( +
No orders yet
+ ) : ( +
+ {recentOrders.map((order) => ( + +
+

#{order.id.slice(-8).toUpperCase()}

+

+ {order.user.name || order.user.email} +

+
+
+ + {order.status} + + + {(order.grandTotal / 100).toFixed(2)} EUR + + + {new Date(order.createdAt).toLocaleDateString()} + +
+ + ))} +
+ )} +
+
+ ) +} diff --git a/app/src/app/admin/product-types/page.tsx b/app/src/app/admin/product-types/page.tsx new file mode 100644 index 0000000..ea59d25 --- /dev/null +++ b/app/src/app/admin/product-types/page.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Input } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { Alert } from '@/components/ui/Alert' + +interface ProductType { + id: string + name: string + slug: string + schema: object + _count: { products: number } +} + +export default function AdminProductTypesPage() { + const [types, setTypes] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [showForm, setShowForm] = useState(false) + const [editId, setEditId] = useState(null) + const [name, setName] = useState('') + const [slug, setSlug] = useState('') + const [schemaStr, setSchemaStr] = useState('{}') + + async function loadTypes() { + const res = await fetch('/api/admin/product-types') + const data = await res.json() + setTypes(data.types || []) + setLoading(false) + } + + useEffect(() => { + loadTypes() + }, []) + + function generateSlug(n: string) { + return n + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError('') + + let schema: object + try { + schema = JSON.parse(schemaStr) + } catch { + setError('Schema must be valid JSON') + return + } + + const url = '/api/admin/product-types' + const method = editId ? 'PUT' : 'POST' + const body = editId + ? { id: editId, name, slug, schema } + : { name, slug, schema } + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + + if (res.ok) { + setSuccess(editId ? 'Updated!' : 'Created!') + setShowForm(false) + setEditId(null) + setName('') + setSlug('') + setSchemaStr('{}') + loadTypes() + } else { + const data = await res.json() + setError(data.error || 'Failed') + } + } + + function startEdit(t: ProductType) { + setEditId(t.id) + setName(t.name) + setSlug(t.slug) + setSchemaStr(JSON.stringify(t.schema, null, 2)) + setShowForm(true) + } + + async function handleDelete(id: string) { + if (!confirm('Delete this product type?')) return + const res = await fetch(`/api/admin/product-types?id=${id}`, { method: 'DELETE' }) + if (res.ok) { + loadTypes() + } + } + + return ( +
+
+

Product Types

+ +
+ + {error && {error}} + {success && {success}} + + {showForm && ( +
+

{editId ? 'Edit Type' : 'New Product Type'}

+
+
+ { + setName(e.target.value) + if (!editId) setSlug(generateSlug(e.target.value)) + }} + required + /> + setSlug(e.target.value)} + required + /> +
+
+ +