From 8dd43c57cad2382dfe80a33a4505bcbdbb8cdbe4 Mon Sep 17 00:00:00 2001 From: Hikmet Date: Tue, 7 Jul 2026 20:40:43 +0300 Subject: [PATCH] Initial WinningHunter MVP --- .env.example | 5 + .env.production.example | 9 + .gitignore | 7 + Dockerfile | 28 + README.md | 108 + docker-compose.yml | 35 + next.config.mjs | 10 + package-lock.json | 2736 +++++++++++++++++++ package.json | 39 + postcss.config.mjs | 6 + prisma/schema.prisma | 563 ++++ prisma/seed.ts | 502 ++++ src/app/api/admin/ingest/seed-demo/route.ts | 7 + src/app/api/ads/[id]/route.ts | 18 + src/app/api/ads/[id]/save/route.ts | 25 + src/app/api/ads/search/route.ts | 87 + src/app/api/auth/login/route.ts | 18 + src/app/api/auth/logout/route.ts | 7 + src/app/api/auth/me/route.ts | 16 + src/app/api/auth/register/route.ts | 31 + src/app/api/plans/route.ts | 7 + src/app/api/saved-ads/[id]/route.ts | 20 + src/app/api/saved-ads/route.ts | 27 + src/app/api/saved-folders/[id]/route.ts | 20 + src/app/api/saved-folders/route.ts | 17 + src/app/api/stores/[id]/route.ts | 12 + src/app/api/stores/[id]/similar/route.ts | 13 + src/app/api/stores/search/route.ts | 30 + src/app/api/tracked-stores/[id]/route.ts | 11 + src/app/api/tracked-stores/route.ts | 26 + src/app/api/usage/summary/route.ts | 9 + src/app/dashboard/account/page.tsx | 7 + src/app/dashboard/admin/data/page.tsx | 9 + src/app/dashboard/ads/page.tsx | 67 + src/app/dashboard/layout.tsx | 41 + src/app/dashboard/page.tsx | 5 + src/app/dashboard/saved-ads/page.tsx | 23 + src/app/dashboard/store-tracker/page.tsx | 24 + src/app/dashboard/stores/[id]/page.tsx | 59 + src/app/dashboard/stores/page.tsx | 50 + src/app/globals.css | 34 + src/app/layout.tsx | 15 + src/app/login/page.tsx | 36 + src/app/page.tsx | 49 + src/app/pricing/page.tsx | 39 + src/app/register/page.tsx | 37 + src/components/ui/button.tsx | 19 + src/components/ui/card.tsx | 3 + src/lib/auth/crypto.ts | 9 + src/lib/auth/current-user.ts | 18 + src/lib/auth/session.ts | 60 + src/lib/db.ts | 15 + src/lib/locked-response.ts | 16 + src/lib/plans.ts | 31 + src/lib/quota.ts | 117 + tailwind.config.ts | 22 + tsconfig.json | 41 + 57 files changed, 5295 insertions(+) create mode 100644 .env.example create mode 100644 .env.production.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 src/app/api/admin/ingest/seed-demo/route.ts create mode 100644 src/app/api/ads/[id]/route.ts create mode 100644 src/app/api/ads/[id]/save/route.ts create mode 100644 src/app/api/ads/search/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/api/auth/register/route.ts create mode 100644 src/app/api/plans/route.ts create mode 100644 src/app/api/saved-ads/[id]/route.ts create mode 100644 src/app/api/saved-ads/route.ts create mode 100644 src/app/api/saved-folders/[id]/route.ts create mode 100644 src/app/api/saved-folders/route.ts create mode 100644 src/app/api/stores/[id]/route.ts create mode 100644 src/app/api/stores/[id]/similar/route.ts create mode 100644 src/app/api/stores/search/route.ts create mode 100644 src/app/api/tracked-stores/[id]/route.ts create mode 100644 src/app/api/tracked-stores/route.ts create mode 100644 src/app/api/usage/summary/route.ts create mode 100644 src/app/dashboard/account/page.tsx create mode 100644 src/app/dashboard/admin/data/page.tsx create mode 100644 src/app/dashboard/ads/page.tsx create mode 100644 src/app/dashboard/layout.tsx create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/dashboard/saved-ads/page.tsx create mode 100644 src/app/dashboard/store-tracker/page.tsx create mode 100644 src/app/dashboard/stores/[id]/page.tsx create mode 100644 src/app/dashboard/stores/page.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/pricing/page.tsx create mode 100644 src/app/register/page.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/lib/auth/crypto.ts create mode 100644 src/lib/auth/current-user.ts create mode 100644 src/lib/auth/session.ts create mode 100644 src/lib/db.ts create mode 100644 src/lib/locked-response.ts create mode 100644 src/lib/plans.ts create mode 100644 src/lib/quota.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..31c938d --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +NEXT_PUBLIC_APP_URL=http://localhost:3000 +DATABASE_URL="postgresql://ht44@localhost:5432/winninghunter?schema=public" +AUTH_COOKIE_NAME=wh_session +AUTH_SESSION_DAYS=30 +ADMIN_EMAILS="admin@winninghunter.local" diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..8aef295 --- /dev/null +++ b/.env.production.example @@ -0,0 +1,9 @@ +NEXT_PUBLIC_APP_URL=https://your-domain.com +DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/winninghunter?schema=public" +AUTH_COOKIE_NAME=wh_session +AUTH_SESSION_DAYS=30 +ADMIN_EMAILS="admin@winninghunter.local" + +# For docker-compose +POSTGRES_PASSWORD="change-this-long-random-password" +PORT=3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2c7041 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.next +.env +*.log +.DS_Store +next-env.d.ts +/prisma/dev.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..755316a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:22-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:22-alpine AS builder +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate +RUN npm run build + +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/next.config.mjs ./next.config.mjs + +EXPOSE 3000 +CMD ["npm", "run", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..336c987 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# WinningHunter MVP + +WinningHunter klonu için çalışan MVP: + +- Next.js 16 + React 19 +- Prisma + PostgreSQL +- Email/password auth +- Plan/subscription/kota altyapısı +- Meta Ads search +- Store explorer + store detail +- Store tracker +- Saved ads/folders +- Pricing +- Admin data panel + +## Lokal kurulum + +```bash +npm install +cp .env.example .env +createdb winninghunter +npm run db:push +npm run db:seed +npm run dev +``` + +Demo hesaplar: + +```txt +Demo: +demo@winninghunter.local / demo1234 + +Admin: +admin@winninghunter.local / admin1234 +``` + +## Production build + +```bash +npm run build +PORT=3001 npm run start +``` + +Doğrulama: + +```bash +curl http://localhost:3001/ +curl http://localhost:3001/login +``` + +## Docker ile çalıştırma + +```bash +cp .env.production.example .env +docker compose up -d --build +docker compose exec app npm run db:seed +``` + +## Coolify deploy + +1. Yeni proje oluştur. +2. Git repo veya ZIP kaynak olarak bu klasörü bağla. +3. Build type: Dockerfile. +4. Domain: `winninghunter.your-domain.com`. +5. Env: + +```env +NEXT_PUBLIC_APP_URL=https://winninghunter.your-domain.com +DATABASE_URL=postgresql://... +AUTH_COOKIE_NAME=wh_session +AUTH_SESSION_DAYS=30 +ADMIN_EMAILS=admin@winninghunter.local +``` + +6. İlk deploy sonrası terminal: + +```bash +npx prisma db push +npm run db:seed +``` + +## Vercel deploy + +1. Vercel projesi oluştur. +2. PostgreSQL sağlayıcısı bağla: Neon/Supabase/Vercel Postgres. +3. `DATABASE_URL` ve diğer env değerlerini gir. +4. Build command: + +```bash +prisma generate && next build +``` + +5. İlk deploy sonrası lokalden: + +```bash +DATABASE_URL="production-url" npx prisma db push +DATABASE_URL="production-url" npm run db:seed +``` + +## Kalan V1 işleri + +- Brand Tracker UI ve AI tagging job worker +- Magic AI embedding search +- TikTok Shop Explorer +- Trends +- MCP server/API credit dashboard +- Stripe checkout gerçek entegrasyonu +- OpenSearch/pg_trgm ile gelişmiş arama diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dd8d96f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: winninghunter + POSTGRES_USER: winninghunter + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-winninghunter_dev_password} + volumes: + - winninghunter_pg:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U winninghunter -d winninghunter"] + interval: 10s + timeout: 5s + retries: 8 + + app: + build: . + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: production + NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000} + DATABASE_URL: postgresql://winninghunter:${POSTGRES_PASSWORD:-winninghunter_dev_password}@db:5432/winninghunter?schema=public + AUTH_COOKIE_NAME: wh_session + AUTH_SESSION_DAYS: 30 + ADMIN_EMAILS: admin@winninghunter.local + ports: + - "${PORT:-3000}:3000" + command: sh -c "npx prisma db push && npm run start" + +volumes: + winninghunter_pg: diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..38582b7 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + typescript: { ignoreBuildErrors: false }, + turbopack: { + root: process.cwd() + } +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..871aadc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2736 @@ +{ + "name": "winninghunter", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "winninghunter", + "version": "0.1.0", + "dependencies": { + "@prisma/client": "5.22.0", + "bcryptjs": "2.4.3", + "next": "^16.3.0-canary.79", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "zod": "3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "2.4.6", + "@types/node": "^26.1.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "autoprefixer": "10.4.20", + "postcss": "8.4.47", + "prisma": "5.22.0", + "tailwindcss": "3.4.13", + "tsx": "4.19.1", + "typescript": "^6.0.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.0-canary.79.tgz", + "integrity": "sha512-F2U7ygW1qpAFXMLOp2vok9V5VWR2aKoQWX9sOPYpLk4NOlkkdeY5UAt1PDfm6nMPagL81AYEauwSU6N3iwUOng==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.0-canary.79.tgz", + "integrity": "sha512-A8I4zhxR5Dsvcbbnai1Mm/i5LxC/lm66tgI7I2tZbiLindg+lTKF7FwcAFyhnqZR89hNRGlxV0ZpCWPeQNZWEw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.0-canary.79.tgz", + "integrity": "sha512-28a+1M3+MLUgGZqZC64vqjdonSTBNJXt60CNw4ZXCOkxPfzvp0wkvKjqL8ZYdLBBHBwTC7tUlmeEsnl7PWTXHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.0-canary.79.tgz", + "integrity": "sha512-MJ0oEhaFxnJ8LBK1zNelNqKQph+ZaWeqLpYOcFdl/aXM4PHHbv+5M5KEVKrbbdjwM2t+wdXmgQ2iozyUbdpaPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.0-canary.79.tgz", + "integrity": "sha512-CgjUs9aeJED98Zwl+bh+Ufo/M0ddPy+2BmupRt8SMaxJZa2wNNn4vognqPfHKr/pkiqr01X3hyuxNEk0CCyW5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.0-canary.79.tgz", + "integrity": "sha512-s2yPLT03R/gr/+FUMMecjgjsN1gUH0M6twOvIwhEMcL00LUB/q4RlVpHsVfAZ5DT3EV8GLI/XW2qhpJFaRQEyA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.0-canary.79.tgz", + "integrity": "sha512-vhbTok2qmQ/cXl4XPoiI7wVAX0iYPdgGqPaP/YgQf3eyTBfU5Ej57mdsd2LFxD2ObTMzAhAC3VidnC6oFhFnPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.0-canary.79.tgz", + "integrity": "sha512-glPd/6XP6jiflTPSCTNGbMkZ7BbNy29nk9QOainug2nwgRHr2xCPxQQNyxwtedCNKjKqXMhLGX8HWMu8PjPpEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.0-canary.79.tgz", + "integrity": "sha512-m0BfBDihZgAHjgl7tkQVyd4jAyVGi1Y4E9taFSmcPbxqFm2gEwdRQ85xdOUBMe8FDQpwIs0xcv1obulpw3VDdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.388", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.388.tgz", + "integrity": "sha512-Pl/aJaqOOxYxda3vcx1IKSJimwYXHDkEnGn0F+kG2EE68dDtx2uCinaS+Vih8Z91B9t8CSAbiF/HKyWcnXjhzw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.3.0-canary.79", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.0-canary.79.tgz", + "integrity": "sha512-rMWsAkNQ/+adD45tnz05D683vA/+mVBze7flZ2oV3qgSlPc5tviVlTnu18uz4eccO53yIuL8zd5sdvrMb4Us8w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.0-canary.79", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.10", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.0-canary.79", + "@next/swc-darwin-x64": "16.3.0-canary.79", + "@next/swc-linux-arm64-gnu": "16.3.0-canary.79", + "@next/swc-linux-arm64-musl": "16.3.0-canary.79", + "@next/swc-linux-x64-gnu": "16.3.0-canary.79", + "@next/swc-linux-x64-musl": "16.3.0-canary.79", + "@next/swc-win32-arm64-msvc": "16.3.0-canary.79", + "@next/swc-win32-x64-msvc": "16.3.0-canary.79", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", + "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d7cee65 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "winninghunter", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "lint": "next lint", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push", + "db:seed": "tsx prisma/seed.ts", + "db:studio": "prisma studio" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "5.22.0", + "bcryptjs": "2.4.3", + "next": "^16.3.0-canary.79", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "zod": "3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "2.4.6", + "@types/node": "^26.1.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "autoprefixer": "10.4.20", + "postcss": "8.4.47", + "prisma": "5.22.0", + "tailwindcss": "3.4.13", + "tsx": "4.19.1", + "typescript": "^6.0.3" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..ba80730 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..e022084 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,563 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum UserRole { + USER + ADMIN +} + +enum PlanCode { + FREE + BASIC + STANDARD + PREMIUM +} + +enum SubscriptionStatus { + ACTIVE + TRIALING + PAST_DUE + CANCELED + EXPIRED +} + +enum UsagePeriod { + DAILY + MONTHLY + LIFETIME +} + +enum AdSource { + META + ADSPY + PINTEREST + TIKTOK +} + +enum AdStatus { + ACTIVE + INACTIVE + UNKNOWN +} + +enum MediaType { + VIDEO + IMAGE + CAROUSEL + DCT + UNKNOWN +} + +enum AdScore { + ESTABLISHED + HAS_POTENTIAL + UNESTABLISHED + UNKNOWN +} + +enum StorePlatform { + SHOPIFY + WOOCOMMERCE + CUSTOM + UNKNOWN +} + +enum ExportStatus { + QUEUED + PROCESSING + COMPLETED + FAILED +} + +enum IngestStatus { + QUEUED + RUNNING + COMPLETED + FAILED +} + +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String? + name String? + avatarUrl String? + role UserRole @default(USER) + locale String @default("tr") + currency String @default("EUR") + theme String @default("system") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLoginAt DateTime? + + sessions AuthSession[] + subscription Subscription? + usageCounters UsageCounter[] + savedFolders SavedFolder[] + savedAds SavedAd[] + filterPresets FilterPreset[] + trackedStores TrackedStore[] + exportJobs ExportJob[] + apiKeys ApiKey[] + + @@index([email]) +} + +model AuthSession { + id String @id @default(cuid()) + userId String + tokenHash String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([expiresAt]) +} + +model Plan { + id String @id @default(cuid()) + code PlanCode @unique + name String + monthlyPriceEur Int + quarterlyPriceEur Int? + yearlyPriceEur Int? + description String? + features Json + isActive Boolean @default(true) + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + subscriptions Subscription[] +} + +model Subscription { + id String @id @default(cuid()) + userId String @unique + planId String + status SubscriptionStatus @default(ACTIVE) + billingInterval String @default("monthly") + currentPeriodStart DateTime? + currentPeriodEnd DateTime? + cancelAtPeriodEnd Boolean @default(false) + provider String? + providerCustomerId String? + providerSubscriptionId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + plan Plan @relation(fields: [planId], references: [id]) + + @@index([planId]) + @@index([status]) +} + +model UsageCounter { + id String @id @default(cuid()) + userId String + metric String + period UsagePeriod + periodKey String + used Int @default(0) + limit Int? + resetAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, metric, periodKey]) + @@index([userId, metric]) + @@index([periodKey]) +} + +model BrandPage { + id String @id @default(cuid()) + source AdSource @default(META) + externalPageId String? + name String + handle String? + logoUrl String? + pageUrl String? + websiteDomain String? + fbLikes Int? + igFollowers Int? + country String? + niche String? + storeId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + store Store? @relation(fields: [storeId], references: [id], onDelete: SetNull) + ads Ad[] + + @@unique([source, externalPageId]) + @@index([websiteDomain]) + @@index([niche]) + @@index([country]) +} + +model Ad { + id String @id @default(cuid()) + source AdSource @default(META) + externalAdId String? + brandPageId String? + status AdStatus @default(UNKNOWN) + mediaType MediaType @default(UNKNOWN) + adScore AdScore @default(UNKNOWN) + primaryText String? + headline String? + description String? + ctaText String? + landingUrl String? + productUrl String? + language String? + countries String[] @default([]) + niche String? + firstSeenAt DateTime? + lastSeenAt DateTime? + createdAtSource DateTime? + daysRunning Int? + estimatedReachMin Int? + estimatedReachMax Int? + estimatedSpendMin Int? + estimatedSpendMax Int? + estimatedCpm Float? + rank Float? + rankPercentile Float? + rankGrowth String? + raw Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + brandPage BrandPage? @relation(fields: [brandPageId], references: [id], onDelete: SetNull) + creatives AdCreative[] + metricsDaily AdMetricDaily[] + countryStats AdCountryStat[] + aiTags AITag[] + savedBy SavedAd[] + + @@unique([source, externalAdId]) + @@index([source, status]) + @@index([mediaType]) + @@index([niche]) + @@index([language]) + @@index([firstSeenAt]) + @@index([lastSeenAt]) + @@index([daysRunning]) + @@index([rankPercentile]) +} + +model AdCreative { + id String @id @default(cuid()) + adId String + type MediaType + url String + thumbnailUrl String? + width Int? + height Int? + durationSeconds Int? + position Int @default(0) + createdAt DateTime @default(now()) + + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + + @@index([adId]) + @@index([type]) +} + +model AdMetricDaily { + id String @id @default(cuid()) + adId String + date DateTime + reachMin Int? + reachMax Int? + spendMin Int? + spendMax Int? + cpm Float? + rank Float? + rankPercentile Float? + activeCountries Int? + createdAt DateTime @default(now()) + + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + + @@unique([adId, date]) + @@index([date]) +} + +model AdCountryStat { + id String @id @default(cuid()) + adId String + countryCode String + reachMin Int? + reachMax Int? + spendMin Int? + spendMax Int? + sharePercent Float? + + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + + @@unique([adId, countryCode]) + @@index([countryCode]) +} + +model AITag { + id String @id @default(cuid()) + adId String + type String + value String + confidence Float? + explanation String? + model String? + promptVersion String? + createdAt DateTime @default(now()) + + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + + @@index([adId]) + @@index([type, value]) +} + +model Store { + id String @id @default(cuid()) + domain String @unique + name String + logoUrl String? + platform StorePlatform @default(UNKNOWN) + country String? + currency String? + language String? + niche String? + categoryPath String[] @default([]) + shopUrl String? + createdAtSource DateTime? + dataSince DateTime? + shopifyTheme String? + productCount Int? + collectionCount Int? + trustpilotScore Float? + trustpilotReviewCount Int? + monthlyVisits Int? + monthlyVisitGrowth Float? + estRevenue30dMin Int? + estRevenue30dMax Int? + estRevenue24hMin Int? + estRevenue24hMax Int? + trafficCountries Json? + socials Json? + raw Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + brandPages BrandPage[] + products StoreProduct[] + snapshots StoreSnapshot[] + pixels StorePixel[] + apps StoreApp[] + trackedBy TrackedStore[] + + @@index([niche]) + @@index([country]) + @@index([currency]) + @@index([monthlyVisits]) + @@index([monthlyVisitGrowth]) + @@index([estRevenue30dMax]) +} + +model StoreSnapshot { + id String @id @default(cuid()) + storeId String + date DateTime + monthlyVisits Int? + estRevenueMin Int? + estRevenueMax Int? + activeAdsCount Int? + productCount Int? + trafficCountries Json? + createdAt DateTime @default(now()) + + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + + @@unique([storeId, date]) + @@index([date]) +} + +model StoreProduct { + id String @id @default(cuid()) + storeId String + externalProductId String? + title String + handle String? + imageUrl String? + productUrl String? + price Float? + compareAtPrice Float? + currency String? + variantCount Int? + isBestSeller Boolean @default(false) + firstSeenAt DateTime? + raw Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + + @@index([storeId]) + @@index([isBestSeller]) + @@index([price]) +} + +model StorePixel { + id String @id @default(cuid()) + storeId String + type String + value String? + + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + + @@unique([storeId, type, value]) + @@index([type]) +} + +model StoreApp { + id String @id @default(cuid()) + storeId String + name String + category String? + detectedAt DateTime @default(now()) + + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + + @@unique([storeId, name]) + @@index([name]) +} + +model SavedFolder { + id String @id @default(cuid()) + userId String + name String + color String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + savedAds SavedAd[] + + @@unique([userId, name]) + @@index([userId]) +} + +model SavedAd { + id String @id @default(cuid()) + userId String + adId String + folderId String? + note String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + folder SavedFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + + @@unique([userId, adId]) + @@index([userId, folderId]) + @@index([adId]) +} + +model TrackedStore { + id String @id @default(cuid()) + userId String + storeId String + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + + @@unique([userId, storeId]) + @@index([userId]) + @@index([storeId]) +} + +model FilterPreset { + id String @id @default(cuid()) + userId String + module String + name String + filters Json + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, module, name]) + @@index([userId, module]) +} + +model ExportJob { + id String @id @default(cuid()) + userId String + module String + filters Json? + status ExportStatus @default(QUEUED) + fileUrl String? + errorMessage String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId, status]) + @@index([module]) +} + +model IngestJob { + id String @id @default(cuid()) + source String + type String + status IngestStatus @default(QUEUED) + startedAt DateTime? + finishedAt DateTime? + recordsImported Int @default(0) + recordsFailed Int @default(0) + errorMessage String? + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([source, status]) + @@index([createdAt]) +} + +model ApiKey { + id String @id @default(cuid()) + userId String + name String + keyHash String @unique + prefix String + lastUsedAt DateTime? + revokedAt DateTime? + monthlyCreditLimit Int? + usedCreditsThisMonth Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([prefix]) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..fc52413 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,502 @@ +import bcrypt from "bcryptjs"; +import { + AdScore, + AdSource, + AdStatus, + MediaType, + PlanCode, + PrismaClient, + StorePlatform, + UsagePeriod, + UserRole +} from "@prisma/client"; + +const prisma = new PrismaClient(); + +const planFeatures = { + FREE: { + adsSearchDaily: 10, + storesSearchDaily: null, + tiktokSearchDaily: 0, + trendsSearchDaily: 5, + apiCreditsMonthly: 0, + trackedStores: 0, + followedBrands: 0, + savedAds: 20, + advancedFilters: false, + lockedResults: true, + exportEnabled: false + }, + BASIC: { + adsSearchDaily: 100, + storesSearchDaily: null, + tiktokSearchDaily: 200, + trendsSearchDaily: 20, + apiCreditsMonthly: 100, + trackedStores: 25, + followedBrands: 2, + savedAds: 500, + advancedFilters: false, + lockedResults: false, + exportEnabled: true + }, + STANDARD: { + adsSearchDaily: 500, + storesSearchDaily: null, + tiktokSearchDaily: 1000, + trendsSearchDaily: 100, + apiCreditsMonthly: 20000, + trackedStores: 50, + followedBrands: 30, + savedAds: 2000, + advancedFilters: true, + lockedResults: false, + exportEnabled: true, + pinterestAds: true, + tiktokAds: true + }, + PREMIUM: { + adsSearchDaily: 2000, + storesSearchDaily: null, + tiktokSearchDaily: null, + trendsSearchDaily: null, + apiCreditsMonthly: 20000, + trackedStores: 500, + followedBrands: 200, + savedAds: null, + advancedFilters: true, + lockedResults: false, + exportEnabled: true, + weeklyResearchCall: true, + pinterestAds: true, + tiktokAds: true + } +}; + +const stores = [ + { + domain: "petpro-demo.com", + name: "PetPro Demo", + logoUrl: "https://placehold.co/96x96/7c3aed/ffffff?text=PP", + country: "US", + currency: "USD", + language: "en", + niche: "Pets", + categoryPath: ["Pets", "Dog Accessories"], + theme: "Dawn", + visits: 120000, + growth: 18.2, + revMin: 45000, + revMax: 90000, + products: [ + ["Smart Dog Collar", 29.99, true], + ["No Pull Dog Harness", 34.99, true], + ["Travel Pet Bottle", 19.99, false] + ], + pixels: ["Meta Pixel", "TikTok Pixel", "Google Analytics"], + apps: ["Klaviyo", "Judge.me", "ReConvert"] + }, + { + domain: "glowly-demo.com", + name: "Glowly Demo", + logoUrl: "https://placehold.co/96x96/ec4899/ffffff?text=GL", + country: "GB", + currency: "GBP", + language: "en", + niche: "Beauty", + categoryPath: ["Beauty", "Skincare"], + theme: "Prestige", + visits: 245000, + growth: 27.5, + revMin: 90000, + revMax: 180000, + products: [ + ["LED Face Sculptor", 69.99, true], + ["Hydrating Serum Kit", 39.99, false], + ["Ice Roller Pro", 22.99, true] + ], + pixels: ["Meta Pixel", "Pinterest Tag", "Google Analytics"], + apps: ["Klaviyo", "Loox", "Recharge"] + }, + { + domain: "fitfuel-demo.com", + name: "FitFuel Demo", + logoUrl: "https://placehold.co/96x96/16a34a/ffffff?text=FF", + country: "US", + currency: "USD", + language: "en", + niche: "Supplements", + categoryPath: ["Health", "Supplements"], + theme: "Impulse", + visits: 310000, + growth: 9.4, + revMin: 130000, + revMax: 260000, + products: [ + ["Greens Energy Blend", 49.99, true], + ["Sleep Recovery Gummies", 24.99, true], + ["Protein Coffee", 39.99, false] + ], + pixels: ["Meta Pixel", "TikTok Pixel", "Google Analytics"], + apps: ["Klaviyo", "Recharge", "Yotpo"] + }, + { + domain: "homezen-demo.com", + name: "HomeZen Demo", + logoUrl: "https://placehold.co/96x96/0ea5e9/ffffff?text=HZ", + country: "DE", + currency: "EUR", + language: "de", + niche: "Household", + categoryPath: ["Household", "Home Gadgets"], + theme: "Refresh", + visits: 87000, + growth: 34.8, + revMin: 30000, + revMax: 70000, + products: [ + ["Magnetic Window Cleaner", 24.99, true], + ["Foldable Storage Rack", 44.99, false], + ["Mini Desk Vacuum", 17.99, true] + ], + pixels: ["Meta Pixel", "Google Analytics"], + apps: ["Judge.me", "Klaviyo"] + } +]; + +const adCopy = [ + { + domain: "petpro-demo.com", + headline: "Smart Dog Collar", + text: "Your dog deserves a safer walk. Meet the smart collar loved by 20,000+ pet parents.", + media: MediaType.VIDEO, + score: AdScore.ESTABLISHED, + countries: ["US", "GB", "CA"], + days: 27, + rank: 8, + niche: "Pets" + }, + { + domain: "petpro-demo.com", + headline: "No Pull Dog Harness", + text: "No more pulling. Give your dog freedom while staying in control.", + media: MediaType.IMAGE, + score: AdScore.HAS_POTENTIAL, + countries: ["US"], + days: 9, + rank: 19, + niche: "Pets" + }, + { + domain: "glowly-demo.com", + headline: "LED Face Sculptor", + text: "Spa-level glow at home. See why creators are switching to LED sculpting.", + media: MediaType.VIDEO, + score: AdScore.ESTABLISHED, + countries: ["GB", "US", "AU"], + days: 42, + rank: 4, + niche: "Beauty" + }, + { + domain: "glowly-demo.com", + headline: "Hydrating Serum Kit", + text: "Dry skin? Build a 3-step hydration routine that actually feels lightweight.", + media: MediaType.CAROUSEL, + score: AdScore.HAS_POTENTIAL, + countries: ["GB"], + days: 15, + rank: 16, + niche: "Beauty" + }, + { + domain: "fitfuel-demo.com", + headline: "Greens Energy Blend", + text: "Your morning coffee is not enough. Get clean energy with greens, adaptogens, and no crash.", + media: MediaType.VIDEO, + score: AdScore.ESTABLISHED, + countries: ["US", "CA"], + days: 61, + rank: 2, + niche: "Supplements" + }, + { + domain: "fitfuel-demo.com", + headline: "Sleep Recovery Gummies", + text: "Wake up rested. Sleep Recovery Gummies support your nightly wind-down routine.", + media: MediaType.IMAGE, + score: AdScore.HAS_POTENTIAL, + countries: ["US"], + days: 6, + rank: 26, + niche: "Supplements" + }, + { + domain: "homezen-demo.com", + headline: "Magnetic Window Cleaner", + text: "Clean both sides of your window from inside. The gadget going viral in Europe.", + media: MediaType.VIDEO, + score: AdScore.HAS_POTENTIAL, + countries: ["DE", "AT", "CH"], + days: 18, + rank: 12, + niche: "Household" + }, + { + domain: "homezen-demo.com", + headline: "Foldable Storage Rack", + text: "Small home? Create more space with a foldable storage rack.", + media: MediaType.IMAGE, + score: AdScore.UNESTABLISHED, + countries: ["DE"], + days: 3, + rank: 49, + niche: "Household", + inactive: true + } +]; + +function daysAgo(days: number) { + const d = new Date(); + d.setDate(d.getDate() - days); + return d; +} + +async function main() { + console.log("🌱 Seeding WinningHunter..."); + + const free = await prisma.plan.upsert({ + where: { code: PlanCode.FREE }, + update: { features: planFeatures.FREE }, + create: { code: PlanCode.FREE, name: "Free", monthlyPriceEur: 0, quarterlyPriceEur: 0, yearlyPriceEur: 0, features: planFeatures.FREE, sortOrder: 0 } + }); + const basic = await prisma.plan.upsert({ + where: { code: PlanCode.BASIC }, + update: { features: planFeatures.BASIC }, + create: { code: PlanCode.BASIC, name: "Basic", monthlyPriceEur: 42, quarterlyPriceEur: 107, yearlyPriceEur: 302, features: planFeatures.BASIC, sortOrder: 1 } + }); + await prisma.plan.upsert({ + where: { code: PlanCode.STANDARD }, + update: { features: planFeatures.STANDARD }, + create: { code: PlanCode.STANDARD, name: "Standard", monthlyPriceEur: 68, quarterlyPriceEur: 173, yearlyPriceEur: 490, features: planFeatures.STANDARD, sortOrder: 2 } + }); + await prisma.plan.upsert({ + where: { code: PlanCode.PREMIUM }, + update: { features: planFeatures.PREMIUM }, + create: { code: PlanCode.PREMIUM, name: "Premium", monthlyPriceEur: 212, quarterlyPriceEur: 541, yearlyPriceEur: 1526, features: planFeatures.PREMIUM, sortOrder: 3 } + }); + + const demo = await prisma.user.upsert({ + where: { email: "demo@winninghunter.local" }, + update: { passwordHash: await bcrypt.hash("demo1234", 10), name: "Demo User" }, + create: { + email: "demo@winninghunter.local", + name: "Demo User", + passwordHash: await bcrypt.hash("demo1234", 10), + role: UserRole.USER, + subscription: { create: { planId: free.id, status: "ACTIVE" } } + } + }); + const admin = await prisma.user.upsert({ + where: { email: "admin@winninghunter.local" }, + update: { passwordHash: await bcrypt.hash("admin1234", 10), name: "Admin", role: UserRole.ADMIN }, + create: { + email: "admin@winninghunter.local", + name: "Admin", + passwordHash: await bcrypt.hash("admin1234", 10), + role: UserRole.ADMIN, + subscription: { create: { planId: basic.id, status: "ACTIVE" } } + } + }); + + for (const s of stores) { + const store = await prisma.store.upsert({ + where: { domain: s.domain }, + update: { + name: s.name, + logoUrl: s.logoUrl, + country: s.country, + currency: s.currency, + language: s.language, + niche: s.niche, + categoryPath: s.categoryPath, + shopifyTheme: s.theme, + monthlyVisits: s.visits, + monthlyVisitGrowth: s.growth, + estRevenue30dMin: s.revMin, + estRevenue30dMax: s.revMax + }, + create: { + domain: s.domain, + name: s.name, + logoUrl: s.logoUrl, + platform: StorePlatform.SHOPIFY, + country: s.country, + currency: s.currency, + language: s.language, + niche: s.niche, + categoryPath: s.categoryPath, + shopUrl: `https://${s.domain}`, + createdAtSource: daysAgo(720), + dataSince: daysAgo(180), + shopifyTheme: s.theme, + productCount: s.products.length, + collectionCount: 8, + trustpilotScore: 4.2 + Math.random() * 0.5, + trustpilotReviewCount: Math.floor(400 + Math.random() * 2000), + monthlyVisits: s.visits, + monthlyVisitGrowth: s.growth, + estRevenue30dMin: s.revMin, + estRevenue30dMax: s.revMax, + estRevenue24hMin: Math.round(s.revMin / 30), + estRevenue24hMax: Math.round(s.revMax / 30), + trafficCountries: { [s.country]: 68, US: 18, GB: 8, CA: 6 }, + socials: { instagram: `https://instagram.com/${s.name.toLowerCase().replaceAll(" ", "")}` } + } + }); + + await prisma.brandPage.upsert({ + where: { source_externalPageId: { source: AdSource.META, externalPageId: `page_${s.domain}` } }, + update: { storeId: store.id, name: s.name, logoUrl: s.logoUrl, websiteDomain: s.domain }, + create: { + source: AdSource.META, + externalPageId: `page_${s.domain}`, + name: s.name, + logoUrl: s.logoUrl, + websiteDomain: s.domain, + pageUrl: `https://facebook.com/${s.domain.replaceAll(".", "")}`, + fbLikes: Math.round(s.visits / 8), + igFollowers: Math.round(s.visits / 5), + country: s.country, + niche: s.niche, + storeId: store.id + } + }); + + for (const [title, price, best] of s.products) { + const handle = String(title).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + await prisma.storeProduct.upsert({ + where: { id: `${store.id}_${handle}` }, + update: { title: String(title), price: Number(price), isBestSeller: Boolean(best) }, + create: { + id: `${store.id}_${handle}`, + storeId: store.id, + title: String(title), + handle, + imageUrl: `https://placehold.co/320x240/f1f5f9/0f172a?text=${encodeURIComponent(String(title))}`, + productUrl: `https://${s.domain}/products/${handle}`, + price: Number(price), + compareAtPrice: Number(price) * 1.6, + currency: s.currency, + variantCount: 3, + isBestSeller: Boolean(best), + firstSeenAt: daysAgo(90) + } + }); + } + for (const pixel of s.pixels) { + await prisma.storePixel.upsert({ where: { storeId_type_value: { storeId: store.id, type: pixel, value: "" } }, update: {}, create: { storeId: store.id, type: pixel, value: "" } }); + } + for (const app of s.apps) { + await prisma.storeApp.upsert({ where: { storeId_name: { storeId: store.id, name: app } }, update: {}, create: { storeId: store.id, name: app, category: "Marketing" } }); + } + for (let i = 0; i < 6; i++) { + const date = daysAgo(i * 30); + await prisma.storeSnapshot.upsert({ + where: { storeId_date: { storeId: store.id, date } }, + update: {}, + create: { storeId: store.id, date, monthlyVisits: Math.round(s.visits * (1 - i * 0.06)), estRevenueMin: Math.round(s.revMin * (1 - i * 0.05)), estRevenueMax: Math.round(s.revMax * (1 - i * 0.05)), activeAdsCount: Math.max(2, 22 - i * 2), productCount: s.products.length } + }); + } + } + + let adIndex = 1; + for (const a of adCopy) { + const page = await prisma.brandPage.findFirstOrThrow({ where: { websiteDomain: a.domain } }); + const ad = await prisma.ad.upsert({ + where: { source_externalAdId: { source: AdSource.META, externalAdId: `demo_ad_${adIndex}` } }, + update: {}, + create: { + source: AdSource.META, + externalAdId: `demo_ad_${adIndex}`, + brandPageId: page.id, + status: a.inactive ? AdStatus.INACTIVE : AdStatus.ACTIVE, + mediaType: a.media, + adScore: a.score, + primaryText: a.text, + headline: a.headline, + description: "Demo competitive intelligence ad.", + ctaText: "Shop Now", + landingUrl: `https://${a.domain}`, + productUrl: `https://${a.domain}/products/${a.headline.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`, + language: a.domain.includes("homezen") ? "de" : "en", + countries: a.countries, + niche: a.niche, + firstSeenAt: daysAgo(a.days), + lastSeenAt: a.inactive ? daysAgo(5) : new Date(), + createdAtSource: daysAgo(a.days), + daysRunning: a.days, + estimatedReachMin: 10000 * adIndex, + estimatedReachMax: 40000 * adIndex, + estimatedSpendMin: 500 * adIndex, + estimatedSpendMax: 1600 * adIndex, + estimatedCpm: 12 + adIndex, + rank: 100 - a.rank, + rankPercentile: a.rank, + rankGrowth: a.rank < 20 ? "Rising" : "Stable" + } + }); + await prisma.adCreative.create({ + data: { + adId: ad.id, + type: a.media, + url: `https://placehold.co/640x480/111827/ffffff?text=${encodeURIComponent(a.headline)}`, + thumbnailUrl: `https://placehold.co/640x480/111827/ffffff?text=${encodeURIComponent(a.headline)}`, + durationSeconds: a.media === MediaType.VIDEO ? 30 + adIndex : null + } + }); + for (const [type, value] of [ + ["hook", a.text.split(".")[0]], + ["angle", `${a.niche} winning angle`], + ["persona", `${a.niche} buyers`], + ["emotion", a.rank < 15 ? "Urgency" : "Trust"] + ]) { + await prisma.aITag.create({ data: { adId: ad.id, type, value, confidence: 0.91, model: "seed-demo", promptVersion: "v0" } }); + } + adIndex++; + } + + const firstAd = await prisma.ad.findFirstOrThrow(); + const folder = await prisma.savedFolder.upsert({ + where: { userId_name: { userId: demo.id, name: "Kazanan Pet Reklamları" } }, + update: {}, + create: { userId: demo.id, name: "Kazanan Pet Reklamları", color: "#22c55e" } + }); + await prisma.savedAd.upsert({ + where: { userId_adId: { userId: demo.id, adId: firstAd.id } }, + update: { folderId: folder.id }, + create: { userId: demo.id, adId: firstAd.id, folderId: folder.id, note: "Demo kayıt." } + }); + const firstStore = await prisma.store.findFirstOrThrow(); + await prisma.trackedStore.upsert({ + where: { userId_storeId: { userId: admin.id, storeId: firstStore.id } }, + update: {}, + create: { userId: admin.id, storeId: firstStore.id, notes: "Demo takip." } + }); + await prisma.usageCounter.upsert({ + where: { userId_metric_periodKey: { userId: demo.id, metric: "ads_search_daily", periodKey: new Date().toISOString().slice(0, 10) } }, + update: { used: 1, limit: 10 }, + create: { userId: demo.id, metric: "ads_search_daily", period: UsagePeriod.DAILY, periodKey: new Date().toISOString().slice(0, 10), used: 1, limit: 10 } + }); + await prisma.ingestJob.create({ data: { source: "seed", type: "demo-data", status: "COMPLETED", startedAt: new Date(), finishedAt: new Date(), recordsImported: 18 } }); + + console.log("✅ Demo: demo@winninghunter.local / demo1234"); + console.log("✅ Admin: admin@winninghunter.local / admin1234"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => prisma.$disconnect()); diff --git a/src/app/api/admin/ingest/seed-demo/route.ts b/src/app/api/admin/ingest/seed-demo/route.ts new file mode 100644 index 0000000..46910a1 --- /dev/null +++ b/src/app/api/admin/ingest/seed-demo/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { requireAdmin } from "@/lib/auth/current-user"; + +export async function POST() { + await requireAdmin(); + return NextResponse.json({ ok: true, message: "Demo seed komut satırından çalışır: npm run db:seed" }); +} diff --git a/src/app/api/ads/[id]/route.ts b/src/app/api/ads/[id]/route.ts new file mode 100644 index 0000000..96aa82a --- /dev/null +++ b/src/app/api/ads/[id]/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; +import { maskAdForPlan } from "@/lib/locked-response"; +import { planFromUser } from "@/lib/plans"; + +export async function GET(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const ad = await prisma.ad.findUnique({ + where: { id: params.id }, + include: { brandPage: true, creatives: true, aiTags: true, countryStats: true } + }); + if (!ad) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 }); + const plan = planFromUser(user as any); + return NextResponse.json({ data: maskAdForPlan(ad as any, plan?.code) }); +} diff --git a/src/app/api/ads/[id]/save/route.ts b/src/app/api/ads/[id]/save/route.ts new file mode 100644 index 0000000..485e333 --- /dev/null +++ b/src/app/api/ads/[id]/save/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function POST(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const ad = await prisma.ad.findUnique({ where: { id: params.id } }); + if (!ad) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 }); + await prisma.savedAd.upsert({ + where: { userId_adId: { userId: user.id, adId: params.id } }, + update: {}, + create: { userId: user.id, adId: params.id } + }); + return NextResponse.json({ ok: true }); +} + +export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + await prisma.savedAd.deleteMany({ where: { userId: user.id, adId: params.id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/ads/search/route.ts b/src/app/api/ads/search/route.ts new file mode 100644 index 0000000..ed0c2cc --- /dev/null +++ b/src/app/api/ads/search/route.ts @@ -0,0 +1,87 @@ +import { NextResponse } from "next/server"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; +import { checkAndConsumeQuota } from "@/lib/quota"; +import { maskAdForPlan } from "@/lib/locked-response"; +import { planFromUser } from "@/lib/plans"; + +export async function GET(req: Request) { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + + const plan = planFromUser(user as any); + const quota = await checkAndConsumeQuota({ userId: user.id, plan, metric: "ads_search_daily" }); + if (!quota.allowed) return NextResponse.json({ error: "QUOTA_EXCEEDED", usage: quota, upgradeRequired: true }, { status: 403 }); + + const url = new URL(req.url); + const q = url.searchParams.get("q")?.trim(); + const page = Math.max(1, Number(url.searchParams.get("page") || 1)); + const limit = Math.min(60, Math.max(1, Number(url.searchParams.get("limit") || 24))); + const status = url.searchParams.get("status"); + const niche = url.searchParams.get("niche"); + const mediaType = url.searchParams.get("mediaType"); + const country = url.searchParams.get("country"); + const sort = url.searchParams.get("sort") || "lastSeen_desc"; + + const where: Prisma.AdWhereInput = {}; + if (status) where.status = status as any; + if (niche) where.niche = { contains: niche, mode: "insensitive" }; + if (mediaType) where.mediaType = mediaType as any; + if (country) where.countries = { has: country }; + if (q) { + where.OR = [ + { primaryText: { contains: q, mode: "insensitive" } }, + { headline: { contains: q, mode: "insensitive" } }, + { brandPage: { name: { contains: q, mode: "insensitive" } } } + ]; + } + + const orderBy: Prisma.AdOrderByWithRelationInput = + sort === "rank_asc" ? { rankPercentile: "asc" } : sort === "days_desc" ? { daysRunning: "desc" } : { lastSeenAt: "desc" }; + + const [total, rows, saved] = await Promise.all([ + prisma.ad.count({ where }), + prisma.ad.findMany({ + where, + orderBy, + skip: (page - 1) * limit, + take: limit, + include: { brandPage: true, creatives: { take: 1 } } + }), + prisma.savedAd.findMany({ where: { userId: user.id }, select: { adId: true } }) + ]); + + const savedIds = new Set(saved.map((s) => s.adId)); + const data = rows.map((ad) => + maskAdForPlan( + { + id: ad.id, + source: ad.source, + status: ad.status, + mediaType: ad.mediaType, + adScore: ad.adScore, + primaryText: ad.primaryText, + headline: ad.headline, + ctaText: ad.ctaText, + landingUrl: ad.landingUrl, + productUrl: ad.productUrl, + language: ad.language, + countries: ad.countries, + niche: ad.niche, + daysRunning: ad.daysRunning, + estimatedReachMin: ad.estimatedReachMin, + estimatedReachMax: ad.estimatedReachMax, + estimatedSpendMin: ad.estimatedSpendMin, + estimatedSpendMax: ad.estimatedSpendMax, + rankPercentile: ad.rankPercentile, + brand: ad.brandPage && { id: ad.brandPage.id, name: ad.brandPage.name, logoUrl: ad.brandPage.logoUrl }, + thumbnailUrl: ad.creatives[0]?.thumbnailUrl || ad.creatives[0]?.url, + isSaved: savedIds.has(ad.id) + }, + plan?.code + ) + ); + + return NextResponse.json({ data, pagination: { page, limit, total }, usage: quota }); +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..e9b407b --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,18 @@ +import bcrypt from "bcryptjs"; +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { prisma } from "@/lib/db"; +import { createSession } from "@/lib/auth/session"; + +const schema = z.object({ email: z.string().email(), password: z.string().min(1) }); + +export async function POST(req: Request) { + const body = schema.parse(await req.json()); + const user = await prisma.user.findUnique({ where: { email: body.email } }); + if (!user?.passwordHash) return NextResponse.json({ error: "INVALID_CREDENTIALS" }, { status: 401 }); + const ok = await bcrypt.compare(body.password, user.passwordHash); + if (!ok) return NextResponse.json({ error: "INVALID_CREDENTIALS" }, { status: 401 }); + await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } }); + await createSession(user.id); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..9220621 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { destroySession } from "@/lib/auth/session"; + +export async function POST() { + await destroySession(); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..03e4e32 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function GET() { + const user = await currentUser(); + if (!user) return NextResponse.json({ user: null }); + return NextResponse.json({ + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + plan: user.subscription?.plan.code + } + }); +} diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..aa32630 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,31 @@ +import bcrypt from "bcryptjs"; +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { prisma } from "@/lib/db"; +import { createSession } from "@/lib/auth/session"; + +const schema = z.object({ + email: z.string().email(), + password: z.string().min(6), + name: z.string().optional() +}); + +export async function POST(req: Request) { + const body = schema.parse(await req.json()); + const free = await prisma.plan.findUnique({ where: { code: "FREE" } }); + if (!free) return NextResponse.json({ error: "PLAN_NOT_SEEDED" }, { status: 500 }); + + const exists = await prisma.user.findUnique({ where: { email: body.email } }); + if (exists) return NextResponse.json({ error: "EMAIL_EXISTS" }, { status: 409 }); + + const user = await prisma.user.create({ + data: { + email: body.email, + name: body.name || body.email.split("@")[0], + passwordHash: await bcrypt.hash(body.password, 10), + subscription: { create: { planId: free.id, status: "ACTIVE" } } + } + }); + await createSession(user.id); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/plans/route.ts b/src/app/api/plans/route.ts new file mode 100644 index 0000000..885d06d --- /dev/null +++ b/src/app/api/plans/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const plans = await prisma.plan.findMany({ where: { isActive: true }, orderBy: { sortOrder: "asc" } }); + return NextResponse.json({ data: plans }); +} diff --git a/src/app/api/saved-ads/[id]/route.ts b/src/app/api/saved-ads/[id]/route.ts new file mode 100644 index 0000000..896c1c7 --- /dev/null +++ b/src/app/api/saved-ads/[id]/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const body = await req.json(); + const saved = await prisma.savedAd.updateMany({ where: { id: params.id, userId: user.id }, data: { folderId: body.folderId || null, note: body.note } }); + return NextResponse.json({ data: saved }); +} + +export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + await prisma.savedAd.deleteMany({ where: { id: params.id, userId: user.id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/saved-ads/route.ts b/src/app/api/saved-ads/route.ts new file mode 100644 index 0000000..2449747 --- /dev/null +++ b/src/app/api/saved-ads/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function GET(req: Request) { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const folderId = new URL(req.url).searchParams.get("folderId"); + const data = await prisma.savedAd.findMany({ + where: { userId: user.id, ...(folderId ? { folderId } : {}) }, + orderBy: { createdAt: "desc" }, + include: { folder: true, ad: { include: { brandPage: true, creatives: { take: 1 } } } } + }); + return NextResponse.json({ data }); +} + +export async function POST(req: Request) { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const body = await req.json(); + const saved = await prisma.savedAd.upsert({ + where: { userId_adId: { userId: user.id, adId: body.adId } }, + update: { folderId: body.folderId || null, note: body.note }, + create: { userId: user.id, adId: body.adId, folderId: body.folderId || null, note: body.note } + }); + return NextResponse.json({ data: saved }); +} diff --git a/src/app/api/saved-folders/[id]/route.ts b/src/app/api/saved-folders/[id]/route.ts new file mode 100644 index 0000000..51b3c78 --- /dev/null +++ b/src/app/api/saved-folders/[id]/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const body = await req.json(); + await prisma.savedFolder.updateMany({ where: { id: params.id, userId: user.id }, data: { name: body.name, color: body.color } }); + return NextResponse.json({ ok: true }); +} + +export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + await prisma.savedFolder.deleteMany({ where: { id: params.id, userId: user.id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/saved-folders/route.ts b/src/app/api/saved-folders/route.ts new file mode 100644 index 0000000..45a5217 --- /dev/null +++ b/src/app/api/saved-folders/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function GET() { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + return NextResponse.json({ data: await prisma.savedFolder.findMany({ where: { userId: user.id }, orderBy: { sortOrder: "asc" } }) }); +} + +export async function POST(req: Request) { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const body = await req.json(); + const folder = await prisma.savedFolder.create({ data: { userId: user.id, name: body.name, color: body.color } }); + return NextResponse.json({ data: folder }); +} diff --git a/src/app/api/stores/[id]/route.ts b/src/app/api/stores/[id]/route.ts new file mode 100644 index 0000000..fda77a5 --- /dev/null +++ b/src/app/api/stores/[id]/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function GET(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const store = await prisma.store.findUnique({ where: { id: params.id }, include: { products: true, snapshots: { orderBy: { date: "asc" } }, pixels: true, apps: true, brandPages: { include: { ads: { include: { creatives: { take: 1 } }, take: 12 } } } } }); + if (!store) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 }); + return NextResponse.json({ data: store }); +} diff --git a/src/app/api/stores/[id]/similar/route.ts b/src/app/api/stores/[id]/similar/route.ts new file mode 100644 index 0000000..da99eac --- /dev/null +++ b/src/app/api/stores/[id]/similar/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function GET(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const store = await prisma.store.findUnique({ where: { id: params.id } }); + if (!store) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 }); + const data = await prisma.store.findMany({ where: { id: { not: params.id }, niche: store.niche }, take: 20, orderBy: { monthlyVisits: "desc" } }); + return NextResponse.json({ data }); +} diff --git a/src/app/api/stores/search/route.ts b/src/app/api/stores/search/route.ts new file mode 100644 index 0000000..c5d4845 --- /dev/null +++ b/src/app/api/stores/search/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function GET(req: Request) { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + + const url = new URL(req.url); + const q = url.searchParams.get("q")?.trim(); + const niche = url.searchParams.get("niche"); + const country = url.searchParams.get("country"); + const sort = url.searchParams.get("sort") || "revenue_desc"; + const page = Math.max(1, Number(url.searchParams.get("page") || 1)); + const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit") || 25))); + + const where: Prisma.StoreWhereInput = {}; + if (niche) where.niche = { contains: niche, mode: "insensitive" }; + if (country) where.country = country; + if (q) where.OR = [{ name: { contains: q, mode: "insensitive" } }, { domain: { contains: q, mode: "insensitive" } }, { niche: { contains: q, mode: "insensitive" } }]; + + const orderBy: Prisma.StoreOrderByWithRelationInput = sort === "traffic_desc" ? { monthlyVisits: "desc" } : sort === "growth_desc" ? { monthlyVisitGrowth: "desc" } : { estRevenue30dMax: "desc" }; + + const [total, rows] = await Promise.all([ + prisma.store.count({ where }), + prisma.store.findMany({ where, orderBy, skip: (page - 1) * limit, take: limit, include: { products: { where: { isBestSeller: true }, take: 3 }, brandPages: { include: { _count: { select: { ads: true } } }, take: 1 } } }) + ]); + return NextResponse.json({ data: rows, pagination: { page, limit, total } }); +} diff --git a/src/app/api/tracked-stores/[id]/route.ts b/src/app/api/tracked-stores/[id]/route.ts new file mode 100644 index 0000000..c68026c --- /dev/null +++ b/src/app/api/tracked-stores/[id]/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; + +export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) { + const params = await context.params; + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + await prisma.trackedStore.deleteMany({ where: { id: params.id, userId: user.id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/tracked-stores/route.ts b/src/app/api/tracked-stores/route.ts new file mode 100644 index 0000000..5b39a7a --- /dev/null +++ b/src/app/api/tracked-stores/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { currentUser } from "@/lib/auth/current-user"; +import { getFeatureLimit, planFromUser } from "@/lib/plans"; + +export async function GET() { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const data = await prisma.trackedStore.findMany({ where: { userId: user.id }, include: { store: { include: { products: { where: { isBestSeller: true }, take: 3 } } } }, orderBy: { createdAt: "desc" } }); + return NextResponse.json({ data }); +} + +export async function POST(req: Request) { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + const plan = planFromUser(user as any); + const limit = getFeatureLimit(plan as any, "tracked_stores"); + const count = await prisma.trackedStore.count({ where: { userId: user.id } }); + if (limit !== null && count >= limit) return NextResponse.json({ error: "TRACKED_STORE_LIMIT_EXCEEDED", upgradeRequired: true }, { status: 403 }); + const body = await req.json(); + const domain = String(body.domain || "").replace(/^https?:\/\//, "").replace(/\/$/, "").toLowerCase(); + const store = await prisma.store.findUnique({ where: { domain } }); + if (!store) return NextResponse.json({ error: "STORE_NOT_FOUND" }, { status: 404 }); + const row = await prisma.trackedStore.upsert({ where: { userId_storeId: { userId: user.id, storeId: store.id } }, update: {}, create: { userId: user.id, storeId: store.id } }); + return NextResponse.json({ data: row }); +} diff --git a/src/app/api/usage/summary/route.ts b/src/app/api/usage/summary/route.ts new file mode 100644 index 0000000..5d0538a --- /dev/null +++ b/src/app/api/usage/summary/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { currentUser } from "@/lib/auth/current-user"; +import { usageSummary } from "@/lib/quota"; + +export async function GET() { + const user = await currentUser(); + if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 }); + return NextResponse.json({ data: await usageSummary(user.id) }); +} diff --git a/src/app/dashboard/account/page.tsx b/src/app/dashboard/account/page.tsx new file mode 100644 index 0000000..7831db4 --- /dev/null +++ b/src/app/dashboard/account/page.tsx @@ -0,0 +1,7 @@ +import { requireUser } from "@/lib/auth/current-user"; +import { Card } from "@/components/ui/card"; + +export default async function AccountPage() { + const user = await requireUser(); + return

My Account

{user.email} · Plan: {user.subscription?.plan.name}

; +} diff --git a/src/app/dashboard/admin/data/page.tsx b/src/app/dashboard/admin/data/page.tsx new file mode 100644 index 0000000..9b5cf97 --- /dev/null +++ b/src/app/dashboard/admin/data/page.tsx @@ -0,0 +1,9 @@ +import { requireAdmin } from "@/lib/auth/current-user"; +import { prisma } from "@/lib/db"; +import { Card } from "@/components/ui/card"; + +export default async function AdminDataPage() { + await requireAdmin(); + const [ads, stores, users, jobs] = await Promise.all([prisma.ad.count(), prisma.store.count(), prisma.user.count(), prisma.ingestJob.findMany({ orderBy: { createdAt: "desc" }, take: 10 })]); + return

Admin Data

{ads}
Ads
{stores}
Stores
{users}
Users

Ingest Jobs

{jobs.map((j) =>
{j.source} · {j.type} · {j.status} · {j.recordsImported} records
)}
; +} diff --git a/src/app/dashboard/ads/page.tsx b/src/app/dashboard/ads/page.tsx new file mode 100644 index 0000000..0c87132 --- /dev/null +++ b/src/app/dashboard/ads/page.tsx @@ -0,0 +1,67 @@ +import Link from "next/link"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/db"; +import { requireUser } from "@/lib/auth/current-user"; +import { maskAdForPlan } from "@/lib/locked-response"; +import { planFromUser } from "@/lib/plans"; +import { Card } from "@/components/ui/card"; + +export default async function AdsPage({ searchParams }: { searchParams: Record }) { + const user = await requireUser(); + const plan = planFromUser(user as any); + const q = searchParams.q?.trim(); + const niche = searchParams.niche; + const mediaType = searchParams.mediaType; + const where: Prisma.AdWhereInput = {}; + if (q) where.OR = [{ primaryText: { contains: q, mode: "insensitive" } }, { headline: { contains: q, mode: "insensitive" } }, { brandPage: { name: { contains: q, mode: "insensitive" } } }]; + if (niche) where.niche = niche; + if (mediaType) where.mediaType = mediaType as any; + const ads = await prisma.ad.findMany({ where, include: { brandPage: true, creatives: { take: 1 }, savedBy: { where: { userId: user.id } } }, orderBy: { rankPercentile: "asc" }, take: 48 }); + const masked = ads.map((ad) => maskAdForPlan({ ...ad, isSaved: ad.savedBy.length > 0 }, plan?.code)); + + return ( +
+
+
+

Search Meta Adlibrary

+

Kazanan Meta reklamlarını keyword, niche ve medya tipine göre keşfet.

+
+
Plan: {plan?.name} · Free ise kartlar kilitli
+
+
+ + + + +
+
+ {["Week's biggest winners", "US winners", "Dropship Ads", "Supplements", "Top Branded"].map((x) => {x})} +
+
+ {masked.map((ad: any) => ( + + {ad.isLocked &&
Start now — Unlock winners
} +
+ +
+
{ad.headline}
+
Top %{Math.round(ad.rankPercentile || 0)}
+
+
{ad.brandPage?.name} · {ad.mediaType} · {ad.daysRunning} gün
+

{ad.primaryText}

+
+
{ad.countries.join(", ")}
Ülke
+
€{ad.estimatedSpendMin ?? "—"}
Min spend
+
{ad.adScore}
Score
+
+
+
+ ))} +
+
+ ); +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx new file mode 100644 index 0000000..6b4216b --- /dev/null +++ b/src/app/dashboard/layout.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import { requireUser } from "@/lib/auth/current-user"; + +const nav = [ + ["Ads", "/dashboard/ads"], + ["Stores", "/dashboard/stores"], + ["Store Tracker", "/dashboard/store-tracker"], + ["Saved Ads", "/dashboard/saved-ads"], + ["TikTok Shop", "#"], + ["Magic AI", "#"], + ["Trends", "#"], + ["Brand Tracker", "#"] +]; + +export default async function DashboardLayout({ children }: { children: React.ReactNode }) { + const user = await requireUser(); + return ( +
+
+
+ WinningHunter.AI + +
+ Upgrade +
+
{user.name || user.email}
+
{user.subscription?.plan.name}
+
+
+
+
+
{children}
+
+ ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..05717d1 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function DashboardIndex() { + redirect("/dashboard/ads"); +} diff --git a/src/app/dashboard/saved-ads/page.tsx b/src/app/dashboard/saved-ads/page.tsx new file mode 100644 index 0000000..a436127 --- /dev/null +++ b/src/app/dashboard/saved-ads/page.tsx @@ -0,0 +1,23 @@ +import { prisma } from "@/lib/db"; +import { requireUser } from "@/lib/auth/current-user"; +import { Card } from "@/components/ui/card"; + +export default async function SavedAdsPage() { + const user = await requireUser(); + const folders = await prisma.savedFolder.findMany({ where: { userId: user.id } }); + const saved = await prisma.savedAd.findMany({ where: { userId: user.id }, include: { folder: true, ad: { include: { brandPage: true, creatives: { take: 1 } } } }, orderBy: { createdAt: "desc" } }); + return ( +
+

Saved Ads

+
+ +

Folders

+
All Saved Ads ({saved.length})
{folders.map((f) =>
{f.name}
)}
+
+
+ {saved.map((s) =>
{s.ad.headline}
{s.ad.brandPage?.name} · {s.folder?.name || "All"}

{s.ad.primaryText}

)} +
+
+
+ ); +} diff --git a/src/app/dashboard/store-tracker/page.tsx b/src/app/dashboard/store-tracker/page.tsx new file mode 100644 index 0000000..3353459 --- /dev/null +++ b/src/app/dashboard/store-tracker/page.tsx @@ -0,0 +1,24 @@ +import { prisma } from "@/lib/db"; +import { requireUser } from "@/lib/auth/current-user"; +import { Card } from "@/components/ui/card"; + +export default async function StoreTrackerPage() { + const user = await requireUser(); + const tracked = await prisma.trackedStore.findMany({ where: { userId: user.id }, include: { store: { include: { products: { where: { isBestSeller: true }, take: 2 } } } }, orderBy: { createdAt: "desc" } }); + return ( +
+

Store Tracker

+

Plan limitine göre rakip Shopify mağazalarını watchlist’e ekle.

+ +
+ + +
+
+ {tracked.map((t) =>
{t.store.name}
{t.store.domain} · {t.store.products.map((p) => p.title).join(", ")} · €{t.store.estRevenue30dMax?.toLocaleString()} est.
)} + {!tracked.length &&
Free planda tracker kapalıdır. Admin demo hesabında örnek takip bulunur.
} +
+
+
+ ); +} diff --git a/src/app/dashboard/stores/[id]/page.tsx b/src/app/dashboard/stores/[id]/page.tsx new file mode 100644 index 0000000..c2a9600 --- /dev/null +++ b/src/app/dashboard/stores/[id]/page.tsx @@ -0,0 +1,59 @@ +import { notFound } from "next/navigation"; +import { prisma } from "@/lib/db"; +import { requireUser } from "@/lib/auth/current-user"; +import { Card } from "@/components/ui/card"; + +export default async function StoreDetailPage({ params }: { params: Promise<{ id: string }> }) { + const resolvedParams = await params; + await requireUser(); + const store = await prisma.store.findUnique({ + where: { id: resolvedParams.id }, + include: { products: true, snapshots: { orderBy: { date: "asc" } }, pixels: true, apps: true, brandPages: { include: { ads: { include: { creatives: { take: 1 } }, take: 12 } } } } + }); + if (!store) notFound(); + const similar = await prisma.store.findMany({ where: { id: { not: store.id }, niche: store.niche }, take: 8, orderBy: { monthlyVisits: "desc" } }); + const ads = store.brandPages.flatMap((p) => p.ads); + return ( +
+
+
+ +
+

{store.name}

+

{store.domain} · {store.country} · {store.niche}

+
+
+ Visit Shop +
+
+
Monthly Visits
{store.monthlyVisits?.toLocaleString()}
+
Est Revenue
€{store.estRevenue30dMin?.toLocaleString()}–€{store.estRevenue30dMax?.toLocaleString()}
+
Products
{store.productCount}
+
Active Ads
{ads.length}
+
+
+ +

Best-selling Products

+
+ {store.products.map((p) =>
{p.title}
{p.currency} {p.price}
)} +
+
+ +

Pixels & Stack

+
{store.pixels.map((p) => {p.type})}
+
{store.apps.map((a) => {a.name})}
+
+
+ +

Top Meta Ads

+
+ {ads.map((ad) =>
{ad.headline}
Top %{ad.rankPercentile} · {ad.daysRunning} gün
)} +
+
+ +

Similar Stores

+
{similar.map((s) =>
{s.name}
{s.monthlyVisits?.toLocaleString()} visits
)}
+
+
+ ); +} diff --git a/src/app/dashboard/stores/page.tsx b/src/app/dashboard/stores/page.tsx new file mode 100644 index 0000000..367102c --- /dev/null +++ b/src/app/dashboard/stores/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/db"; +import { requireUser } from "@/lib/auth/current-user"; +import { Card } from "@/components/ui/card"; + +export default async function StoresPage({ searchParams }: { searchParams: Record }) { + await requireUser(); + const q = searchParams.q?.trim(); + const niche = searchParams.niche; + const where: Prisma.StoreWhereInput = {}; + if (q) where.OR = [{ name: { contains: q, mode: "insensitive" } }, { domain: { contains: q, mode: "insensitive" } }]; + if (niche) where.niche = niche; + const stores = await prisma.store.findMany({ where, include: { products: { where: { isBestSeller: true }, take: 2 }, brandPages: { include: { _count: { select: { ads: true } } }, take: 1 } }, orderBy: { estRevenue30dMax: "desc" }, take: 50 }); + return ( +
+
+

Explore Stores

+

Shopify mağazalarını trafik, gelir, niche ve aktif reklam sayısıyla keşfet.

+
+
+ + + +
+ +
+ + + + + + {stores.map((s) => ( + + + + + + + + + + ))} + +
Shop InfoBest SellersNicheMonthly VisitsEst. Revenue 30dMeta Ads
{s.name}
{s.domain} · {s.country}
{s.products.map((p) => p.title).join(", ")}{s.niche}{s.monthlyVisits?.toLocaleString()} +{s.monthlyVisitGrowth}%€{s.estRevenue30dMin?.toLocaleString()}–€{s.estRevenue30dMax?.toLocaleString()}{s.brandPages[0]?._count.ads || 0}Details
+
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..621f15d --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,34 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: light; +} + +body { + margin: 0; + background: #f8fafc; + color: #0f172a; +} + +* { + box-sizing: border-box; +} + +a { + color: inherit; + text-decoration: none; +} + +.glass { + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(16px); + border: 1px solid rgba(148, 163, 184, 0.22); +} + +.locked-blur { + filter: blur(5px); + pointer-events: none; + user-select: none; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..4722148 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "WinningHunter MVP", + description: "Dropshipping reklam ve mağaza istihbaratı MVP" +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..2ac007d --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useState } from "react"; + +export default function LoginPage() { + const [email, setEmail] = useState("demo@winninghunter.local"); + const [password, setPassword] = useState("demo1234"); + const [error, setError] = useState(""); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + const res = await fetch("/api/auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password }) }); + if (!res.ok) { + setError("Giriş başarısız. Demo: demo@winninghunter.local / demo1234"); + return; + } + location.href = "/dashboard/ads"; + } + + return ( +
+
+

Giriş yap

+

Demo hesap hazır gelir.

+ + setEmail(e.target.value)} /> + + setPassword(e.target.value)} /> + {error &&
{error}
} + + Hesap oluştur +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..154ca5b --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,49 @@ +import { LinkButton } from "@/components/ui/button"; + +export default function LandingPage() { + return ( +
+ +
+
+
+ Meta Ads + Shopify Store Intelligence MVP +
+

+ Kazanan reklamları ve mağazaları dakikalar içinde keşfet. +

+

+ Dropshipping ve e-ticaret için reklam kütüphanesi, mağaza keşfi, kaydetme, takip ve kota tabanlı üyelik altyapısı tek panelde. +

+
+ Ücretsiz başla + Demo panel +
+
+
+
+
+ Live winners + Rising +
+ {["Smart Dog Collar", "LED Face Sculptor", "Greens Energy Blend"].map((item, i) => ( +
+
{item}
+
+
+
+
Top %{4 + i * 6} · {27 + i * 9} days running
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx new file mode 100644 index 0000000..0e498b6 --- /dev/null +++ b/src/app/pricing/page.tsx @@ -0,0 +1,39 @@ +import { prisma } from "@/lib/db"; +import { LinkButton } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; + +export default async function PricingPage() { + const plans = await prisma.plan.findMany({ orderBy: { sortOrder: "asc" } }).catch(() => []); + const fallback = [ + { code: "FREE", name: "Free", monthlyPriceEur: 0, features: { adsSearchDaily: 10, trackedStores: 0 } }, + { code: "BASIC", name: "Basic", monthlyPriceEur: 42, features: { adsSearchDaily: 100, trackedStores: 25 } }, + { code: "STANDARD", name: "Standard", monthlyPriceEur: 68, features: { adsSearchDaily: 500, trackedStores: 50 } }, + { code: "PREMIUM", name: "Premium", monthlyPriceEur: 212, features: { adsSearchDaily: 2000, trackedStores: 500 } } + ]; + const rows = plans.length ? plans : fallback; + return ( +
+
+
+

Fiyatlandırma

+

Kota tabanlı freemium model: arama, takip ve API kredileri planlara göre açılır.

+
+
+ {rows.map((plan: any) => ( + +
{plan.code}
+

{plan.name}

+
€{plan.monthlyPriceEur}/ay
+
    +
  • Ads arama/gün: {String((plan.features as any).adsSearchDaily ?? "Sınırsız")}
  • +
  • Store tracker: {String((plan.features as any).trackedStores ?? "Sınırsız")}
  • +
  • Saved ads: {String((plan.features as any).savedAds ?? "Sınırsız")}
  • +
+ Başla +
+ ))} +
+
+
+ ); +} diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..17d8dab --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useState } from "react"; + +export default function RegisterPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [name, setName] = useState(""); + const [error, setError] = useState(""); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + const res = await fetch("/api/auth/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password, name }) }); + if (!res.ok) { + setError("Kayıt başarısız. E-posta kullanılıyor olabilir veya seed çalışmamış olabilir."); + return; + } + location.href = "/dashboard/ads"; + } + + return ( +
+
+

Ücretsiz başla

+ + setName(e.target.value)} /> + + setEmail(e.target.value)} /> + + setPassword(e.target.value)} /> + {error &&
{error}
} + +
+
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..032a5f9 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,19 @@ +import Link from "next/link"; +import { ButtonHTMLAttributes } from "react"; + +export function Button({ className = "", ...props }: ButtonHTMLAttributes) { + return ( +