From 05b540997e0402e8e3345dad7f30b2e694c07634 Mon Sep 17 00:00:00 2001 From: "Najjar\\NajjarV02" Date: Mon, 13 Apr 2026 17:57:59 +0400 Subject: [PATCH] feat: add admin authentication and management features - Implemented Prisma schema with models for AdminUser, AppSettings, and Snapshot. - Created seed script to initialize the database with an admin user and JWT secret. - Developed admin login page with form handling and error management. - Added API routes for admin login, logout, change password, and JWT verification. - Integrated Stripe for payment intent management in admin orders. - Established middleware for protecting admin routes with JWT authentication. - Created Zustand stores for managing persona and snapshot states. --- .env.example | 4 + ...minDashboardAuthCrudEnhancements.prompt.md | 116 ++ .gitignore | 2 + STRIPE_INTEGRATION.md | 108 ++ next.config.mjs | 1 - package-lock.json | 1617 ++++++++++++++++- package.json | 10 + prisma.config.ts | 11 + prisma/lootah.db | Bin 0 -> 32768 bytes prisma/schema.prisma | 28 + prisma/seed.ts | 60 + src/app/admin/login/page.tsx | 172 ++ src/app/admin/page.tsx | 725 ++++++-- src/app/api/admin/change-password/route.ts | 37 + src/app/api/admin/login/route.ts | 49 + src/app/api/admin/logout/route.ts | 13 + src/app/api/admin/orders/route.ts | 32 + src/app/api/admin/verify/route.ts | 26 + src/app/api/create-payment-intent/route.ts | 39 + src/components/CheckoutOverlay.tsx | 71 +- src/components/ConfigPanel.tsx | 38 +- src/components/checkout/PaymentStep.tsx | 185 +- src/components/checkout/ReviewStep.tsx | 46 +- src/lib/prisma.ts | 15 + src/middleware.ts | 34 + src/store/useOrderStore.test.ts | 18 +- src/store/useOrderStore.ts | 64 +- src/store/usePersonaStore.ts | 140 ++ src/store/usePricingStore.ts | 21 + src/store/useSnapshotStore.ts | 18 + 30 files changed, 3338 insertions(+), 362 deletions(-) create mode 100644 .env.example create mode 100644 .github/prompts/plan-adminDashboardAuthCrudEnhancements.prompt.md create mode 100644 STRIPE_INTEGRATION.md create mode 100644 prisma.config.ts create mode 100644 prisma/lootah.db create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 src/app/admin/login/page.tsx create mode 100644 src/app/api/admin/change-password/route.ts create mode 100644 src/app/api/admin/login/route.ts create mode 100644 src/app/api/admin/logout/route.ts create mode 100644 src/app/api/admin/orders/route.ts create mode 100644 src/app/api/admin/verify/route.ts create mode 100644 src/app/api/create-payment-intent/route.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/middleware.ts create mode 100644 src/store/usePersonaStore.ts create mode 100644 src/store/useSnapshotStore.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0f574b1 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Stripe Keys +# Get your keys from https://dashboard.stripe.com/apikeys +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RBtf7I7xOcO9rzigsLLK3esMLmBlJoRztbzUadPhQm7tcHQuScViFEkwdfAwDxbaqt5n8BOuJV9wRSMdn2IrxIX00lqGOOJfT +STRIPE_SECRET_KEY=sk_test_51RBtf7I7xOcO9rzitxMqK3jnTb3SPdEbyGxGBnPccGEfrIrpiEFEOIEG2oHuTumaUejUN4FyAOBg0AVCBRn6AOKI00LeWSDC10 diff --git a/.github/prompts/plan-adminDashboardAuthCrudEnhancements.prompt.md b/.github/prompts/plan-adminDashboardAuthCrudEnhancements.prompt.md new file mode 100644 index 0000000..12b170c --- /dev/null +++ b/.github/prompts/plan-adminDashboardAuthCrudEnhancements.prompt.md @@ -0,0 +1,116 @@ +# Plan: Admin Dashboard Auth + CRUD + Enhancements + +## TL;DR +Add password authentication to the admin dashboard, enable full CRUD for personas/pricing, and add useful admin features like order analytics and configurator settings management. + +## Phase 1: Admin Authentication (Database-backed with Prisma + SQLite) + +### Steps +1. **Setup Prisma + SQLite** + - Install `prisma` and `@prisma/client` + - Create `prisma/schema.prisma` with SQLite provider + - Models: `AdminUser` (id, username, passwordHash, createdAt), `AppSettings` (key, value) + - Run `npx prisma db push` to create the database + - Create `src/lib/prisma.ts` — singleton Prisma client + +2. **Create seed script** `prisma/seed.ts` + - Creates default admin user with bcrypt-hashed password + - Generates and stores JWT secret in `AppSettings` table + - Run with `npx prisma db seed` + +3. **Create admin auth API routes** + - `src/app/api/admin/login/route.ts` — POST, accepts `{ username, password }`, verifies bcrypt hash from DB, returns JWT in httpOnly cookie (JWT secret from DB) + - `src/app/api/admin/verify/route.ts` — GET, checks JWT cookie validity + - `src/app/api/admin/logout/route.ts` — POST, clears auth cookie + - `src/app/api/admin/change-password/route.ts` — POST, accepts `{ currentPassword, newPassword }`, updates hash in DB + +4. **Create admin middleware** `src/middleware.ts` + - Protects all `/admin/*` routes (except `/admin/login/`) + - Checks for valid JWT cookie, redirects to `/admin/login/` if missing/invalid + - Note: middleware can't use Prisma directly (edge runtime), so JWT secret needs to be in env OR verify via API call + +5. **Create admin login page** `src/app/admin/login/page.tsx` + - Username + password form (styled to match existing admin design) + - Calls login API, redirects to `/admin/` on success + - Shows error on wrong credentials + +6. **Add JWT_SECRET to .env.local** (only this one — password is in DB) + - Alternative: store JWT secret in DB and load at startup into a module-level cache + +### Dependencies to install +- `prisma` (dev), `@prisma/client`, `bcryptjs`, `@types/bcryptjs` (dev), `jose` + +## Phase 2: Full CRUD for Pricing Items + Personas + +### Steps (depends on Phase 1) +6. **Add/Remove pricing items in admin** — Update `src/app/admin/page.tsx` + - "Add Item" button with fields: id (auto-slug from label), label, price + - Delete button per row (with confirmation) + - Update `usePricingStore` to support `addItem()` and `removeItem()` actions + +7. **Persona management section** — New section in admin page + - List current personas with edit capability (label, description, colors) + - Add new persona form + - Delete persona button + - Create `usePersonaStore` or extend `usePricingStore` to manage persona definitions + - Sync persona list with ConfigPanel's `PERSONA_OPTIONS` (currently hardcoded) + +## Phase 3: Additional Admin Features + +### Steps (parallel with Phase 2) +8. **Orders dashboard section** — New section in admin page + - Show orders from Stripe API (list recent payments) + - Display: order ID, customer email, amount, status, date + - New API route `src/app/api/admin/orders/route.ts` to fetch from Stripe + +9. **Analytics overview cards** at top of admin page + - Total revenue (from Stripe) + - Number of orders + - Most popular persona + - Most popular color + +10. **Configurator settings** section + - Edit default color (`#96a2b6`) + - Toggle available color options + - Set min/max price boundaries + +11. **Logout button** in admin header + +## Relevant Files +- `src/app/admin/page.tsx` — Main admin dashboard (add CRUD UI, analytics) +- `src/app/admin/login/page.tsx` — New login page +- `src/app/api/admin/login/route.ts` — New auth API +- `src/app/api/admin/verify/route.ts` — New verify API +- `src/app/api/admin/orders/route.ts` — New orders API +- `src/middleware.ts` — New, protects admin routes +- `src/lib/prisma.ts` — New, Prisma client singleton +- `prisma/schema.prisma` — New, database schema (AdminUser, AppSettings) +- `prisma/seed.ts` — New, seeds default admin user +- `src/store/usePricingStore.ts` — Add `addItem()`, `removeItem()` actions +- `src/components/ConfigPanel.tsx` — Persona options currently hardcoded (PERSONA_OPTIONS const) +- `.env.local` — Add ADMIN_PASSWORD, ADMIN_JWT_SECRET + +## Verification +1. Visit `/admin/` without login → redirected to `/admin/login/` +2. Enter wrong password → error message shown +3. Enter correct password → redirected to `/admin/` +4. Add a pricing item → appears in configurator pricing breakdown +5. Remove a pricing item → disappears from pricing +6. Edit persona → reflected in ConfigPanel +7. Orders section shows real Stripe payment data +8. Refresh admin page → still logged in (cookie persists) +9. Run `npx vitest run` → all existing tests pass + +## Decisions +- **Database: Prisma + SQLite** — file-based, no external server needed, real ORM +- Admin password stored as bcrypt hash in DB (not in env vars) +- JWT secret stored in AppSettings table in DB +- JWT in httpOnly cookie — secure, no localStorage tokens +- jose library for JWT — lightweight, edge-compatible (works in Next.js middleware) +- Personas need to move from hardcoded array to store-driven — breaking change in ConfigPanel +- Stripe orders fetched via Stripe API directly — no local DB needed for orders + +## Further Considerations +1. **Persona storage**: Currently hardcoded in ConfigPanel. Moving to a store means ConfigPanel reads from store dynamically. This is required for admin CRUD to work. **Recommended: create persona store with localStorage persistence (same pattern as pricing store)** +2. **Stripe orders vs local orders**: Currently orders only exist client-side. To show in admin, we fetch from Stripe's payment intents list. **Recommended: use Stripe API directly, no local DB needed** +3. **Multi-admin support**: Currently single password. If needed later, can upgrade to NextAuth with credentials provider. **Recommended: start simple, upgrade if needed** diff --git a/.gitignore b/.gitignore index dbe83e3..11c8a90 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ next-env.d.ts # IDE .idea .vscode + +/src/generated/prisma diff --git a/STRIPE_INTEGRATION.md b/STRIPE_INTEGRATION.md new file mode 100644 index 0000000..e43d3ab --- /dev/null +++ b/STRIPE_INTEGRATION.md @@ -0,0 +1,108 @@ +# Stripe Payment Integration + +## ملخص التعديلات + +تم استبدال نظام الدفع التجريبي (Demo) بتكامل حقيقي مع **Stripe** لمعالجة المدفوعات. + +--- + +## الملفات الجديدة + +### 1. `src/app/api/create-payment-intent/route.ts` +- API Route على السيرفر لإنشاء Stripe PaymentIntent +- يستقبل المبلغ بالدرهم (AED) ويحوله لأصغر وحدة (فلس) +- يرسل metadata تشمل الـ persona واللون وإيميل العميل + +### 2. `.env.example` +- ملف مرجعي يحتوي على أسماء المتغيرات المطلوبة لمفاتيح Stripe + +--- + +## الملفات المعدّلة + +### 3. `src/store/useOrderStore.ts` +**قبل:** +```ts +export interface PaymentInfo { + cardNumber: string; + expiry: string; + cvv: string; + nameOnCard: string; +} +``` + +**بعد:** +```ts +export interface PaymentInfo { + paymentIntentId: string; + clientSecret: string; + status: 'idle' | 'processing' | 'succeeded' | 'failed'; + errorMessage: string; +} +``` + +- تمت إضافة action جديد: `createPaymentIntent()` — يستدعي الـ API Route وينشئ PaymentIntent +- تم تعديل `setPayment()` ليقبل `Partial` للتحديث التدريجي + +--- + +### 4. `src/components/checkout/PaymentStep.tsx` +**قبل:** فورم يدوي يجمع بيانات البطاقة (رقم، تاريخ انتهاء، CVV، اسم) — بدون معالجة حقيقية + +**بعد:** يستخدم `` من Stripe Elements — يعرض واجهة دفع آمنة تدعم: +- بطاقات الائتمان/الخصم +- Apple Pay +- Google Pay +- طرق دفع أخرى حسب إعدادات حساب Stripe + +--- + +### 5. `src/components/checkout/ReviewStep.tsx` +**قبل:** زر "Place Order" كان يعمل `setTimeout(1500)` فقط (محاكاة) + +**بعد:** يستدعي `stripe.confirmPayment()` لتأكيد الدفع الحقيقي عبر Stripe، مع عرض رسائل الخطأ إذا فشل الدفع + +--- + +### 6. `src/components/CheckoutOverlay.tsx` +- تم تحميل Stripe.js عبر `loadStripe()` +- يتم إنشاء PaymentIntent تلقائياً عند الدخول لخطوة الدفع +- يتم تغليف خطوتي Payment و Review بـ `` provider من Stripe + +--- + +### 7. `src/store/useOrderStore.test.ts` +- تم تحديث الـ tests لتتوافق مع الـ `PaymentInfo` الجديد + +--- + +## الحزم المضافة + +``` +stripe — Stripe SDK (سيرفر) +@stripe/stripe-js — Stripe.js loader (كلاينت) +@stripe/react-stripe-js — React components (Elements, PaymentElement) +``` + +--- + +## الإعداد + +1. أنشئ ملف `.env.local` في root المشروع: + +```env +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx +STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx +``` + +2. احصل على المفاتيح من: https://dashboard.stripe.com/apikeys + +3. للتجربة استخدم مفاتيح الـ test (`pk_test_` / `sk_test_`) + +--- + +## تدفق الدفع الجديد + +``` +Config → Shipping → [Payment Intent Created] → Payment (Stripe Elements) → Review → confirmPayment() → Confirmed +``` diff --git a/next.config.mjs b/next.config.mjs index c7af7f8..f586d65 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'export', images: { unoptimized: true, }, diff --git a/package-lock.json b/package-lock.json index d4575fa..70acf3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,27 @@ "name": "yslootah-robotics-g1-configurator", "version": "0.1.0", "dependencies": { + "@libsql/client": "^0.17.2", + "@prisma/adapter-libsql": "^7.7.0", + "@prisma/client": "^7.7.0", "@react-spring/three": "^10.0.3", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@react-three/postprocessing": "^2.16.0", + "@stripe/react-stripe-js": "^6.1.0", + "@stripe/stripe-js": "^9.1.0", "@types/three": "^0.183.1", + "bcryptjs": "^3.0.3", "framer-motion": "^12.38.0", "gsap": "^3.14.2", "i18next": "^26.0.3", "i18next-browser-languagedetector": "^8.2.1", + "jose": "^6.2.2", "next": "16.2.2", "react": "19.0.0", "react-dom": "19.0.0", "react-i18next": "^17.0.2", + "stripe": "^22.0.1", "three": "^0.170.0", "zustand": "^5.0.12" }, @@ -29,11 +37,13 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/bcryptjs": "^2.4.6", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^6.0.1", "jsdom": "^29.0.2", + "prisma": "^7.7.0", "tailwindcss": "4.2.2", "typescript": "^5.0.0", "vitest": "^4.1.2" @@ -293,6 +303,36 @@ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", "license": "Apache-2.0" }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", @@ -344,6 +384,19 @@ } } }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -860,6 +913,174 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@libsql/client": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.2.tgz", + "integrity": "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q==", + "license": "MIT", + "dependencies": { + "@libsql/core": "^0.17.2", + "@libsql/hrana-client": "^0.9.0", + "js-base64": "^3.7.5", + "libsql": "^0.5.28", + "promise-limit": "^2.7.0" + } + }, + "node_modules/@libsql/core": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.2.tgz", + "integrity": "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g==", + "license": "MIT", + "dependencies": { + "js-base64": "^3.7.5" + } + }, + "node_modules/@libsql/darwin-arm64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", + "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/darwin-x64": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", + "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@libsql/hrana-client": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.9.0.tgz", + "integrity": "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw==", + "license": "MIT", + "dependencies": { + "@libsql/isomorphic-ws": "^0.1.5", + "cross-fetch": "^4.0.0", + "js-base64": "^3.7.5", + "node-fetch": "^3.3.2" + } + }, + "node_modules/@libsql/isomorphic-ws": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", + "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", + "license": "MIT", + "dependencies": { + "@types/ws": "^8.5.4", + "ws": "^8.13.0" + } + }, + "node_modules/@libsql/linux-arm-gnueabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", + "integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm-musleabihf": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", + "integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", + "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-arm64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", + "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-gnu": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", + "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/linux-x64-musl": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", + "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@libsql/win32-x64-msvc": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", + "integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@mediapipe/tasks-vision": { "version": "0.10.17", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", @@ -897,6 +1118,12 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@neon-rs/load": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", + "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "16.2.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", @@ -1041,6 +1268,369 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@prisma/adapter-libsql": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-libsql/-/adapter-libsql-7.7.0.tgz", + "integrity": "sha512-hKPKDIUU91FIC9SfTI3Xga6NGI0W9URmtPoovH94Eighxfx/agjEimTaizivNXgNwW3L/ZY+PceW126jBt3KdA==", + "license": "Apache-2.0", + "dependencies": { + "@libsql/client": "^0.17.0", + "@prisma/driver-adapter-utils": "7.7.0", + "async-mutex": "0.5.0" + } + }, + "node_modules/@prisma/client": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.7.0.tgz", + "integrity": "sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.7.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.7.0.tgz", + "integrity": "sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.7.0.tgz", + "integrity": "sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.7.0.tgz", + "integrity": "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/dev/node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.7.0.tgz", + "integrity": "sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.7.0.tgz", + "integrity": "sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0", + "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "@prisma/fetch-engine": "7.7.0", + "@prisma/get-platform": "7.7.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz", + "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz", + "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.7.0.tgz", + "integrity": "sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0", + "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "@prisma/get-platform": "7.7.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz", + "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@react-spring/animated": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz", @@ -1502,9 +2092,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/@stripe/react-stripe-js": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.1.0.tgz", + "integrity": "sha512-LbKbRv4+wUSHLb5VNxqiYcKaqXPvTju0bJaF0RrzH0h4+aKWDXAk4RzUBcpNxxj8KtjuxICElANs1Li7aTv1IQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=9.0.0 <10.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.1.0.tgz", + "integrity": "sha512-v51LoEfZNiNS/5DcarWPCYgn24w4dqwwALR4GTbMW/N0DDzzj4DgYNoixX6PYvpt6uIJMucGUabn/BHhylggIQ==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1900,6 +2513,13 @@ "license": "MIT", "peer": true }, + "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/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1935,7 +2555,6 @@ "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2002,6 +2621,15 @@ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@use-gesture/core": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", @@ -2172,6 +2800,23 @@ "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", "license": "BSD-3-Clause" }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2217,6 +2862,25 @@ "node": ">=12" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2249,6 +2913,22 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.2.tgz", + "integrity": "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==", + "devOptional": true, + "license": "MIT" + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -2282,6 +2962,35 @@ "ieee754": "^1.2.1" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/camera-controls": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", @@ -2325,12 +3034,68 @@ "node": ">=18" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "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/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2356,6 +3121,57 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/cross-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/cross-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2397,6 +3213,15 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -2418,6 +3243,33 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2428,6 +3280,13 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/detect-gpu": { "version": "5.0.70", "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", @@ -2455,12 +3314,46 @@ "license": "MIT", "peer": true }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", "license": "Apache-2.0" }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -2488,6 +3381,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-module-lexer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", @@ -2515,6 +3421,60 @@ "node": ">=12.0.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2533,12 +3493,64 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "license": "MIT" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/framer-motion": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", @@ -2581,6 +3593,41 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glsl-noise": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", @@ -2591,9 +3638,23 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/gsap": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", @@ -2606,6 +3667,16 @@ "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", "license": "Apache-2.0" }, + "node_modules/hono": { + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -2628,6 +3699,13 @@ "void-elements": "3.1.0" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/i18next": { "version": "26.0.3", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz", @@ -2668,6 +3746,23 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2717,6 +3812,13 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2739,19 +3841,32 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "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==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jsdom": { "version": "29.0.2", @@ -2794,6 +3909,54 @@ } } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/libsql": { + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", + "integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==", + "cpu": [ + "x64", + "arm64", + "wasm32", + "arm" + ], + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "@neon-rs/load": "^0.0.4", + "detect-libc": "2.0.2" + }, + "optionalDependencies": { + "@libsql/darwin-arm64": "0.5.29", + "@libsql/darwin-x64": "0.5.29", + "@libsql/linux-arm-gnueabihf": "0.5.29", + "@libsql/linux-arm-musleabihf": "0.5.29", + "@libsql/linux-arm64-gnu": "0.5.29", + "@libsql/linux-arm64-musl": "0.5.29", + "@libsql/linux-x64-gnu": "0.5.29", + "@libsql/linux-x64-musl": "0.5.29", + "@libsql/win32-x64-msvc": "0.5.29" + } + }, + "node_modules/libsql/node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -3064,6 +4227,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz", @@ -3074,6 +4256,22 @@ "node": "20 || >=22" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3152,6 +4350,27 @@ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/n8ao": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", @@ -3162,6 +4381,19 @@ "three": ">=0.137" } }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3261,6 +4493,85 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3272,6 +4583,13 @@ ], "license": "MIT" }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -3298,7 +4616,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { @@ -3320,6 +4645,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -3349,6 +4686,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/postprocessing": { "version": "6.39.0", "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz", @@ -3380,6 +4731,46 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/prisma": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.7.0.tgz", + "integrity": "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.7.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.7.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/promise-limit": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", + "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", + "license": "ISC" + }, "node_modules/promise-worker-transferable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", @@ -3390,6 +4781,42 @@ "lie": "^3.0.2" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3400,6 +4827,34 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -3471,6 +4926,20 @@ } } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -3485,6 +4954,16 @@ "node": ">=8" } }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3494,6 +4973,16 @@ "node": ">=0.10.0" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", @@ -3528,6 +5017,13 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -3560,6 +5056,12 @@ "node": ">=10" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -3633,6 +5135,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3642,6 +5157,16 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3689,6 +5214,23 @@ "node": ">=8" } }, + "node_modules/stripe": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.0.1.tgz", + "integrity": "sha512-Yw764pZ6s8Xu4CtUZdD5uWOkw6gc9xzO9OKylCuj1gMhMDLbyGbDtaPNNSFE4mB6njYSHESYIVbF1iIzUfAl2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -3798,7 +5340,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -3978,7 +5520,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/use-sync-external-store": { @@ -3999,6 +5540,21 @@ "node": ">= 4" } }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vite": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz", @@ -4181,6 +5737,15 @@ "node": ">=18" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webgl-constants": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", @@ -4259,6 +5824,27 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -4276,6 +5862,17 @@ "dev": true, "license": "MIT" }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, "node_modules/zustand": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", diff --git a/package.json b/package.json index 2e418da..d4e2ac6 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,27 @@ "test:watch": "vitest" }, "dependencies": { + "@libsql/client": "^0.17.2", + "@prisma/adapter-libsql": "^7.7.0", + "@prisma/client": "^7.7.0", "@react-spring/three": "^10.0.3", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@react-three/postprocessing": "^2.16.0", + "@stripe/react-stripe-js": "^6.1.0", + "@stripe/stripe-js": "^9.1.0", "@types/three": "^0.183.1", + "bcryptjs": "^3.0.3", "framer-motion": "^12.38.0", "gsap": "^3.14.2", "i18next": "^26.0.3", "i18next-browser-languagedetector": "^8.2.1", + "jose": "^6.2.2", "next": "16.2.2", "react": "19.0.0", "react-dom": "19.0.0", "react-i18next": "^17.0.2", + "stripe": "^22.0.1", "three": "^0.170.0", "zustand": "^5.0.12" }, @@ -32,11 +40,13 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/bcryptjs": "^2.4.6", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^6.0.1", "jsdom": "^29.0.2", + "prisma": "^7.7.0", "tailwindcss": "4.2.2", "typescript": "^5.0.0", "vitest": "^4.1.2" diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..47b92ff --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: "file:./prisma/lootah.db", + }, +}); diff --git a/prisma/lootah.db b/prisma/lootah.db new file mode 100644 index 0000000000000000000000000000000000000000..494bb18c6b9da7276be3340ef6cf261a44413634 GIT binary patch literal 32768 zcmeI)O>f#r7zc2>B#=a-=s)ty*fX~0`C%cA$8x<|ZbzQrANM*m5)|$y zj_0{w1%czZB%2r59H%*!8O?Fx2bcjh3z`Smp~$s61zLqg)pDYo$G-b|CDU2ahKQv^%ur6=_5- zRXs0udLE5D*N8-+$gXRZ_RP;`E1J)YM%fa!8nv2Pv6`dOx@GL`FRlNQN@ll~d9K}c zz0>gZ5$jDgX+Jt1Z#Uy+H>EhY5YDHvg%y5ye_X?GJMC^G^m@%cTXbp1YrgW{PWL9y z6&jUNwP6aSO3{2Fh|}T=2Z_S-%1oK~NQ_S@t`AoesqE@1f0iC&8$nR_qNv?%g)`Z? zi{vvUQ+3w1OMF*IzoAEc?^3mM<$mB^tGu+X&Ly*@)$1&0+S8O~Hs*(a@~P~~3jf#c zB)FJ#%5>*aU~z?MSnV&kf2Noh@3iy40vd)V$31t4hKE;&{&hFl7r9Ayl38=*TDLe{ zrWR)t?9XJ}w@csu+P}ZlWFl)%NbZTGSdy}-xd6S7IjjfG1rE~ zV-petAOHafKmY;|fB*y_009U<00KX*z=Ooo#cLluOveAKT&DWt28fnJ00Izz00bZa z0SG_<0uX=z1irh#@G(FC@GQOk@+4}89((COQVUcqC`2XHb$r{`bFQzbM3Nod^JSG~ zHBHOwlH_W#E9YIE__pKdzO3suRW#ShI~vU^d5t)ZD{GES3#zXZYP*i@t4_}LsP4+L zu1Zu@x0;BPN^#5=14~)cN literal 0 HcmV?d00001 diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..a801c90 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,28 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "sqlite" +} + +model AdminUser { + id String @id @default(cuid()) + username String @unique + passwordHash String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model AppSettings { + key String @id + value String + updatedAt DateTime @updatedAt +} + +model Snapshot { + paymentIntentId String @id + imageData String + createdAt DateTime @default(now()) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..8600244 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,60 @@ +import { PrismaClient } from '../src/generated/prisma/client.js'; +import { PrismaLibSql } from '@prisma/adapter-libsql'; +import bcrypt from 'bcryptjs'; +import { randomBytes } from 'crypto'; +import path from 'path'; + +const dbPath = path.resolve(process.cwd(), 'prisma', 'lootah.db'); +const adapter = new PrismaLibSql({ url: `file:${dbPath}` }); +const prisma = new PrismaClient({ adapter } as ConstructorParameters[0]); + +async function main() { + console.log('Seeding database...'); + + // Create default admin user + const existingAdmin = await prisma.adminUser.findUnique({ + where: { username: 'admin' }, + }); + + if (!existingAdmin) { + const passwordHash = await bcrypt.hash('admin123', 12); + await prisma.adminUser.create({ + data: { + username: 'admin', + passwordHash, + }, + }); + console.log('✓ Created admin user (username: admin, password: admin123)'); + console.log(' ⚠️ Change the password after first login!'); + } else { + console.log('✓ Admin user already exists, skipping.'); + } + + // Generate and store JWT secret + const existingSecret = await prisma.appSettings.findUnique({ + where: { key: 'jwt_secret' }, + }); + + if (!existingSecret) { + const jwtSecret = randomBytes(64).toString('hex'); + await prisma.appSettings.upsert({ + where: { key: 'jwt_secret' }, + update: { value: jwtSecret }, + create: { key: 'jwt_secret', value: jwtSecret }, + }); + console.log('✓ Generated JWT secret and stored in database.'); + } else { + console.log('✓ JWT secret already exists, skipping.'); + } + + console.log('Seeding complete!'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx new file mode 100644 index 0000000..e93b842 --- /dev/null +++ b/src/app/admin/login/page.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function AdminLoginPage() { + const router = useRouter(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + const res = await fetch('/api/admin/login/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + const data = await res.json(); + if (res.ok) { + router.push('/admin/'); + router.refresh(); + } else { + setError(data.error ?? 'Login failed'); + } + } catch { + setError('Network error. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ 🔐 +
+

+ Admin Login +

+

+ Lootah Robotics — G1 Configurator +

+
+ +
+
+ + setUsername(e.target.value)} + style={inputStyle} + onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')} + onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')} + placeholder="admin" + /> +
+
+ + setPassword(e.target.value)} + style={inputStyle} + onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')} + onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')} + placeholder="••••••••" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+ ); +} + +const pageStyle: React.CSSProperties = { + minHeight: '100vh', + background: '#ffffff', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '2rem', + fontFamily: 'system-ui, -apple-system, sans-serif', +}; + +const cardStyle: React.CSSProperties = { + width: '100%', + maxWidth: '380px', + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(20px)', + border: '1px solid rgba(0, 0, 0, 0.06)', + borderRadius: '1rem', + padding: '2rem', +}; + +const labelStyle: React.CSSProperties = { + display: 'block', + fontSize: '0.75rem', + fontWeight: 600, + color: '#374151', + marginBottom: '0.375rem', + letterSpacing: '0.02em', +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '0.6rem 0.875rem', + borderRadius: '0.5rem', + border: '1px solid rgba(0, 0, 0, 0.1)', + background: '#ffffff', + color: '#1a1a2e', + fontSize: '0.875rem', + outline: 'none', + transition: 'border-color 0.2s ease', + boxSizing: 'border-box', +}; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 10ebe43..61d8caf 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,208 +1,526 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { pricingStore, usePricingStore } from '@/store/usePricingStore'; +import { personaStore, usePersonaStore } from '@/store/usePersonaStore'; +import { useRouter } from 'next/navigation'; import Link from 'next/link'; -export default function AdminPage() { - const items = usePricingStore((s) => s.items); - const isHydrated = usePricingStore((s) => s.isHydrated); - const [editedPrices, setEditedPrices] = useState>({}); - const [saved, setSaved] = useState(false); +interface Order { + id: string; + amount: number; + currency: string; + status: string; + created: number; + metadata: Record; +} - useEffect(() => { - pricingStore.getState().hydrate(); - }, []); +type Tab = 'pricing' | 'personas' | 'orders'; + +export default function AdminPage() { + const router = useRouter(); + const [activeTab, setActiveTab] = useState('pricing'); + + // --------------- PRICING --------------- + const items = usePricingStore((s) => s.items); + const isPricingHydrated = usePricingStore((s) => s.isHydrated); + const [editedPrices, setEditedPrices] = useState>({}); + const [priceSaved, setPriceSaved] = useState(false); + const [newItem, setNewItem] = useState({ id: '', label: '', price: '' }); + const [addItemError, setAddItemError] = useState(''); + + useEffect(() => { pricingStore.getState().hydrate(); }, []); useEffect(() => { const map: Record = {}; - items.forEach((item) => { - map[item.id] = item.price; - }); + items.forEach((item) => { map[item.id] = item.price; }); setEditedPrices(map); }, [items]); const handlePriceChange = (id: string, value: string) => { const num = parseInt(value.replace(/[^0-9]/g, ''), 10); - if (!isNaN(num)) { - setEditedPrices((prev) => ({ ...prev, [id]: num })); - } else if (value === '') { - setEditedPrices((prev) => ({ ...prev, [id]: 0 })); - } + setEditedPrices((prev) => ({ ...prev, [id]: isNaN(num) ? 0 : num })); }; - const handleSave = () => { + const handleSavePrices = () => { Object.entries(editedPrices).forEach(([id, price]) => { pricingStore.getState().updatePrice(id, price); }); - setSaved(true); - setTimeout(() => setSaved(false), 2000); + setPriceSaved(true); + setTimeout(() => setPriceSaved(false), 2000); }; - const handleReset = () => { - pricingStore.getState().resetPrices(); - setSaved(false); + const handleAddItem = () => { + setAddItemError(''); + const id = newItem.id.trim().toLowerCase().replace(/\s+/g, '-'); + const label = newItem.label.trim(); + const price = parseInt(newItem.price, 10); + if (!id || !label || isNaN(price) || price < 0) { + setAddItemError('Please fill all fields with valid values.'); + return; + } + if (items.some((i) => i.id === id)) { + setAddItemError(`ID "${id}" already exists.`); + return; + } + pricingStore.getState().addItem({ id, label, price }); + setNewItem({ id: '', label: '', price: '' }); }; - const formatPrice = (price: number) => { - return new Intl.NumberFormat('en-AE', { style: 'decimal' }).format(price); + // --------------- PERSONAS --------------- + const personas = usePersonaStore((s) => s.personas); + const isPersonaHydrated = usePersonaStore((s) => s.isHydrated); + const [newPersona, setNewPersona] = useState({ label: '', description: '', torso: '#3b82f6', legs: '#3b82f6' }); + const [personaError, setPersonaError] = useState(''); + const [personaSaved, setPersonaSaved] = useState(false); + + useEffect(() => { personaStore.getState().hydrate(); }, []); + + const handleAddPersona = () => { + setPersonaError(''); + const label = newPersona.label.trim(); + const description = newPersona.description.trim(); + if (!label || !description) { + setPersonaError('Label and description are required.'); + return; + } + personaStore.getState().addPersona({ + label, + description, + colors: { torso: newPersona.torso, legs: newPersona.legs }, + }); + setNewPersona({ label: '', description: '', torso: '#3b82f6', legs: '#3b82f6' }); + setPersonaSaved(true); + setTimeout(() => setPersonaSaved(false), 2000); }; - if (!isHydrated) { - return ( -
-

Loading...

-
- ); + // --------------- ORDERS --------------- + const [orders, setOrders] = useState([]); + const [ordersLoading, setOrdersLoading] = useState(false); + const [ordersError, setOrdersError] = useState(''); + const [totalRevenue, setTotalRevenue] = useState(0); + + const loadOrders = useCallback(async () => { + setOrdersLoading(true); + setOrdersError(''); + try { + const res = await fetch('/api/admin/orders/'); + if (!res.ok) throw new Error('Failed to load orders'); + const data = await res.json(); + setOrders(data.orders ?? []); + const succeeded = (data.orders ?? []).filter((o: Order) => o.status === 'succeeded'); + setTotalRevenue(succeeded.reduce((sum: number, o: Order) => sum + o.amount, 0)); + } catch (err) { + setOrdersError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setOrdersLoading(false); + } + }, []); + + useEffect(() => { + if (activeTab === 'orders') loadOrders(); + }, [activeTab, loadOrders]); + + // --------------- AUTH --------------- + const handleLogout = async () => { + await fetch('/api/admin/logout/', { method: 'POST' }); + router.push('/admin/login/'); + router.refresh(); + }; + + // --------------- CHANGE PASSWORD --------------- + const [showPwModal, setShowPwModal] = useState(false); + const [pwForm, setPwForm] = useState({ current: '', next: '', confirm: '' }); + const [pwError, setPwError] = useState(''); + const [pwSaved, setPwSaved] = useState(false); + + const handleChangePassword = async () => { + setPwError(''); + if (pwForm.next !== pwForm.confirm) { setPwError('Passwords do not match.'); return; } + if (pwForm.next.length < 8) { setPwError('Password must be at least 8 characters.'); return; } + const res = await fetch('/api/admin/change-password/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword: pwForm.current, newPassword: pwForm.next }), + }); + const data = await res.json(); + if (res.ok) { + setPwSaved(true); + setTimeout(() => { setPwSaved(false); setShowPwModal(false); setPwForm({ current: '', next: '', confirm: '' }); }, 1500); + } else { + setPwError(data.error ?? 'Failed to update password.'); + } + }; + + const formatPrice = (price: number) => + new Intl.NumberFormat('en-AE', { style: 'decimal' }).format(price); + const formatAmount = (amount: number, currency: string) => + new Intl.NumberFormat('en-AE', { style: 'currency', currency: currency.toUpperCase(), maximumFractionDigits: 0 }).format(amount / 100); + const formatDate = (ts: number) => + new Date(ts * 1000).toLocaleDateString('en-AE', { day: 'numeric', month: 'short', year: 'numeric' }); + + if (!isPricingHydrated) { + return

Loading…

; } return (
- {/* Header */} -
+ {/* HEADER */} +
-

- Pricing Dashboard +

+ Admin Dashboard

-

- Edit prices for the G1 Robot Configurator -

+

Lootah Robotics G1 Configurator

+
+
+ + + ← Configurator
- - Back to Configurator -
- {/* Pricing Table */} -
- {/* Table Header */} -
- - Item - - - Price (AED) - -
+ {/* ANALYTICS STRIP */} +
+ + + 0 ? formatAmount(totalRevenue, 'aed') : '—'} /> +
- {/* Rows */} - {items.map((item, index) => ( -
+ {(['pricing', 'personas', 'orders'] as Tab[]).map((t) => ( +
+ {t} + ))}
- {/* Actions */} -
- - -
+ {/* ===== PRICING TAB ===== */} + {activeTab === 'pricing' && ( +
+ + + {items.map((item, i) => ( +
+
+
{item.label}
+
{item.id}
+
+
+ AED + handlePriceChange(item.id, e.target.value)} + style={tableInputStyle} + onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59,130,246,0.5)')} + onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0,0,0,0.1)')} + aria-label={`Price for ${item.label}`} + /> +
+
+ {item.id !== 'base' && ( + + )} +
+
+ ))} +
+ + {/* Add new pricing item */} +
+

Add Pricing Item

+
+
+ + setNewItem((p) => ({ ...p, id: e.target.value }))} /> +
+
+ + setNewItem((p) => ({ ...p, label: e.target.value }))} /> +
+
+ + setNewItem((p) => ({ ...p, price: e.target.value }))} /> +
+
+ {addItemError &&

{addItemError}

} +
+ + + +
+
+
+ )} + + {/* ===== PERSONAS TAB ===== */} + {activeTab === 'personas' && ( +
+ {!isPersonaHydrated ? ( +

Loading personas…

+ ) : ( + + + {personas.map((p, i) => ( +
+
+
{p.label}
+
{p.description}
+
+
+
+ {p.colors.torso} +
+
+
+ {p.colors.legs} +
+
+ {p.id !== 'none' && ( + + )} +
+
+ ))} + + )} + + {/* Add new persona */} +
+

Add Persona

+
+
+ + setNewPersona((p) => ({ ...p, label: e.target.value }))} /> +
+
+ + setNewPersona((p) => ({ ...p, description: e.target.value }))} /> +
+
+ +
+ setNewPersona((p) => ({ ...p, torso: e.target.value }))} style={{ width: 36, height: 32, border: '1px solid rgba(0,0,0,0.1)', borderRadius: 4, cursor: 'pointer', padding: 2 }} /> + setNewPersona((p) => ({ ...p, torso: e.target.value }))} /> +
+
+
+ +
+ setNewPersona((p) => ({ ...p, legs: e.target.value }))} style={{ width: 36, height: 32, border: '1px solid rgba(0,0,0,0.1)', borderRadius: 4, cursor: 'pointer', padding: 2 }} /> + setNewPersona((p) => ({ ...p, legs: e.target.value }))} /> +
+
+
+ {personaError &&

{personaError}

} +
+ + +
+
+
+ )} + + {/* ===== ORDERS TAB ===== */} + {activeTab === 'orders' && ( +
+
+ +
+ {ordersError &&

{ordersError}

} + {!ordersLoading && orders.length === 0 && !ordersError && ( +

No orders found.

+ )} + {orders.length > 0 && ( + + + {orders.map((order, i) => ( + + ))} + + )} +
+ )}
+ + {/* CHANGE PASSWORD MODAL */} + {showPwModal && ( +
+
+

Change Password

+
+ {(['current', 'next', 'confirm'] as const).map((field) => ( +
+ + setPwForm((p) => ({ ...p, [field]: e.target.value }))} + style={formInputStyle} + /> +
+ ))} + {pwError &&

{pwError}

} + {pwSaved &&

Password updated!

} +
+
+ + +
+
+
+ )}
); } +// ===== SUB-COMPONENTS ===== + +function OrderRow({ + order, + isLast, + formatAmount, + formatDate, +}: { + order: Order; + isLast: boolean; + formatAmount: (amount: number, currency: string) => string; + formatDate: (ts: number) => string; +}) { + const [expanded, setExpanded] = useState(false); + const m = order.metadata; + const name = m.customerName || '—'; + const email = m.customerEmail || ''; + const phone = m.customerPhone || ''; + const address = [m.customerAddress, m.customerCity, m.customerCountry, m.customerPostalCode] + .filter(Boolean).join(', ') || ''; + const persona = m.persona || ''; + const color = m.color || ''; + + return ( +
+ {/* Main row */} +
+
+
{name}
+ {email &&
{email}
} +
{order.id}
+
+
{formatAmount(order.amount, order.currency)}
+
+ + {order.status} + +
+
{formatDate(order.created)}
+ +
+ + {/* Expanded customer details */} + {expanded && ( +
+
+ {[ + { label: 'Full Name', value: m.customerName }, + { label: 'Email', value: m.customerEmail }, + { label: 'Phone', value: m.customerPhone }, + { label: 'Address', value: address }, + { label: 'Persona', value: persona }, + { label: 'Color', value: color }, + ].map(({ label, value }) => + value ? ( +
+
{label}
+
{value}
+
+ ) : null + )} +
+
+ )} +
+ ); +} + +function StatCard({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function TableCard({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function TableHeader({ cols, labels }: { cols: string; labels: string[] }) { + return ( +
+ {labels.map((l) => ( + {l} + ))} +
+ ); +} + +// ===== STYLES ===== + const pageStyle: React.CSSProperties = { minHeight: '100vh', background: '#ffffff', display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', justifyContent: 'center', padding: '2rem', fontFamily: 'system-ui, -apple-system, sans-serif', @@ -210,5 +528,102 @@ const pageStyle: React.CSSProperties = { const containerStyle: React.CSSProperties = { width: '100%', - maxWidth: '640px', + maxWidth: '860px', + paddingTop: '1rem', +}; + +const labelStyle: React.CSSProperties = { + display: 'block', + fontSize: '0.7rem', + fontWeight: 600, + color: '#374151', + marginBottom: '0.25rem', + letterSpacing: '0.02em', +}; + +const formInputStyle: React.CSSProperties = { + width: '100%', + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + border: '1px solid rgba(0,0,0,0.1)', + background: '#ffffff', + color: '#1a1a2e', + fontSize: '0.8rem', + outline: 'none', + boxSizing: 'border-box', +}; + +const tableInputStyle: React.CSSProperties = { + width: '120px', + padding: '0.4rem 0.6rem', + borderRadius: '0.375rem', + border: '1px solid rgba(0,0,0,0.1)', + background: '#ffffff', + color: '#1a1a2e', + fontSize: '0.8rem', + fontFamily: 'monospace', + textAlign: 'right' as const, + outline: 'none', +}; + +const ghostBtnStyle: React.CSSProperties = { + padding: '0.4rem 0.875rem', + borderRadius: '0.375rem', + border: '1px solid rgba(0,0,0,0.1)', + background: 'transparent', + color: '#374151', + fontSize: '0.75rem', + cursor: 'pointer', +}; + +const primaryBtnStyle: React.CSSProperties = { + padding: '0.5rem 1.25rem', + borderRadius: '0.375rem', + border: '1px solid rgba(59,130,246,0.3)', + background: 'rgba(59,130,246,0.08)', + color: '#2563eb', + fontSize: '0.8rem', + fontWeight: 600, + cursor: 'pointer', +}; + +const secondaryBtnStyle: React.CSSProperties = { + padding: '0.5rem 1rem', + borderRadius: '0.375rem', + border: '1px solid rgba(0,0,0,0.1)', + background: 'rgba(248,248,246,0.8)', + color: '#374151', + fontSize: '0.8rem', + cursor: 'pointer', +}; + +const dangerBtnStyle: React.CSSProperties = { + padding: '0.5rem 1rem', + borderRadius: '0.375rem', + border: '1px solid rgba(239,68,68,0.2)', + background: 'rgba(239,68,68,0.05)', + color: '#ef4444', + fontSize: '0.8rem', + cursor: 'pointer', +}; + +const deleteBtnStyle: React.CSSProperties = { + width: '24px', + height: '24px', + borderRadius: '50%', + border: '1px solid rgba(239,68,68,0.2)', + background: 'rgba(239,68,68,0.06)', + color: '#ef4444', + fontSize: '0.65rem', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 0, +}; + +const errorTextStyle: React.CSSProperties = { + color: '#dc2626', + fontSize: '0.75rem', + margin: '0.5rem 0 0', }; diff --git a/src/app/api/admin/change-password/route.ts b/src/app/api/admin/change-password/route.ts new file mode 100644 index 0000000..f43ff8d --- /dev/null +++ b/src/app/api/admin/change-password/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { jwtVerify } from 'jose'; +import bcrypt from 'bcryptjs'; +import { prisma } from '@/lib/prisma'; + +export async function POST(request: Request) { + try { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const jwtSecret = process.env.ADMIN_JWT_SECRET; + if (!jwtSecret) return NextResponse.json({ error: 'Server error' }, { status: 500 }); + + const secret = new TextEncoder().encode(jwtSecret); + const { payload } = await jwtVerify(token, secret); + + const { currentPassword, newPassword } = await request.json(); + if (!currentPassword || !newPassword || newPassword.length < 8) { + return NextResponse.json({ error: 'Invalid input. New password must be at least 8 characters.' }, { status: 400 }); + } + + const user = await prisma.adminUser.findUnique({ where: { id: payload.sub as string } }); + if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 }); + + const valid = await bcrypt.compare(currentPassword, user.passwordHash); + if (!valid) return NextResponse.json({ error: 'Current password is incorrect' }, { status: 401 }); + + const newHash = await bcrypt.hash(newPassword, 12); + await prisma.adminUser.update({ where: { id: user.id }, data: { passwordHash: newHash } }); + + return NextResponse.json({ success: true }); + } catch { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/login/route.ts b/src/app/api/admin/login/route.ts new file mode 100644 index 0000000..2524c35 --- /dev/null +++ b/src/app/api/admin/login/route.ts @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server'; +import bcrypt from 'bcryptjs'; +import { SignJWT } from 'jose'; +import { prisma } from '@/lib/prisma'; + +export async function POST(request: Request) { + try { + const { username, password } = await request.json(); + + if (!username || !password) { + return NextResponse.json({ error: 'Username and password are required' }, { status: 400 }); + } + + const user = await prisma.adminUser.findUnique({ where: { username } }); + if (!user) { + return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); + } + + const valid = await bcrypt.compare(password, user.passwordHash); + if (!valid) { + return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); + } + + const jwtSecret = process.env.ADMIN_JWT_SECRET; + if (!jwtSecret) { + return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); + } + + const secret = new TextEncoder().encode(jwtSecret); + const token = await new SignJWT({ sub: user.id, username: user.username }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(secret); + + const response = NextResponse.json({ success: true }); + response.cookies.set('admin_token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 7 days + path: '/', + }); + + return response; + } catch { + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/src/app/api/admin/logout/route.ts b/src/app/api/admin/logout/route.ts new file mode 100644 index 0000000..bf58eb1 --- /dev/null +++ b/src/app/api/admin/logout/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server'; + +export async function POST() { + const response = NextResponse.json({ success: true }); + response.cookies.set('admin_token', '', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 0, + path: '/', + }); + return response; +} diff --git a/src/app/api/admin/orders/route.ts b/src/app/api/admin/orders/route.ts new file mode 100644 index 0000000..eb632d3 --- /dev/null +++ b/src/app/api/admin/orders/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server'; +import Stripe from 'stripe'; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2026-03-25.dahlia' as Parameters[1]['apiVersion'], +}); + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const limit = Math.min(parseInt(searchParams.get('limit') ?? '20', 10), 100); + + try { + const paymentIntents = await stripe.paymentIntents.list({ + limit, + expand: ['data.latest_charge'], + }); + + const orders = paymentIntents.data.map((pi) => ({ + id: pi.id, + amount: pi.amount, + currency: pi.currency, + status: pi.status, + created: pi.created, + metadata: pi.metadata, + })); + + return NextResponse.json({ orders, hasMore: paymentIntents.has_more }); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/admin/verify/route.ts b/src/app/api/admin/verify/route.ts new file mode 100644 index 0000000..72db89a --- /dev/null +++ b/src/app/api/admin/verify/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { jwtVerify } from 'jose'; + +export async function GET() { + try { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + + if (!token) { + return NextResponse.json({ authenticated: false }, { status: 401 }); + } + + const jwtSecret = process.env.ADMIN_JWT_SECRET; + if (!jwtSecret) { + return NextResponse.json({ authenticated: false }, { status: 500 }); + } + + const secret = new TextEncoder().encode(jwtSecret); + await jwtVerify(token, secret); + + return NextResponse.json({ authenticated: true }); + } catch { + return NextResponse.json({ authenticated: false }, { status: 401 }); + } +} diff --git a/src/app/api/create-payment-intent/route.ts b/src/app/api/create-payment-intent/route.ts new file mode 100644 index 0000000..c374884 --- /dev/null +++ b/src/app/api/create-payment-intent/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server'; +import Stripe from 'stripe'; + +if (!process.env.STRIPE_SECRET_KEY) { + console.error('STRIPE_SECRET_KEY is not set in environment variables'); +} + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { + apiVersion: '2026-03-25.dahlia', +}); + +export async function POST(request: Request) { + try { + const { amount, currency, metadata, receiptEmail } = await request.json(); + + if (!amount || typeof amount !== 'number' || amount <= 0) { + return NextResponse.json({ error: 'Invalid amount' }, { status: 400 }); + } + + // Stripe expects amount in the smallest currency unit (fils for AED) + const amountInFils = Math.round(amount * 100); + + const paymentIntent = await stripe.paymentIntents.create({ + amount: amountInFils, + currency: currency || 'aed', + metadata: metadata || {}, + ...(receiptEmail ? { receipt_email: receiptEmail } : {}), + automatic_payment_methods: { enabled: true }, + }); + + return NextResponse.json({ + clientSecret: paymentIntent.client_secret, + paymentIntentId: paymentIntent.id, + }); + } catch (err) { + const message = err instanceof Error ? err.message : 'Internal server error'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/components/CheckoutOverlay.tsx b/src/components/CheckoutOverlay.tsx index c51199f..9095899 100644 --- a/src/components/CheckoutOverlay.tsx +++ b/src/components/CheckoutOverlay.tsx @@ -1,12 +1,16 @@ 'use client'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements } from '@stripe/react-stripe-js'; import { useOrderStore, orderStore, type CheckoutStep } from '@/store/useOrderStore'; import { ShippingStep } from './checkout/ShippingStep'; import { PaymentStep } from './checkout/PaymentStep'; import { ReviewStep } from './checkout/ReviewStep'; import { ConfirmationStep } from './checkout/ConfirmationStep'; +const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); + const STEPS: { id: CheckoutStep; label: string }[] = [ { id: 'shipping', label: 'Shipping' }, { id: 'payment', label: 'Payment' }, @@ -19,6 +23,10 @@ function getStepIndex(step: CheckoutStep): number { export function CheckoutOverlay() { const step = useOrderStore((s) => s.step); + const clientSecret = useOrderStore((s) => s.payment.clientSecret); + const paymentStatus = useOrderStore((s) => s.payment.status); + const paymentError = useOrderStore((s) => s.payment.errorMessage); + const [isCreatingIntent, setIsCreatingIntent] = useState(false); const handleClose = useCallback(() => { orderStore.getState().resetOrder(); @@ -33,6 +41,16 @@ export function CheckoutOverlay() { } }, [step]); + // Create payment intent when entering the payment step (only once) + useEffect(() => { + if (step === 'payment' && !clientSecret && !isCreatingIntent && paymentStatus !== 'failed') { + setIsCreatingIntent(true); + orderStore.getState().createPaymentIntent().finally(() => { + setIsCreatingIntent(false); + }); + } + }, [step, clientSecret, isCreatingIntent, paymentStatus]); + // Close on Escape key useEffect(() => { if (step === 'config') return; @@ -153,8 +171,55 @@ export function CheckoutOverlay() { {/* Step Content */}
{step === 'shipping' && } - {step === 'payment' && } - {step === 'review' && } + {(step === 'payment' || step === 'review') && clientSecret ? ( + + {/* Keep PaymentElement mounted (hidden) during review so confirmPayment works */} +
+ +
+ {step === 'review' && } +
+ ) : (step === 'payment' || step === 'review') && !clientSecret ? ( +
+ {paymentStatus === 'failed' ? ( +
+
{paymentError || 'Failed to initialize payment'}
+ +
+ ) : ( + Initializing secure payment... + )} +
+ ) : null} {step === 'confirmed' && }
diff --git a/src/components/ConfigPanel.tsx b/src/components/ConfigPanel.tsx index a68eff9..59ed731 100644 --- a/src/components/ConfigPanel.tsx +++ b/src/components/ConfigPanel.tsx @@ -1,44 +1,22 @@ 'use client'; -import { useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { configStore, useConfigStore } from '@/store/useConfigStore'; +import { personaStore, usePersonaStore } from '@/store/usePersonaStore'; import { PricingEngine } from './PricingEngine'; -// Persona attire options with visual metadata -const PERSONA_OPTIONS = [ - { - id: 'none', - label: 'Default', - description: 'Original robot appearance', - colors: { torso: '#3b82f6', legs: '#3b82f6' }, - }, - { - id: 'emarati-kandura', - label: 'Emarati Kandura', - description: 'Traditional white robe attire', - colors: { torso: '#f8fafc', legs: '#f8fafc' }, - }, - { - id: 'industrial-vest', - label: 'Industrial Vest', - description: 'High-visibility safety vest', - colors: { torso: '#f59e0b', legs: '#3b82f6' }, - }, - { - id: 'business-suit', - label: 'Business Suit', - description: 'Professional navy suit', - colors: { torso: '#1e293b', legs: '#1e293b' }, - }, -] as const; - export function ConfigPanel() { const activeColors = useConfigStore((s) => s.activeColors); const activePersona = useConfigStore((s) => s.activePersonaAttire); + const personas = usePersonaStore((s) => s.personas); const colorsSectionRef = useRef(null); const personaSectionRef = useRef(null); + useEffect(() => { + personaStore.getState().hydrate(); + }, []); + const handleColorChange = useCallback((key: 'primary' | 'secondary' | 'accent', value: string) => { configStore.getState().setColors({ [key]: value }); }, []); @@ -80,7 +58,7 @@ export function ConfigPanel() { >

Persona Attire

- {PERSONA_OPTIONS.map((persona) => { + {personas.map((persona) => { const isActive = activePersona === persona.id; return (
); diff --git a/src/components/checkout/ReviewStep.tsx b/src/components/checkout/ReviewStep.tsx index 158beb0..87e817f 100644 --- a/src/components/checkout/ReviewStep.tsx +++ b/src/components/checkout/ReviewStep.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { useStripe, useElements } from '@stripe/react-stripe-js'; import { useOrderStore, orderStore } from '@/store/useOrderStore'; function formatAED(price: number): string { @@ -8,16 +9,41 @@ function formatAED(price: number): string { } export function ReviewStep() { + const stripe = useStripe(); + const elements = useElements(); const shipping = useOrderStore((s) => s.shipping); const orderTotal = useOrderStore((s) => s.orderTotal); const personaSummary = useOrderStore((s) => s.personaSummary); const colorSummary = useOrderStore((s) => s.colorSummary); + const clientSecret = useOrderStore((s) => s.payment.clientSecret); const [isProcessing, setIsProcessing] = useState(false); + const [errorMsg, setErrorMsg] = useState(''); const handlePlaceOrder = async () => { + if (!stripe || !elements || !clientSecret) return; + setIsProcessing(true); - // Simulate payment processing delay - await new Promise((resolve) => setTimeout(resolve, 1500)); + setErrorMsg(''); + + const { error } = await stripe.confirmPayment({ + elements, + clientSecret, + confirmParams: { + return_url: `${window.location.origin}?order=confirmed`, + receipt_email: shipping.email, + }, + redirect: 'if_required', + }); + + if (error) { + setErrorMsg(error.message || 'Payment failed. Please try again.'); + orderStore.getState().setPayment({ status: 'failed', errorMessage: error.message || '' }); + setIsProcessing(false); + return; + } + + // Payment succeeded + orderStore.getState().setPayment({ status: 'succeeded' }); orderStore.getState().placeOrder(); }; @@ -69,10 +95,24 @@ export function ReviewStep() {
+ {/* Error message */} + {errorMsg && ( +
+ {errorMsg} +
+ )} + {/* Place Order Button */}