diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2b8380c --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Copy to .env and fill in values. Never commit .env. + +# --- App URLs (local LAN: use your container IP or 127.0.0.1) --- +NEXTAUTH_URL=http://127.0.0.1:8008 +AUTH_URL=http://127.0.0.1:8008 +AUTH_SECRET=generate_a_long_random_string_min_32_chars + +# --- Database (PostgreSQL) --- +DATABASE_URL=postgresql://fundraise:fundraise_local_dev@localhost:5432/fundraising + +# --- Branding & campaign copy --- +NEXT_PUBLIC_APP_NAME=Democracy Rising +PUBLIC_APP_NAME=Democracy Rising +PUBLIC_CREDIT_NAME=BLW +PUBLIC_CAMPAIGN_GOAL_USD=250000 + +# --- Legal / disclosure placeholders (not legal advice) --- +NEXT_PUBLIC_COMMITTEE_LEGAL_NAME_PLACEHOLDER=Your Committee Legal Name Here +COMMITTEE_LEGAL_NAME_PLACEHOLDER=Your Committee Legal Name Here +NEXT_PUBLIC_DISCLAIMER_TEXT=This deployment is for local demonstration. Configure FEC/state disclosures before accepting live contributions. +DISCLAIMER_TEXT=This deployment is for local demonstration. Configure FEC/state disclosures before accepting live contributions. + +# --- Stripe (test keys for development; use restricted keys in shared environments) --- +STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Credits: whole credits granted per CREDIT_RATIO_CENTS_PER_USD cents donated (100 = 1 credit per dollar). +CREDIT_RATIO_CENTS_PER_USD=100 + +# Optional: Stripe CLI for local webhook forwarding: +# stripe listen --forward-to 127.0.0.1:8008/api/webhooks/stripe diff --git a/.gitignore b/.gitignore index 5ef6a52..1df4829 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel @@ -39,3 +40,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/src/generated/prisma diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/README.md b/README.md index e215bc4..1dd3229 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,88 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Democratic Fundraiser -## Getting Started +A local-first Democratic fundraising platform built with Next.js, Stripe Payment Intents, Auth.js credentials login, Prisma, PostgreSQL, and a server-authoritative supporter wallet. -First, run the development server: +The app is designed to make donating feel active instead of transactional: cinematic landing page, public momentum meter, issue narrative cards, impact planner, Blue Wave (BLW) mock credit index, `/raised` totals page, wallet rewards, raffle entries, and compliance placeholders that must be configured before real fundraising. + +## Features + +- Stripe card donations through Payment Intents and Elements. +- Server-confirmed wallet credits from Stripe webhooks with idempotent ledger writes. +- Mock Blue Wave (BLW) supporter economy for rewards UX. BLW is not crypto and is not tradable. +- Supporter wallet with digital perk redemptions and raffle ticket spending. +- Public fundraising stats endpoint for raised total, donor count, and goal progress. +- Campaign action center, impact planner, issue grid, accountability frame, and rewards preview. +- Local LAN runtime on `0.0.0.0:8008`. + +## Stack + +- Next.js App Router + TypeScript +- React 19 +- Tailwind CSS 4 +- Prisma 7 + PostgreSQL +- Auth.js / NextAuth credentials provider +- Stripe SDK + Stripe React Elements +- Framer Motion for motion and interaction polish + +## Quick Start ```bash +cd /root/fundraising-platform +npm install +cp .env.example .env +npm run db:migrate +npm run db:seed npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open `http://127.0.0.1:8008` or `http://:8008`. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Configuration -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +Copy `.env.example` to `.env` and fill in the real local values. Do not commit `.env`. -## Learn More +Required values: -To learn more about Next.js, take a look at the following resources: +- `DATABASE_URL` +- `AUTH_URL` +- `NEXTAUTH_URL` +- `AUTH_SECRET` +- `STRIPE_SECRET_KEY` +- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` +- `STRIPE_WEBHOOK_SECRET` +- `PUBLIC_CAMPAIGN_GOAL_USD` +- `NEXT_PUBLIC_COMMITTEE_LEGAL_NAME_PLACEHOLDER` +- `NEXT_PUBLIC_DISCLAIMER_TEXT` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +For local Stripe webhook forwarding: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```bash +stripe listen --forward-to 127.0.0.1:8008/api/webhooks/stripe +``` -## Deploy on Vercel +## Useful Commands -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +```bash +npm run dev # Next dev server on 0.0.0.0:8008 +npm run build # Production build +npm run start # Serve production build on 0.0.0.0:8008 +npm run db:migrate # Apply Prisma migrations in dev +npm run db:seed # Seed demo users, wallet, perks, raffles +npm run test:integration # DB and optional Stripe smoke checks +npm run test:http # HTTP smoke checks against a running server +``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +## Donation Flow + +1. A signed-in supporter chooses an allowed donation tier. +2. The server creates a Stripe PaymentIntent and stores the user id plus BLW spot snapshot in metadata. +3. Stripe confirms the card payment. +4. Stripe sends `payment_intent.succeeded` to `/api/webhooks/stripe`. +5. The webhook verifies the signature, checks idempotency, creates the donation, appends a ledger entry, and increments wallet credits exactly once. +6. The wallet reads the updated balance and enables rewards or raffle actions. + +## Compliance Notes + +This repository is a technical demo until configured by qualified campaign counsel. Political fundraising can require FEC, state, donor eligibility, disclosure, reporting, refund, and payment processor review. Replace all committee placeholders and legal copy before accepting live contributions. + +Never commit secrets, live Stripe keys, donor exports, production database dumps, or private credentials. diff --git a/content/issues.json b/content/issues.json new file mode 100644 index 0000000..0dededc --- /dev/null +++ b/content/issues.json @@ -0,0 +1,72 @@ +[ + { + "id": "democracy", + "title": "Democracy that delivers", + "subtitle": "Institutional integrity", + "body": "Americans across parties cite corruption and accountability as urgent priorities. We invest in transparent governance, voting access, and ethics enforcement—because durable progress requires guardrails, not strongman shortcuts.", + "accent": "from-sky-500/30 to-indigo-600/20" + }, + { + "id": "economy", + "title": "An economy built for workers", + "subtitle": "Wages, costs, and fairness", + "body": "When everyday costs bite harder than paychecks grow, families pay the price. We organize around fair wages, anti‑price‑gouging guardrails, and tax fairness so prosperity flows to communities—not only to the top.", + "accent": "from-emerald-500/30 to-teal-600/20" + }, + { + "id": "healthcare", + "title": "Healthcare you can actually use", + "subtitle": "Affordability and access", + "body": "Coverage on paper is not care in practice. We fight for lower premiums and out‑of‑pocket costs, maternal health, mental health parity, and rural access—treating healthcare as infrastructure, not a luxury.", + "accent": "from-fuchsia-500/25 to-pink-600/15" + }, + { + "id": "climate", + "title": "Climate action that creates jobs", + "subtitle": "Energy transition", + "body": "The clean energy transition is an industrial strategy: domestic manufacturing, union pathways, and resilient grids. We reject false choices between jobs and the planet—policy can deliver both.", + "accent": "from-lime-500/25 to-green-700/20" + }, + { + "id": "labor", + "title": "Labor rights are democracy at work", + "subtitle": "Organizing power", + "body": "When workers can bargain safely, standards rise for everyone. We support organizing rights, enforcement against wage theft, and protections for gig and contract workers facing algorithmic squeeze.", + "accent": "from-amber-500/25 to-orange-600/20" + }, + { + "id": "education", + "title": "Strong public schools, open doors", + "subtitle": "Opportunity", + "body": "Public education is the promise that ZIP codes don’t decide destiny. We fund classrooms fairly, expand vocational and apprenticeship pathways, and protect students from book‑banning theatrics that substitute culture wars for outcomes.", + "accent": "from-violet-500/25 to-purple-700/20" + }, + { + "id": "reproductive", + "title": "Autonomy and evidence‑based care", + "subtitle": "Reproductive healthcare", + "body": "Medical decisions belong between patients and providers—period. We oppose legislative interference in private care and support access and privacy protections grounded in science and civil liberties.", + "accent": "from-rose-500/25 to-red-700/15" + }, + { + "id": "housing", + "title": "Homes people can afford", + "subtitle": "Stability", + "body": "Housing insecurity ripples through health, education, and jobs. We pursue supply solutions that respect communities, tenant protections where markets fail, and fair lending—so housing is shelter, not speculation.", + "accent": "from-cyan-500/25 to-blue-700/20" + }, + { + "id": "tech", + "title": "Technology that serves people", + "subtitle": "Privacy and competition", + "body": "Big platforms shouldn’t write the rules they profit from. We support privacy rights, strong antitrust enforcement, and AI accountability—democratic standards for systems that shape what we see, buy, and believe.", + "accent": "from-slate-500/30 to-zinc-700/25" + }, + { + "id": "justice", + "title": "Equal justice under law", + "subtitle": "Civil rights", + "body": "Justice means consistent rights—not geography lotteries. We invest in civil rights enforcement, policing accountability with community input, and restoration pathways that reduce harm and rebuild trust.", + "accent": "from-indigo-500/25 to-blue-900/25" + } +] diff --git a/package-lock.json b/package-lock.json index 22e19bf..947abf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,20 +7,37 @@ "": { "name": "fundraising-platform", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { - "next": "15.3.2", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.8.0", + "@stripe/react-stripe-js": "^6.3.0", + "@stripe/stripe-js": "^9.4.0", + "bcryptjs": "^3.0.3", + "clsx": "^2.1.1", + "framer-motion": "^12.38.0", + "next": "^16.2.6", + "next-auth": "^5.0.0-beta.31", + "pg": "^8.20.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "stripe": "^22.1.1", + "zod": "^4.4.3" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", + "dotenv": "^17.4.2", "eslint": "^9", - "eslint-config-next": "15.3.2", + "eslint-config-next": "^16.2.6", + "prisma": "^7.8.0", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } }, @@ -37,6 +54,338 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -70,6 +419,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -214,6 +1005,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -796,6 +1600,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -810,15 +1621,15 @@ } }, "node_modules/@next/env": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz", - "integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", - "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", + "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", "dev": true, "license": "MIT", "dependencies": { @@ -826,9 +1637,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz", - "integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "cpu": [ "arm64" ], @@ -842,9 +1653,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz", - "integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "cpu": [ "x64" ], @@ -858,9 +1669,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz", - "integrity": "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "cpu": [ "arm64" ], @@ -874,9 +1685,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz", - "integrity": "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "cpu": [ "arm64" ], @@ -890,9 +1701,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz", - "integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "cpu": [ "x64" ], @@ -906,9 +1717,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz", - "integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "cpu": [ "x64" ], @@ -922,9 +1733,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz", - "integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "cpu": [ "arm64" ], @@ -938,9 +1749,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz", - "integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "cpu": [ "x64" ], @@ -1001,6 +1812,396 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", + "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1008,18 +2209,35 @@ "dev": true, "license": "MIT" }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", - "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", - "dev": true, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, "license": "MIT" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" + "node_modules/@stripe/react-stripe-js": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.3.0.tgz", + "integrity": "sha512-N1FTRNCMKySElDz1lAsf/m6Oy5vcl6LRVXcW29t0Y3U3HYOAqCBlk6nuDsR2x7SAuaXkVCjnpCqrNbA/7l74jg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=9.3.1 <10.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.4.0.tgz", + "integrity": "sha512-zXG86DnoLU5nH2U18tX5ApTE3IAm+txoiPm4YFyEtRS0K5y7ZDH9M8e1Le/cZyI6wvW3BkJn2daZe2FqZOrSSg==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } }, "node_modules/@swc/helpers": { "version": "0.5.15", @@ -1312,6 +2530,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1337,17 +2562,27 @@ "version": "20.19.40", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.40.tgz", "integrity": "sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2180,6 +3415,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", @@ -2207,6 +3452,34 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", + "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", + "devOptional": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -2231,15 +3504,67 @@ "node": ">=8" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "streamsearch": "^1.1.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=10.16.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } } }, "node_modules/call-bind": { @@ -2339,12 +3664,50 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2372,11 +3735,25 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2391,7 +3768,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2480,6 +3857,16 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2516,6 +3903,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2539,6 +3950,19 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2554,6 +3978,24 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2561,6 +4003,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", @@ -2575,6 +4027,19 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -2752,6 +4217,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2826,25 +4343,24 @@ } }, "node_modules/eslint-config-next": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.3.2.tgz", - "integrity": "sha512-FerU4DYccO4FgeYFFglz0SnaKRe1ejXQrDb8kWUkTAg036YWi+jUsgg4sIGNCDhAsDITsZaL4MzBWKB6f4G1Dg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", + "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.3.2", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -2853,6 +4369,19 @@ } } }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", @@ -3056,16 +4585,23 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react/node_modules/semver": { @@ -3172,11 +4708,41 @@ "node": ">=0.10.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3223,6 +4789,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3313,6 +4896,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "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", @@ -3354,6 +4996,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -3364,6 +5016,16 @@ "node": ">= 0.4" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3389,6 +5051,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3434,6 +5103,16 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3494,9 +5173,23 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3591,6 +5284,57 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3900,6 +5644,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4056,7 +5807,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4081,17 +5832,25 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4107,6 +5866,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4485,11 +6257,17 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4498,6 +6276,32 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4565,6 +6369,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4572,6 +6391,40 @@ "dev": true, "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -4614,16 +6467,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.2.tgz", - "integrity": "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", "dependencies": { - "@next/env": "15.3.2", - "@swc/counter": "0.1.3", + "@next/env": "16.2.6", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -4632,22 +6483,22 @@ "next": "dist/bin/next" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.2", - "@next/swc-darwin-x64": "15.3.2", - "@next/swc-linux-arm64-gnu": "15.3.2", - "@next/swc-linux-arm64-musl": "15.3.2", - "@next/swc-linux-x64-gnu": "15.3.2", - "@next/swc-linux-x64-musl": "15.3.2", - "@next/swc-win32-arm64-msvc": "15.3.2", - "@next/swc-win32-x64-msvc": "15.3.2", - "sharp": "^0.34.1" + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@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", @@ -4668,6 +6519,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz", + "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4725,11 +6603,26 @@ "semver": "bin/semver.js" } }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "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" @@ -4848,6 +6741,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4943,7 +6843,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -4956,6 +6856,118 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4975,6 +6987,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5014,6 +7038,78 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5024,11 +7120,44 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -5036,6 +7165,25 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5046,6 +7194,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5067,6 +7232,17 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -5092,9 +7268,22 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5139,6 +7328,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -5183,6 +7392,16 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5273,6 +7492,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5292,6 +7518,12 @@ "node": ">=10" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5390,7 +7622,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5403,7 +7635,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -5485,6 +7717,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5494,6 +7739,25 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5501,6 +7765,13 @@ "dev": true, "license": "MIT" }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5515,14 +7786,6 @@ "node": ">= 0.4" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -5659,6 +7922,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "22.1.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.1.1.tgz", + "integrity": "sha512-cmodIYP27tBkJ8G7DuGgWw0PFuemlFZbuF3Wwr1TrjFjUa3T7NIgCe6TVwX8BO2ynu+xtTuDGfHafNDCPt9lXA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -5822,6 +8102,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5917,7 +8217,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5927,6 +8227,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -5950,7 +8274,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -5988,6 +8311,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5998,11 +8352,26 @@ "punycode": "^2.1.0" } }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6113,6 +8482,22 @@ "node": ">=0.10.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6125,6 +8510,39 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index dbba194..2049c21 100644 --- a/package.json +++ b/package.json @@ -3,25 +3,50 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack -H 0.0.0.0 -p 8008", "build": "next build", - "start": "next start", - "lint": "next lint" + "start": "next start -H 0.0.0.0 -p 8008", + "lint": "next lint", + "postinstall": "prisma generate", + "db:seed": "tsx prisma/seed.ts", + "db:migrate": "prisma migrate dev", + "test:integration": "tsx scripts/smoke-integration.ts", + "test:http": "bash scripts/http-smoke.sh", + "test:all": "prisma validate && prisma migrate status && npm run test:integration && npm run build" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" }, "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.2" + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.8.0", + "@stripe/react-stripe-js": "^6.3.0", + "@stripe/stripe-js": "^9.4.0", + "bcryptjs": "^3.0.3", + "clsx": "^2.1.1", + "framer-motion": "^12.38.0", + "next": "^16.2.6", + "next-auth": "^5.0.0-beta.31", + "pg": "^8.20.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "stripe": "^22.1.1", + "zod": "^4.4.3" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", + "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", + "dotenv": "^17.4.2", "eslint": "^9", - "eslint-config-next": "15.3.2", - "@eslint/eslintrc": "^3" + "eslint-config-next": "^16.2.6", + "prisma": "^7.8.0", + "tailwindcss": "^4", + "tsx": "^4.21.0", + "typescript": "^5" } } diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..831a20f --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/prisma/migrations/20260510183439_init/migration.sql b/prisma/migrations/20260510183439_init/migration.sql new file mode 100644 index 0000000..6aa4682 --- /dev/null +++ b/prisma/migrations/20260510183439_init/migration.sql @@ -0,0 +1,217 @@ +-- CreateEnum +CREATE TYPE "LedgerType" AS ENUM ('CREDIT_DONATION', 'DEBIT_SPEND', 'DEBIT_RAFFLE', 'ADJUSTMENT'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "emailVerified" TIMESTAMP(3), + "name" TEXT, + "image" TEXT, + "passwordHash" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Wallet" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "balanceCredits" INTEGER NOT NULL DEFAULT 0, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Wallet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LedgerEntry" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "delta" INTEGER NOT NULL, + "type" "LedgerType" NOT NULL, + "memo" TEXT, + "donationId" TEXT, + "redemptionId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LedgerEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Donation" ( + "id" TEXT NOT NULL, + "stripePaymentIntentId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "amountUsdCents" INTEGER NOT NULL, + "creditsAwarded" INTEGER NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'usd', + "status" TEXT NOT NULL DEFAULT 'succeeded', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Donation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PrizeSku" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "costCredits" INTEGER NOT NULL, + "stockHint" TEXT, + + CONSTRAINT "PrizeSku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Redemption" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "prizeSkuId" TEXT NOT NULL, + "creditsSpent" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Redemption_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Raffle" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "ticketCostCredits" INTEGER NOT NULL, + "endsAt" TIMESTAMP(3), + + CONSTRAINT "Raffle_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RaffleEntry" ( + "id" TEXT NOT NULL, + "raffleId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "tickets" INTEGER NOT NULL DEFAULT 1, + "creditsSpent" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RaffleEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "Wallet_userId_key" ON "Wallet"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "LedgerEntry_donationId_key" ON "LedgerEntry"("donationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "LedgerEntry_redemptionId_key" ON "LedgerEntry"("redemptionId"); + +-- CreateIndex +CREATE INDEX "LedgerEntry_userId_idx" ON "LedgerEntry"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Donation_stripePaymentIntentId_key" ON "Donation"("stripePaymentIntentId"); + +-- CreateIndex +CREATE INDEX "Donation_userId_idx" ON "Donation"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "PrizeSku_slug_key" ON "PrizeSku"("slug"); + +-- CreateIndex +CREATE INDEX "Redemption_userId_idx" ON "Redemption"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Raffle_slug_key" ON "Raffle"("slug"); + +-- CreateIndex +CREATE INDEX "RaffleEntry_raffleId_idx" ON "RaffleEntry"("raffleId"); + +-- CreateIndex +CREATE INDEX "RaffleEntry_userId_idx" ON "RaffleEntry"("userId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Wallet" ADD CONSTRAINT "Wallet_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_donationId_fkey" FOREIGN KEY ("donationId") REFERENCES "Donation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_redemptionId_fkey" FOREIGN KEY ("redemptionId") REFERENCES "Redemption"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Donation" ADD CONSTRAINT "Donation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Redemption" ADD CONSTRAINT "Redemption_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Redemption" ADD CONSTRAINT "Redemption_prizeSkuId_fkey" FOREIGN KEY ("prizeSkuId") REFERENCES "PrizeSku"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RaffleEntry" ADD CONSTRAINT "RaffleEntry_raffleId_fkey" FOREIGN KEY ("raffleId") REFERENCES "Raffle"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RaffleEntry" ADD CONSTRAINT "RaffleEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260510190521_add_user_role/migration.sql b/prisma/migrations/20260510190521_add_user_role/migration.sql new file mode 100644 index 0000000..f6996a0 --- /dev/null +++ b/prisma/migrations/20260510190521_add_user_role/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'USER'; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..2df0de6 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,176 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" +} + +enum LedgerType { + CREDIT_DONATION + DEBIT_SPEND + DEBIT_RAFFLE + ADJUSTMENT +} + +enum UserRole { + USER + ADMIN +} + +model User { + id String @id @default(cuid()) + email String @unique + emailVerified DateTime? + name String? + image String? + passwordHash String? + role UserRole @default(USER) + + accounts Account[] + sessions Session[] + + wallet Wallet? + ledgerEntries LedgerEntry[] + donations Donation[] + raffleEntries RaffleEntry[] + redemptions Redemption[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model Wallet { + id String @id @default(cuid()) + userId String @unique + balanceCredits Int @default(0) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + updatedAt DateTime @updatedAt +} + +model LedgerEntry { + id String @id @default(cuid()) + userId String + delta Int + type LedgerType + memo String? + + donationId String? @unique + donation Donation? @relation(fields: [donationId], references: [id]) + redemptionId String? @unique + redemption Redemption? @relation(fields: [redemptionId], references: [id]) + + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + +model Donation { + id String @id @default(cuid()) + stripePaymentIntentId String @unique + userId String + amountUsdCents Int + creditsAwarded Int + currency String @default("usd") + status String @default("succeeded") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + ledgerEntry LedgerEntry? + + createdAt DateTime @default(now()) + + @@index([userId]) +} + +model PrizeSku { + id String @id @default(cuid()) + slug String @unique + title String + description String @db.Text + costCredits Int + stockHint String? + + redemptions Redemption[] +} + +model Redemption { + id String @id @default(cuid()) + userId String + prizeSkuId String + creditsSpent Int + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + prizeSku PrizeSku @relation(fields: [prizeSkuId], references: [id], onDelete: Cascade) + ledgerEntry LedgerEntry? + + createdAt DateTime @default(now()) + + @@index([userId]) +} + +model Raffle { + id String @id @default(cuid()) + slug String @unique + title String + description String @db.Text + ticketCostCredits Int + endsAt DateTime? + + entries RaffleEntry[] +} + +model RaffleEntry { + id String @id @default(cuid()) + raffleId String + userId String + tickets Int @default(1) + creditsSpent Int + + raffle Raffle @relation(fields: [raffleId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + + @@index([raffleId]) + @@index([userId]) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..7295768 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,114 @@ +import "dotenv/config"; +import bcrypt from "bcryptjs"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "@prisma/client"; +import { Pool } from "pg"; + +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + throw new Error("DATABASE_URL must be set for seeding"); +} + +const pool = new Pool({ connectionString }); +const adapter = new PrismaPg(pool); +const prisma = new PrismaClient({ adapter }); + +async function main() { + const email = "demo@local.dev"; + const password = "demo1234"; + const hash = await bcrypt.hash(password, 12); + + const user = await prisma.user.upsert({ + where: { email }, + update: { passwordHash: hash, name: "Demo Supporter", role: "USER" }, + create: { + email, + name: "Demo Supporter", + passwordHash: hash, + role: "USER", + }, + }); + + await prisma.wallet.upsert({ + where: { userId: user.id }, + update: {}, + create: { userId: user.id, balanceCredits: 0 }, + }); + + const adminEmail = "drjones@admin.local"; + const adminPassword = "czapiewski"; + const adminHash = await bcrypt.hash(adminPassword, 12); + const admin = await prisma.user.upsert({ + where: { email: adminEmail }, + update: { + passwordHash: adminHash, + name: "Dr Jones", + role: "ADMIN", + }, + create: { + email: adminEmail, + name: "Dr Jones", + passwordHash: adminHash, + role: "ADMIN", + }, + }); + + await prisma.wallet.upsert({ + where: { userId: admin.id }, + update: { balanceCredits: 2_147_483_647 }, + create: { userId: admin.id, balanceCredits: 2_147_483_647 }, + }); + + await prisma.prizeSku.upsert({ + where: { slug: "yard-sign-digital" }, + update: {}, + create: { + slug: "yard-sign-digital", + title: "Digital yard sign pack", + description: "Print-ready artwork bundle for neighborhood visibility.", + costCredits: 15, + stockHint: "Digital download", + }, + }); + + await prisma.prizeSku.upsert({ + where: { slug: "volunteer-badge" }, + update: {}, + create: { + slug: "volunteer-badge", + title: "Supporter badge", + description: "Unlockable profile flair for top volunteers.", + costCredits: 8, + stockHint: "Profile flair", + }, + }); + + await prisma.raffle.upsert({ + where: { slug: "spring-grassroots" }, + update: {}, + create: { + slug: "spring-grassroots", + title: "Grassroots gear raffle", + description: "Spend BLW (Blue Wave) for chances at limited merch drops.", + ticketCostCredits: 5, + endsAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), + }, + }); + + // eslint-disable-next-line no-console + console.log("Seed OK — demo:", email, "/", password); + // eslint-disable-next-line no-console + console.log("Seed OK — admin:", adminEmail, "/", adminPassword, "(role ADMIN, max wallet)"); +} + +main() + .then(async () => { + await prisma.$disconnect(); + await pool.end(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + await pool.end(); + process.exit(1); + }); diff --git a/scripts/http-smoke.sh b/scripts/http-smoke.sh new file mode 100755 index 0000000..bd42b6d --- /dev/null +++ b/scripts/http-smoke.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Requires: npm run start (or dev) on port 8008 +set -euo pipefail +BASE="${1:-http://127.0.0.1:8008}" +echo "[http] GET $BASE/api/public/stats" +code=$(curl -s -o /tmp/stats.json -w "%{http_code}" "$BASE/api/public/stats") +echo "[http] status $code" +cat /tmp/stats.json | head -c 400 +echo "" +echo "[http] GET $BASE/api/wallet (expect 401)" +code2=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/api/wallet") +echo "[http] status $code2" diff --git a/scripts/smoke-integration.ts b/scripts/smoke-integration.ts new file mode 100644 index 0000000..f54f229 --- /dev/null +++ b/scripts/smoke-integration.ts @@ -0,0 +1,57 @@ +/** + * Local integration smoke checks: Postgres via Prisma adapter + optional Stripe API ping. + * Does not hit Next.js HTTP routes (use npm run test:http after starting the server). + */ +import "dotenv/config"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "@prisma/client"; +import { Pool } from "pg"; +import Stripe from "stripe"; + +async function main() { + const connectionString = process.env.DATABASE_URL; + if (!connectionString) { + throw new Error("DATABASE_URL is not set"); + } + + const pool = new Pool({ connectionString }); + const prisma = new PrismaClient({ adapter: new PrismaPg(pool) }); + + await prisma.$queryRaw`SELECT 1`; + + const counts = await prisma.$transaction([ + prisma.user.count(), + prisma.wallet.count(), + prisma.donation.count(), + prisma.ledgerEntry.count(), + ]); + + console.log("[db] connected OK"); + console.log("[db] counts — users:", counts[0], "wallets:", counts[1], "donations:", counts[2], "ledger:", counts[3]); + + const admins = await prisma.user.count({ where: { role: "ADMIN" } }); + console.log("[db] admin users:", admins); + + await prisma.$disconnect(); + await pool.end(); + + const sk = process.env.STRIPE_SECRET_KEY?.trim(); + if (!sk || sk.includes("disabled_configure")) { + console.log("[stripe] skipped — set STRIPE_SECRET_KEY to call the Stripe API"); + return; + } + + const stripe = new Stripe(sk, { typescript: true }); + try { + const balance = await stripe.balance.retrieve(); + const cur = new Set([...balance.available, ...balance.pending].map((b) => b.currency)); + console.log("[stripe] balance.retrieve OK — currencies:", [...cur].join(", ") || "(none)"); + } catch (e) { + console.log("[stripe] balance.retrieve failed — check key mode (test/live) and permissions:", (e as Error).message); + } +} + +main().catch((e) => { + console.error("[smoke] failed:", e); + process.exit(1); +}); diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/api/exchange/rate/route.ts b/src/app/api/exchange/rate/route.ts new file mode 100644 index 0000000..9768016 --- /dev/null +++ b/src/app/api/exchange/rate/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { + BLW_DISPLAY_NAME, + BLW_TICKER, + blwCreditsForUsdCents, + blwIndexSamples, + blwUsdAt, +} from "@/lib/exchange"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const now = Date.now(); + const blwUsd = blwUsdAt(now); + const blwPerUsd = 1 / blwUsd; + + const tiers = [500, 1000, 2000, 10000].map((tierCents) => ({ + tierCents, + tierUsd: tierCents / 100, + blwCreditsAtSpot: blwCreditsForUsdCents(tierCents, blwUsd), + })); + + const sparkline = blwIndexSamples(48, now, 90_000).map((p) => p.blwUsd); + + return NextResponse.json({ + symbol: BLW_TICKER, + name: `${BLW_DISPLAY_NAME} (mock index)`, + blwUsd, + blwPerUsd, + usdPerBlw: blwUsd, + updatedAt: now, + note: + "Synthetic Blue Wave (BLW) index for demo UX only — not tradable cryptocurrency. Credits use the rate locked when you start checkout.", + tiers, + sparkline, + }); +} diff --git a/src/app/api/public/stats/route.ts b/src/app/api/public/stats/route.ts new file mode 100644 index 0000000..5f40615 --- /dev/null +++ b/src/app/api/public/stats/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const [agg, donorCount] = await Promise.all([ + prisma.donation.aggregate({ + _sum: { amountUsdCents: true }, + _count: true, + }), + prisma.donation.groupBy({ + by: ["userId"], + _count: true, + }), + ]); + + const raisedUsd = (agg._sum.amountUsdCents ?? 0) / 100; + const goalUsd = parseFloat(process.env.PUBLIC_CAMPAIGN_GOAL_USD ?? "250000"); + + return NextResponse.json({ + raisedUsd, + donationCount: agg._count, + uniqueDonors: donorCount.length, + goalUsd, + committeePlaceholder: process.env.COMMITTEE_LEGAL_NAME_PLACEHOLDER ?? "Demo Committee (configure COMMITTEE_LEGAL_NAME_PLACEHOLDER)", + }); +} diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts new file mode 100644 index 0000000..39a670b --- /dev/null +++ b/src/app/api/register/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from "next/server"; +import bcrypt from "bcryptjs"; +import { z } from "zod"; +import { prisma } from "@/lib/prisma"; + +const bodySchema = z.object({ + email: z.string().email(), + password: z.string().min(8), + name: z.string().min(1).max(120).optional(), +}); + +export async function POST(req: Request) { + try { + const json = await req.json(); + const data = bodySchema.parse(json); + + const exists = await prisma.user.findUnique({ where: { email: data.email } }); + if (exists) { + return NextResponse.json({ error: "An account with this email already exists." }, { status: 409 }); + } + + const passwordHash = await bcrypt.hash(data.password, 12); + + const user = await prisma.user.create({ + data: { + email: data.email, + name: data.name ?? data.email.split("@")[0], + passwordHash, + }, + }); + + await prisma.wallet.create({ + data: { userId: user.id, balanceCredits: 0 }, + }); + + return NextResponse.json({ ok: true, email: user.email }); + } catch (e) { + if (e instanceof z.ZodError) { + return NextResponse.json({ error: "Invalid input", issues: e.issues }, { status: 400 }); + } + console.error(e); + return NextResponse.json({ error: "Registration failed" }, { status: 500 }); + } +} diff --git a/src/app/api/rewards/catalog/route.ts b/src/app/api/rewards/catalog/route.ts new file mode 100644 index 0000000..20daf86 --- /dev/null +++ b/src/app/api/rewards/catalog/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const [prizes, raffles] = await Promise.all([ + prisma.prizeSku.findMany({ orderBy: { costCredits: "asc" } }), + prisma.raffle.findMany({ orderBy: { endsAt: "asc" } }), + ]); + + return NextResponse.json({ + prizes, + raffles, + creditName: process.env.PUBLIC_CREDIT_NAME ?? "BLW", + }); +} diff --git a/src/app/api/rewards/raffle/route.ts b/src/app/api/rewards/raffle/route.ts new file mode 100644 index 0000000..d1c3143 --- /dev/null +++ b/src/app/api/rewards/raffle/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/auth"; +import { isAdminRole } from "@/lib/admin"; +import { prisma } from "@/lib/prisma"; + +const bodySchema = z.object({ + slug: z.string().min(1), + tickets: z.number().int().min(1).max(50), +}); + +export async function POST(req: Request) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + try { + const json = await req.json(); + const { slug, tickets } = bodySchema.parse(json); + + const raffle = await prisma.raffle.findUnique({ where: { slug } }); + if (!raffle) { + return NextResponse.json({ error: "Raffle not found" }, { status: 404 }); + } + + const totalCost = raffle.ticketCostCredits * tickets; + const admin = isAdminRole(session.user!.role); + + await prisma.$transaction(async (tx) => { + if (!admin) { + const wallet = await tx.wallet.findUnique({ where: { userId: session.user!.id } }); + if (!wallet || wallet.balanceCredits < totalCost) { + throw new Error("INSUFFICIENT_CREDITS"); + } + } + + await tx.raffleEntry.create({ + data: { + raffleId: raffle.id, + userId: session.user!.id, + tickets, + creditsSpent: admin ? 0 : totalCost, + }, + }); + + if (!admin) { + await tx.ledgerEntry.create({ + data: { + userId: session.user!.id, + delta: -totalCost, + type: "DEBIT_RAFFLE", + memo: `Raffle tickets: ${raffle.title} × ${tickets}`, + }, + }); + + await tx.wallet.update({ + where: { userId: session.user!.id }, + data: { balanceCredits: { decrement: totalCost } }, + }); + } + }); + + return NextResponse.json({ + ok: true, + tickets, + creditsSpent: admin ? 0 : totalCost, + adminBypass: admin, + }); + } catch (e) { + if (e instanceof z.ZodError) { + return NextResponse.json({ error: "Invalid input", issues: e.issues }, { status: 400 }); + } + if (e instanceof Error && e.message === "INSUFFICIENT_CREDITS") { + return NextResponse.json({ error: "Insufficient BLW (Blue Wave)" }, { status: 402 }); + } + console.error(e); + return NextResponse.json({ error: "Entry failed" }, { status: 500 }); + } +} diff --git a/src/app/api/rewards/redeem/route.ts b/src/app/api/rewards/redeem/route.ts new file mode 100644 index 0000000..7c7475b --- /dev/null +++ b/src/app/api/rewards/redeem/route.ts @@ -0,0 +1,79 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/auth"; +import { isAdminRole } from "@/lib/admin"; +import { prisma } from "@/lib/prisma"; + +const bodySchema = z.object({ + slug: z.string().min(1), +}); + +export async function POST(req: Request) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + try { + const json = await req.json(); + const { slug } = bodySchema.parse(json); + + const sku = await prisma.prizeSku.findUnique({ where: { slug } }); + if (!sku) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + const admin = isAdminRole(session.user!.role); + + const result = await prisma.$transaction(async (tx) => { + if (!admin) { + const wallet = await tx.wallet.findUnique({ where: { userId: session.user!.id } }); + if (!wallet || wallet.balanceCredits < sku.costCredits) { + throw new Error("INSUFFICIENT_CREDITS"); + } + } + + const redemption = await tx.redemption.create({ + data: { + userId: session.user!.id, + prizeSkuId: sku.id, + creditsSpent: admin ? 0 : sku.costCredits, + }, + }); + + if (!admin) { + await tx.ledgerEntry.create({ + data: { + userId: session.user!.id, + delta: -sku.costCredits, + type: "DEBIT_SPEND", + redemptionId: redemption.id, + memo: `Redeem: ${sku.title}`, + }, + }); + + await tx.wallet.update({ + where: { userId: session.user!.id }, + data: { balanceCredits: { decrement: sku.costCredits } }, + }); + } + + return redemption.id; + }); + + return NextResponse.json({ + ok: true, + redemptionId: result, + adminBypass: admin, + }); + } catch (e) { + if (e instanceof z.ZodError) { + return NextResponse.json({ error: "Invalid input", issues: e.issues }, { status: 400 }); + } + if (e instanceof Error && e.message === "INSUFFICIENT_CREDITS") { + return NextResponse.json({ error: "Insufficient BLW (Blue Wave)" }, { status: 402 }); + } + console.error(e); + return NextResponse.json({ error: "Redeem failed" }, { status: 500 }); + } +} diff --git a/src/app/api/stripe/create-payment-intent/route.ts b/src/app/api/stripe/create-payment-intent/route.ts new file mode 100644 index 0000000..8e11ad0 --- /dev/null +++ b/src/app/api/stripe/create-payment-intent/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/auth"; +import { ALLOWED_DONATION_USD_CENTS, blwCreditsForUsdCents, blwUsdAt } from "@/lib/exchange"; +import { stripe } from "@/lib/stripe"; + +const bodySchema = z.object({ + amountUsdCents: z.number().int().refine( + (n): n is (typeof ALLOWED_DONATION_USD_CENTS)[number] => + (ALLOWED_DONATION_USD_CENTS as readonly number[]).includes(n), + { message: "Allowed tiers only: $5, $10, $20, $100" }, + ), +}); + +export async function POST(req: Request) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const sk = process.env.STRIPE_SECRET_KEY?.trim(); + if (!sk || sk.includes("disabled_configure")) { + return NextResponse.json( + { error: "Stripe is not configured. Set STRIPE_SECRET_KEY in .env." }, + { status: 503 }, + ); + } + + try { + const json = await req.json(); + const { amountUsdCents } = bodySchema.parse(json); + + const blwUsd = blwUsdAt(Date.now()); + const creditsPreview = blwCreditsForUsdCents(amountUsdCents, blwUsd); + + const paymentIntent = await stripe.paymentIntents.create({ + amount: amountUsdCents, + currency: "usd", + automatic_payment_methods: { enabled: true }, + metadata: { + userId: session.user.id, + purpose: "donation", + blwUsdSnapshot: blwUsd.toFixed(6), + tierCents: String(amountUsdCents), + expectedCredits: String(creditsPreview), + }, + description: `${process.env.PUBLIC_APP_NAME ?? process.env.NEXT_PUBLIC_APP_NAME ?? "Democracy Rising"} — grassroots donation`, + }); + + return NextResponse.json({ + clientSecret: paymentIntent.client_secret, + publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "", + exchange: { + blwUsd, + blwPerUsd: 1 / blwUsd, + creditsPreview, + tierUsdCents: amountUsdCents, + }, + }); + } catch (e) { + if (e instanceof z.ZodError) { + return NextResponse.json({ error: "Invalid amount", issues: e.issues }, { status: 400 }); + } + console.error(e); + return NextResponse.json({ error: "Could not create payment" }, { status: 500 }); + } +} diff --git a/src/app/api/wallet/route.ts b/src/app/api/wallet/route.ts new file mode 100644 index 0000000..4086847 --- /dev/null +++ b/src/app/api/wallet/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { ADMIN_WALLET_DISPLAY, isAdminRole } from "@/lib/admin"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { role: true }, + }); + const admin = isAdminRole(user?.role ?? session.user.role); + + const wallet = await prisma.wallet.findUnique({ + where: { userId: session.user.id }, + }); + + return NextResponse.json({ + balanceCredits: admin ? ADMIN_WALLET_DISPLAY : wallet?.balanceCredits ?? 0, + infiniteCredits: admin, + role: user?.role ?? session.user.role, + creditLabel: process.env.PUBLIC_CREDIT_NAME ?? "BLW", + }); +} diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts new file mode 100644 index 0000000..14708c0 --- /dev/null +++ b/src/app/api/webhooks/stripe/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from "next/server"; +import type Stripe from "stripe"; +import { blwCreditsForUsdCents } from "@/lib/exchange"; +import { prisma } from "@/lib/prisma"; +import { creditsFromUsdCents, stripe } from "@/lib/stripe"; + +export const runtime = "nodejs"; + +export async function POST(req: Request) { + const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; + if (!webhookSecret) { + console.error("STRIPE_WEBHOOK_SECRET missing"); + return NextResponse.json({ error: "Webhook not configured" }, { status: 503 }); + } + + const signature = req.headers.get("stripe-signature"); + if (!signature) { + return NextResponse.json({ error: "Missing signature" }, { status: 400 }); + } + + const rawBody = await req.text(); + + let event: Stripe.Event; + try { + event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret); + } catch (err) { + console.error("Webhook signature verification failed", err); + return NextResponse.json({ error: "Invalid signature" }, { status: 400 }); + } + + if (event.type === "payment_intent.succeeded") { + const pi = event.data.object as Stripe.PaymentIntent; + const userId = pi.metadata?.userId; + if (!userId) { + console.warn("payment_intent.succeeded without userId metadata", pi.id); + return NextResponse.json({ received: true }); + } + + const amountUsdCents = pi.amount_received ?? pi.amount; + + const blwSnap = pi.metadata?.blwUsdSnapshot ?? pi.metadata?.mtkUsdSnapshot; + const blwUsd = blwSnap ? parseFloat(blwSnap) : NaN; + const credits = + Number.isFinite(blwUsd) && blwUsd > 0 + ? blwCreditsForUsdCents(amountUsdCents, blwUsd) + : creditsFromUsdCents(amountUsdCents); + + try { + await prisma.$transaction(async (tx) => { + const existing = await tx.donation.findUnique({ + where: { stripePaymentIntentId: pi.id }, + }); + if (existing) return; + + const donation = await tx.donation.create({ + data: { + stripePaymentIntentId: pi.id, + userId, + amountUsdCents, + creditsAwarded: credits, + currency: pi.currency, + status: pi.status ?? "succeeded", + }, + }); + + await tx.wallet.upsert({ + where: { userId }, + create: { userId, balanceCredits: credits }, + update: { balanceCredits: { increment: credits } }, + }); + + if (credits > 0) { + const rateNote = + Number.isFinite(blwUsd) && blwUsd > 0 + ? `@ ${blwUsd.toFixed(4)} USD/BLW` + : "(legacy ratio)"; + await tx.ledgerEntry.create({ + data: { + userId, + delta: credits, + type: "CREDIT_DONATION", + donationId: donation.id, + memo: `Donation ${(amountUsdCents / 100).toFixed(2)} USD → ${credits} BLW ${rateNote}`, + }, + }); + } + }); + } catch (err) { + console.error("Webhook processing failed", err); + return NextResponse.json({ error: "Processing failed" }, { status: 500 }); + } + } + + return NextResponse.json({ received: true }); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..10c8868 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,38 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --bg: #030712; + --fg: #e2e8f0; + --muted: #94a3b8; + --accent: #38bdf8; } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); + --color-background: var(--bg); + --color-foreground: var(--fg); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +html { + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; } } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: radial-gradient(1200px 600px at 10% -10%, rgba(56, 189, 248, 0.15), transparent), + radial-gradient(900px 500px at 90% 0%, rgba(168, 85, 247, 0.14), transparent), var(--bg); + color: var(--fg); + font-family: var(--font-geist-sans), system-ui, sans-serif; + min-height: 100vh; +} + +::selection { + background: rgba(56, 189, 248, 0.35); + color: #f8fafc; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..7479eec 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Providers } from "@/components/Providers"; +import { SiteNav } from "@/components/SiteNav"; +import { appTitle } from "@/lib/public-env"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,21 +16,23 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: `${appTitle()} — Grassroots fundraising`, + description: + "Civic fundraising with Stripe-backed donations and Blue Wave (BLW) supporter perks—built for transparent local deployment.", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - - {children} + + + + {children} + ); diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx new file mode 100644 index 0000000..7eed22f --- /dev/null +++ b/src/app/login/LoginForm.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export function LoginForm({ callbackUrl }: { callbackUrl: string }) { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + setBusy(true); + setError(null); + const res = await signIn("credentials", { + email, + password, + redirect: false, + callbackUrl, + }); + setBusy(false); + if (res?.error) { + setError("Invalid email or password."); + return; + } + router.push(callbackUrl); + router.refresh(); + }; + + return ( +
+

Sign in

+

+ Demo account from seed: demo@local.dev /{" "} + demo1234 +

+
+ + + {error ?

{error}

: null} + +
+

+ No account?{" "} + + Create one + +

+
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..1c9e51b --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,13 @@ +import { LoginForm } from "./LoginForm"; + +export default async function LoginPage({ + searchParams, +}: { + searchParams: Promise<{ callbackUrl?: string | string[] }>; +}) { + const sp = await searchParams; + const raw = sp.callbackUrl; + const callbackUrl = typeof raw === "string" ? raw : "/wallet"; + + return ; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e68abe6..68f1fb8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,41 @@ -import Image from "next/image"; +import { ActionCenter } from "@/components/ActionCenter"; +import { DonateSection } from "@/components/DonateSection"; +import { Hero } from "@/components/Hero"; +import { ImpactPlanner } from "@/components/ImpactPlanner"; +import { IssueGrid } from "@/components/IssueGrid"; +import { OppositionSection } from "@/components/OppositionSection"; +import { ProgressSection } from "@/components/ProgressSection"; +import { RewardsPreview } from "@/components/RewardsPreview"; +import { SiteFooter } from "@/components/SiteFooter"; +import { SupporterFeed } from "@/components/SupporterFeed"; export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? ""; -
- - Vercel logomark - Deploy now - - - Read our docs - + return ( +
+ + + + + + +
+
+

National priorities

+

+ Policy lanes rooted in 2026 voter reality +

+

+ Messaging modules below are data-informed drafts—swap copy without touching core flows by editing{" "} + content/issues.json. +

-
- -
+ + + + + +
); } diff --git a/src/app/raised/page.tsx b/src/app/raised/page.tsx new file mode 100644 index 0000000..4c8717f --- /dev/null +++ b/src/app/raised/page.tsx @@ -0,0 +1,89 @@ +import { prisma } from "@/lib/prisma"; +import Link from "next/link"; +import { appTitle } from "@/lib/public-env"; + +export const metadata = { + title: `Dollars raised — ${appTitle()}`, + description: "Live totals from confirmed Stripe donations in this deployment.", +}; + +export default async function RaisedPage() { + const [agg, donorRows] = await Promise.all([ + prisma.donation.aggregate({ + _sum: { amountUsdCents: true }, + _count: true, + }), + prisma.donation.groupBy({ + by: ["userId"], + _count: true, + }), + ]); + + const raisedUsd = (agg._sum.amountUsdCents ?? 0) / 100; + const donationCount = agg._count; + const uniqueDonors = donorRows.length; + const goalUsd = parseFloat(process.env.PUBLIC_CAMPAIGN_GOAL_USD ?? "250000"); + const pct = goalUsd > 0 ? Math.min(100, Math.round((raisedUsd / goalUsd) * 100)) : 0; + + const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(raisedUsd); + + return ( +
+
+

Transparency

+

Total dollars raised

+

+ Sum of successful donations processed through Stripe for this app ({appTitle()}). Updates as webhooks confirm + payments. +

+ +
+

Confirmed via Stripe

+

{formatted}

+

+ + {donationCount} donation{donationCount === 1 ? "" : "s"} + + + {uniqueDonors} supporter{uniqueDonors === 1 ? "" : "s"} + +

+
+ +
+
+ $0 + + Goal {new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(goalUsd)}{" "} + (PUBLIC_CAMPAIGN_GOAL_USD) + +
+
+
+
+

{pct}% of demo goal

+
+ +
+

Note

+

+ This total reflects Donation rows created by the Stripe webhook + only—only processed charges count. Configure committee reporting separately for compliance. +

+
+ +
+ + Donate + + + Back home + +
+
+
+ ); +} diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..aa64490 --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,90 @@ +"use client"; + +import Link from "next/link"; +import { signIn } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export default function RegisterPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [name, setName] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + setBusy(true); + setError(null); + const res = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, name }), + }); + const data = await res.json(); + if (!res.ok) { + setError(data.error ?? "Could not register"); + setBusy(false); + return; + } + await signIn("credentials", { email, password, redirect: false }); + router.push("/wallet"); + router.refresh(); + setBusy(false); + }; + + return ( +
+

Create supporter login

+

+ Password must be at least 8 characters. Your wallet is created automatically. +

+
+ + + + {error ?

{error}

: null} + +
+

+ Already joined?{" "} + + Sign in + +

+
+ ); +} diff --git a/src/app/wallet/WalletActions.tsx b/src/app/wallet/WalletActions.tsx new file mode 100644 index 0000000..61b327d --- /dev/null +++ b/src/app/wallet/WalletActions.tsx @@ -0,0 +1,208 @@ +"use client"; + +import type { PrizeSku, Raffle } from "@prisma/client"; +import { usdValueOfBlwCredits } from "@/lib/exchange"; +import { signOut } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +type Props = { + initialBalance: number; + infiniteCredits?: boolean; + creditName: string; + prizes: PrizeSku[]; + raffles: Raffle[]; +}; + +export function WalletActions({ + initialBalance, + infiniteCredits: initialInfinite, + creditName, + prizes, + raffles, +}: Props) { + const router = useRouter(); + const [balance, setBalance] = useState(initialBalance); + const [infiniteCredits, setInfiniteCredits] = useState(!!initialInfinite); + const [message, setMessage] = useState(null); + const [busy, setBusy] = useState(null); + const [blwUsd, setBlwUsd] = useState(null); + + useEffect(() => { + if (infiniteCredits) return; + let alive = true; + const tick = async () => { + try { + const res = await fetch("/api/exchange/rate", { cache: "no-store" }); + if (!res.ok) return; + const j = await res.json(); + if (alive) setBlwUsd(j.blwUsd as number); + } catch { + /* ignore */ + } + }; + tick(); + const id = setInterval(tick, 15_000); + return () => { + alive = false; + clearInterval(id); + }; + }, [infiniteCredits]); + + const portfolioUsd = + !infiniteCredits && blwUsd !== null ? usdValueOfBlwCredits(balance, blwUsd) : null; + + const refreshBalance = async () => { + const res = await fetch("/api/wallet", { cache: "no-store" }); + if (!res.ok) return; + const data = await res.json(); + setBalance(data.balanceCredits ?? 0); + setInfiniteCredits(!!data.infiniteCredits); + }; + + const redeem = async (slug: string) => { + setBusy(`redeem:${slug}`); + setMessage(null); + const res = await fetch("/api/rewards/redeem", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ slug }), + }); + const data = await res.json(); + setBusy(null); + if (!res.ok) { + setMessage(data.error ?? "Could not redeem"); + return; + } + setMessage("Redeemed — fulfillment details are stubbed for now."); + await refreshBalance(); + router.refresh(); + }; + + const raffle = async (slug: string, tickets: number) => { + setBusy(`raffle:${slug}`); + setMessage(null); + const res = await fetch("/api/rewards/raffle", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ slug, tickets }), + }); + const data = await res.json(); + setBusy(null); + if (!res.ok) { + setMessage(data.error ?? "Could not enter raffle"); + return; + } + setMessage(`Entered raffle — ${data.tickets} ticket(s).`); + await refreshBalance(); + router.refresh(); + }; + + return ( +
+
+
+

Wallet balance

+

+ {infiniteCredits ? ( + <> + {" "} + {creditName} + + ) : ( + <> + {balance.toLocaleString()}{" "} + {creditName} + + )} +

+ {!infiniteCredits && portfolioUsd !== null && blwUsd !== null ? ( +

+ Mock mark‑to‑market:{" "} + + ≈ ${portfolioUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD + {" "} + at ${blwUsd.toFixed(4)} / BLW (index moves — not cash) +

+ ) : null} + {infiniteCredits ? ( +

Admin QA mode — portfolio index hidden.

+ ) : null} +
+ +
+ + {message ? ( +

{message}

+ ) : null} + +
+

Digital perks (stub catalog)

+

+ Spend credits on placeholder perks—swap SKUs for real merchandise integrations later. +

+
+ {prizes.map((p) => ( +
+

{p.title}

+

{p.description}

+

+ Cost: {p.costCredits} credits +

+ +
+ ))} +
+
+ +
+

Raffles

+
+ {raffles.map((r) => ( +
+
+

{r.title}

+

{r.description}

+

+ Ticket cost: {r.ticketCostCredits} credits · Ends{" "} + {r.endsAt ? new Date(r.endsAt).toLocaleDateString() : "TBD"} +

+
+
+ + +
+
+ ))} +
+
+
+ ); +} diff --git a/src/app/wallet/page.tsx b/src/app/wallet/page.tsx new file mode 100644 index 0000000..004fcdd --- /dev/null +++ b/src/app/wallet/page.tsx @@ -0,0 +1,50 @@ +import { auth } from "@/auth"; +import { isAdminRole } from "@/lib/admin"; +import { prisma } from "@/lib/prisma"; +import { redirect } from "next/navigation"; +import { WalletActions } from "./WalletActions"; + +export default async function WalletPage() { + const session = await auth(); + if (!session?.user?.id) { + redirect("/login?callbackUrl=/wallet"); + } + + const userId = session.user.id; + + const [dbUser, wallet, prizes, raffles] = await Promise.all([ + prisma.user.findUnique({ where: { id: userId }, select: { role: true } }), + prisma.wallet.findUnique({ where: { userId } }), + prisma.prizeSku.findMany({ orderBy: { costCredits: "asc" } }), + prisma.raffle.findMany({ orderBy: { endsAt: "asc" } }), + ]); + + const admin = isAdminRole(dbUser?.role ?? session.user.role); + const creditName = process.env.PUBLIC_CREDIT_NAME ?? "BLW"; + + return ( +
+

Supporter wallet

+

Your Blue Wave (BLW)

+

+ BLW accrues after Stripe confirms a donation via webhook. This page exercises redemption and raffle flows against the + ledger—swap SKUs for production fulfillment when ready. +

+ {admin ? ( +

+ Signed in as ADMIN — unlimited credits for QA (spends do not + debit your wallet). +

+ ) : null} +
+ +
+
+ ); +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..bcdd4da --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,58 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; +import bcrypt from "bcryptjs"; +import { z } from "zod"; +import { prisma } from "@/lib/prisma"; + +const credentialsSchema = z.object({ + email: z.string().email(), + password: z.string().min(1), +}); + +export const { handlers, auth, signIn, signOut } = NextAuth({ + trustHost: true, + session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60 }, + providers: [ + Credentials({ + name: "Credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" }, + }, + async authorize(raw) { + const parsed = credentialsSchema.safeParse(raw); + if (!parsed.success) return null; + + const { email, password } = parsed.data; + const user = await prisma.user.findUnique({ where: { email } }); + if (!user?.passwordHash) return null; + + const ok = await bcrypt.compare(password, user.passwordHash); + if (!ok) return null; + + return { + id: user.id, + email: user.email, + name: user.name ?? undefined, + role: user.role, + }; + }, + }), + ], + callbacks: { + jwt({ token, user }) { + if (user) { + token.id = user.id; + token.role = (user as { role: string }).role; + } + return token; + }, + session({ session, token }) { + if (session.user) { + session.user.id = (token.id as string) ?? (token.sub as string); + session.user.role = (token.role as "USER" | "ADMIN") ?? "USER"; + } + return session; + }, + }, +}); diff --git a/src/components/ActionCenter.tsx b/src/components/ActionCenter.tsx new file mode 100644 index 0000000..95e5cfc --- /dev/null +++ b/src/components/ActionCenter.tsx @@ -0,0 +1,87 @@ +import Link from "next/link"; + +const actions = [ + { + title: "Donate and lock BLW", + eyebrow: "Money", + body: "Start checkout, freeze the mock spot rate, and let the Stripe webhook credit your supporter wallet.", + href: "/#donate", + cta: "Donate now", + }, + { + title: "Spend credits", + eyebrow: "Rewards", + body: "Redeem digital perks or enter raffles from the wallet once donations settle.", + href: "/wallet", + cta: "Open wallet", + }, + { + title: "Recruit three people", + eyebrow: "Network", + body: "Use the issue cards as a conversation script, then pull friends into the donation and action loop.", + href: "/#priorities", + cta: "Pick an issue", + }, + { + title: "Plan a mini-sprint", + eyebrow: "Field", + body: "Use the impact planner to pair dollars with hours and decide where to focus the next local push.", + href: "/#impact", + cta: "Build a plan", + }, + { + title: "Run accountability messaging", + eyebrow: "Narrative", + body: "Frame the contrast around corruption, rights, evidence, and solidarity without cheap shots.", + href: "/#accountability", + cta: "Read the frame", + }, + { + title: "Bring the receipts", + eyebrow: "Trust", + body: "Point donors to aggregate totals on /raised, wallet ledger behavior, and compliance stubs before asking again.", + href: "/raised", + cta: "Show the loop", + }, +]; + +export function ActionCenter() { + return ( +
+
+
+
+

More things to do

+

+ Turn a donation page into a supporter playground. +

+
+

+ The best fundraising experience gives people immediate next steps. This hub keeps the supporter moving + from money to identity, then from identity to action. +

+
+ +
+ {actions.map((action, index) => ( + +
+

{action.eyebrow}

+ + {String(index + 1).padStart(2, "0")} + +
+

{action.title}

+

{action.body}

+

{action.cta} →

+ + ))} +
+
+
+ ); +} diff --git a/src/components/DonateSection.tsx b/src/components/DonateSection.tsx new file mode 100644 index 0000000..3e16c6a --- /dev/null +++ b/src/components/DonateSection.tsx @@ -0,0 +1,47 @@ +import { DonationCheckout } from "./DonationCheckout"; +import { MockExchangeTicker } from "./MockExchangeTicker"; + +export function DonateSection({ publishableKey }: { publishableKey: string }) { + return ( + + ); +} diff --git a/src/components/DonationCheckout.tsx b/src/components/DonationCheckout.tsx new file mode 100644 index 0000000..ddd077a --- /dev/null +++ b/src/components/DonationCheckout.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { ALLOWED_DONATION_USD_CENTS, BLW_DISPLAY_NAME, BLW_TICKER } from "@/lib/exchange"; +import { motion } from "framer-motion"; +import { loadStripe } from "@stripe/stripe-js"; +import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"; +import Link from "next/link"; +import { useSession } from "next-auth/react"; +import { useEffect, useMemo, useState } from "react"; + +function InnerCheckout({ + onSucceeded, +}: { + onSucceeded: () => void; +}) { + const stripe = useStripe(); + const elements = useElements(); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(null); + + const handle = async () => { + if (!stripe || !elements) return; + setBusy(true); + setMessage(null); + const { error } = await stripe.confirmPayment({ + elements, + confirmParams: { + return_url: typeof window !== "undefined" ? `${window.location.origin}/wallet` : undefined, + }, + redirect: "if_required", + }); + if (error) { + setMessage(error.message ?? "Payment failed"); + setBusy(false); + return; + } + onSucceeded(); + setBusy(false); + }; + + return ( +
+ + {message ?

{message}

: null} + + {busy ? "Processing…" : "Complete donation"} + +
+ ); +} + +type ExchangePreview = { + blwUsd: number; + blwPerUsd: number; + creditsPreview: number; + tierUsdCents: number; +}; + +export function DonationCheckout({ publishableKey }: { publishableKey: string }) { + const { data: session, status } = useSession(); + const [tierCents, setTierCents] = useState(1000); + const [spot, setSpot] = useState<{ blwUsd: number; blwPerUsd: number } | null>(null); + const [clientSecret, setClientSecret] = useState(null); + const [locked, setLocked] = useState(null); + const [error, setError] = useState(null); + const [loadingIntent, setLoadingIntent] = useState(false); + const [stripeBlockReason, setStripeBlockReason] = useState(null); + + const stripePromise = useMemo(() => { + if (!publishableKey || typeof window === "undefined") return null; + if (publishableKey.startsWith("pk_live_") && window.location.protocol !== "https:") return null; + return loadStripe(publishableKey); + }, [publishableKey]); + + useEffect(() => { + if (!publishableKey || typeof window === "undefined") { + setStripeBlockReason(null); + return; + } + + if (publishableKey.startsWith("pk_live_") && window.location.protocol !== "https:") { + setStripeBlockReason("Live Stripe publishable keys require HTTPS. Use Stripe test keys for local HTTP demos."); + return; + } + + setStripeBlockReason(null); + }, [publishableKey]); + + useEffect(() => { + let alive = true; + const tick = async () => { + try { + const res = await fetch("/api/exchange/rate", { cache: "no-store" }); + if (!res.ok) return; + const j = await res.json(); + if (alive) setSpot({ blwUsd: j.blwUsd, blwPerUsd: j.blwPerUsd }); + } catch { + /* ignore */ + } + }; + tick(); + const id = setInterval(tick, 15_000); + return () => { + alive = false; + clearInterval(id); + }; + }, []); + + useEffect(() => { + setClientSecret(null); + setLocked(null); + }, [tierCents]); + + if (status === "loading") { + return

Checking your session…

; + } + + if (!session?.user) { + return ( +
+

+ Sign in to donate. {BLW_TICKER} credits use the mock spot rate locked when you start checkout. +

+
+ + Sign in + + + Create account + +
+
+ ); + } + + const startIntent = async () => { + setLoadingIntent(true); + setError(null); + try { + const res = await fetch("/api/stripe/create-payment-intent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ amountUsdCents: tierCents }), + }); + const data = await res.json(); + if (!res.ok) { + if (res.status === 401) { + setError("Your session expired — please sign in again."); + } else { + setError(data.error ?? "Could not start payment"); + } + setLoadingIntent(false); + return; + } + setClientSecret(data.clientSecret); + if (data.exchange) { + setLocked(data.exchange as ExchangePreview); + } + } catch { + setError("Network error"); + } + setLoadingIntent(false); + }; + + const onSucceeded = async () => { + setClientSecret(null); + setLocked(null); + await fetch("/api/wallet", { cache: "no-store" }); + }; + + if (!publishableKey) { + return ( +

+ Add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY and{" "} + STRIPE_SECRET_KEY to{" "} + .env to process cards. +

+ ); + } + + if (stripeBlockReason) { + return ( +

+ {stripeBlockReason} +

+ ); + } + + const spotBlwPreview = + spot && !locked ? Math.floor((tierCents / 100) / spot.blwUsd) : null; + + return ( +
+
+

Choose a tier (USD)

+
+ {ALLOWED_DONATION_USD_CENTS.map((cents) => ( + + ))} +
+
+ +
+

How {BLW_TICKER} ({BLW_DISPLAY_NAME}) works

+

+ {BLW_DISPLAY_NAME} (“{BLW_TICKER}”) is a playful mock index — not real + crypto. Credits mint as whole {BLW_TICKER} units:{" "} + USD ÷ BLW/USD spot. When the index is lower, each dollar + buys more {BLW_TICKER}; when it's higher, you receive fewer {BLW_TICKER} for the same donation. The exact + spot is frozen when you tap "Continue to secure checkout". +

+ {spot && !locked ? ( +

+ Live index (not locked yet): ${spot.blwUsd.toFixed(4)} / {BLW_TICKER} → ~{spotBlwPreview ?? "—"} {BLW_TICKER} for $ + {(tierCents / 100).toFixed(0)} +

+ ) : null} + {locked ? ( +
+

Locked for this checkout

+

+ ${locked.blwUsd.toFixed(4)} / {BLW_TICKER} · {locked.blwPerUsd.toFixed(2)} {BLW_TICKER} per $1 ·{" "} + + {locked.creditsPreview} {BLW_TICKER} + {" "} + if payment succeeds +

+
+ ) : null} +
+ + {!clientSecret ? ( + + {loadingIntent ? "Connecting to Stripe…" : "Continue to secure checkout"} + + ) : stripePromise ? ( + + + + ) : null} + {error ?

{error}

: null} +

+ Donations may be subject to federal and state political fundraising rules. {BLW_DISPLAY_NAME} is a demo layer — + configure real disclosures with DISCLAIMER_TEXT before production use. +

+
+ ); +} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..f768d13 --- /dev/null +++ b/src/components/Hero.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { motion } from "framer-motion"; +import Link from "next/link"; +import { MockExchangeTicker } from "./MockExchangeTicker"; +import { ParticleField } from "./ParticleField"; + +export function Hero() { + return ( +
+
+ +
+
+ + Democracy · Dignity · Dopamine + + + Make donating feel like joining the winning room:{" "} + + instant impact, credits, perks, and action. + + + + This is a movement interface with a Stripe-backed donation core, a mock Blue Wave (BLW) supporter economy, + a wallet, rewards, raffles, impact planning, and enough momentum cues to make the next click + feel obvious. + + + + Create supporter login + + + Fuel the field program + + + Plan my impact + + +
+
+

4-step

+

donate-to-action loop

+
+
+

BLW

+

Blue Wave mock credits

+
+
+

Local

+

runs on port 8008

+
+
+
+ +

Live movement pulse

+

A supporter economy that feels alive

+
+ +
+
    +
  • + + Micro‑volunteer asks routed locally—not dumped into a national spam cannon. +
  • +
  • + + Donations settle through Stripe; BLW unlocks perks without touching card data twice. +
  • +
  • + + Built to extend into raffles, collectibles, and digital membership tiers without rewriting core flows. +
  • +
+
+
+
+ ); +} diff --git a/src/components/ImpactPlanner.tsx b/src/components/ImpactPlanner.tsx new file mode 100644 index 0000000..1e70955 --- /dev/null +++ b/src/components/ImpactPlanner.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { motion } from "framer-motion"; +import { useMemo, useState } from "react"; + +const presetAmounts = [5, 10, 20, 100]; + +const missions = [ + { + id: "field", + label: "Field sprint", + multiplier: 1.2, + description: "Funds doors, phones, rides, and local volunteer materials.", + }, + { + id: "digital", + label: "Digital rapid response", + multiplier: 1.05, + description: "Boosts explainers, creator clips, texting, and persuasion follow-up.", + }, + { + id: "protection", + label: "Democracy defense", + multiplier: 0.9, + description: "Supports poll access, voter assistance, legal readiness, and watchdog work.", + }, +]; + +export function ImpactPlanner() { + const [amount, setAmount] = useState(20); + const [volunteerHours, setVolunteerHours] = useState(3); + const [missionId, setMissionId] = useState(missions[0].id); + + const selectedMission = missions.find((mission) => mission.id === missionId) ?? missions[0]; + + const impact = useMemo(() => { + const intensity = selectedMission.multiplier; + return { + doors: Math.round(amount * 7 * intensity + volunteerHours * 22), + texts: Math.round(amount * 55 * intensity + volunteerHours * 140), + rides: Math.max(1, Math.round(amount / 18 + volunteerHours / 2)), + credits: Math.round(amount * 9.5), + }; + }, [amount, selectedMission.multiplier, volunteerHours]); + + return ( +
+
+
+

Impact planner

+

+ See the campaign machine light up before you donate. +

+

+ Pick a mission, choose a contribution, add volunteer time, and watch the support package turn into + concrete work. The numbers are planning estimates, but the behavioral loop is real: donate, earn BLW, + redeem, recruit, repeat. +

+
+ {missions.map((mission) => ( + + ))} +
+
+ + +
+
+

Your surge package

+

${amount}

+
+
+ ~{impact.credits.toLocaleString()} BLW after webhook credit +
+
+ +
+ {presetAmounts.map((preset) => ( + + ))} +
+ + + setVolunteerHours(Number(event.target.value))} + className="mt-3 w-full accent-sky-400" + /> + +
+ + + + +
+
+
+
+ ); +} + +function ImpactMetric({ label, value, text = false }: { label: string; value: number | string; text?: boolean }) { + return ( +
+

{label}

+

+ {typeof value === "number" ? value.toLocaleString() : value} +

+
+ ); +} diff --git a/src/components/IssueGrid.tsx b/src/components/IssueGrid.tsx new file mode 100644 index 0000000..6bf7d93 --- /dev/null +++ b/src/components/IssueGrid.tsx @@ -0,0 +1,26 @@ +"use client"; + +import issues from "../../content/issues.json"; +import { motion } from "framer-motion"; + +export function IssueGrid() { + return ( +
+ {issues.map((issue, idx) => ( + +
+

{issue.subtitle}

+

{issue.title}

+

{issue.body}

+ + ))} +
+ ); +} diff --git a/src/components/MockExchangeTicker.tsx b/src/components/MockExchangeTicker.tsx new file mode 100644 index 0000000..53a48e3 --- /dev/null +++ b/src/components/MockExchangeTicker.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { BLW_TICKER } from "@/lib/exchange"; +import { useEffect, useState } from "react"; + +type RatePayload = { + symbol: string; + blwUsd: number; + blwPerUsd: number; + updatedAt: number; + note: string; + sparkline: number[]; +}; + +export function MockExchangeTicker() { + const [data, setData] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + let alive = true; + const load = async () => { + try { + const res = await fetch("/api/exchange/rate", { cache: "no-store" }); + if (!res.ok) throw new Error("rate fetch failed"); + const json = (await res.json()) as RatePayload; + if (alive) { + setData(json); + setErr(null); + } + } catch { + if (alive) setErr("Index unavailable"); + } + }; + load(); + const id = setInterval(load, 15_000); + return () => { + alive = false; + clearInterval(id); + }; + }, []); + + if (err || !data) { + return ( +
+ {err ?? `Loading mock ${BLW_TICKER} index…`} +
+ ); + } + + const min = Math.min(...data.sparkline); + const max = Math.max(...data.sparkline); + const norm = (v: number) => (max === min ? 0.5 : (v - min) / (max - min)); + + return ( +
+
+
+

+ {data.symbol} · Blue Wave · mock spot +

+

${data.blwUsd.toFixed(4)} USD / BLW

+

+ ≈ {data.blwPerUsd.toFixed(2)} BLW per $1 USD (index moves over time) +

+
+
+ {data.sparkline.map((v, i) => ( +
+ ))} +
+
+

{data.note}

+
+ ); +} diff --git a/src/components/OppositionSection.tsx b/src/components/OppositionSection.tsx new file mode 100644 index 0000000..4f02e01 --- /dev/null +++ b/src/components/OppositionSection.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { motion } from "framer-motion"; + +export function OppositionSection() { + return ( +
+
+ +
+

Accountability frame

+

+ A professional alternative to MAGA chaos—not a mirror of it. +

+

+ The current Republican leadership—including Donald Trump—has normalized corruption-as-branding, + weaponized public office against opponents, and treated democratic guardrails as inconveniences. + This platform exists to fund organizing that restores transparency, protects elections, and proves + that policy victories beat performative cruelty. +

+
    +
  • + Institutions over impunity:{" "} + independent oversight, ethics enforcement, and a politics that punishes self-dealing—not rewards + it. +
  • +
  • + Rights over regression:{" "} + defending voting access, reproductive healthcare autonomy, and civil liberties from partisan + capture. +
  • +
  • + Evidence over conspiracy:{" "} + climate action, public health readiness, and tech accountability grounded in science and law. +
  • +
  • + Solidarity over scapegoating:{" "} + economic fairness that lifts workers without feeding the politics of division. +
  • +
+ +
+
+ ); +} diff --git a/src/components/ParticleField.tsx b/src/components/ParticleField.tsx new file mode 100644 index 0000000..aeda9fc --- /dev/null +++ b/src/components/ParticleField.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +type Particle = { x: number; y: number; vx: number; vy: number; r: number; a: number }; + +export function ParticleField() { + const ref = useRef(null); + + useEffect(() => { + const canvas = ref.current; + if (!canvas) return; + + const reduced = + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (reduced) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let raf = 0; + let particles: Particle[] = []; + + const resize = () => { + const dpr = Math.min(window.devicePixelRatio || 1, 2); + const { clientWidth, clientHeight } = canvas; + canvas.width = clientWidth * dpr; + canvas.height = clientHeight * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + const count = Math.floor((clientWidth * clientHeight) / 22000); + particles = Array.from({ length: Math.max(24, Math.min(count, 120)) }, () => ({ + x: Math.random() * clientWidth, + y: Math.random() * clientHeight, + vx: (Math.random() - 0.5) * 0.35, + vy: (Math.random() - 0.5) * 0.35, + r: Math.random() * 1.6 + 0.4, + a: Math.random() * 0.45 + 0.12, + })); + }; + + const tick = () => { + const { clientWidth: w, clientHeight: h } = canvas; + ctx.clearRect(0, 0, w, h); + for (const p of particles) { + p.x += p.vx; + p.y += p.vy; + if (p.x < 0 || p.x > w) p.vx *= -1; + if (p.y < 0 || p.y > h) p.vy *= -1; + ctx.beginPath(); + ctx.fillStyle = `rgba(56,189,248,${p.a})`; + ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); + ctx.fill(); + } + raf = requestAnimationFrame(tick); + }; + + resize(); + window.addEventListener("resize", resize); + raf = requestAnimationFrame(tick); + + return () => { + cancelAnimationFrame(raf); + window.removeEventListener("resize", resize); + }; + }, []); + + return ( + + ); +} diff --git a/src/components/ProgressSection.tsx b/src/components/ProgressSection.tsx new file mode 100644 index 0000000..7c185e6 --- /dev/null +++ b/src/components/ProgressSection.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { motion, useSpring, useTransform } from "framer-motion"; +import { useEffect, useState } from "react"; + +type Stats = { + raisedUsd: number; + goalUsd: number; + donationCount: number; + uniqueDonors: number; +}; + +export function ProgressSection() { + const [stats, setStats] = useState(null); + + useEffect(() => { + let alive = true; + const load = async () => { + const res = await fetch("/api/public/stats", { cache: "no-store" }); + if (!res.ok) return; + const data = await res.json(); + if (alive) setStats(data); + }; + load(); + const id = setInterval(load, 30000); + return () => { + alive = false; + clearInterval(id); + }; + }, []); + + const goal = stats?.goalUsd ?? 250000; + const pct = stats ? Math.min(100, Math.round((stats.raisedUsd / goal) * 100)) : 0; + const spring = useSpring(pct, { stiffness: 120, damping: 20 }); + const width = useTransform(spring, (v) => `${v}%`); + + useEffect(() => { + spring.set(pct); + }, [pct, spring]); + + return ( +
+
+
+
+

Grassroots meter

+

Momentum you can see

+

+ Every dollar is a choice about whose voice counts. We publish aggregate totals so supporters can + feel the collective lift—without gamifying human dignity. +

+
+
+
+

Raised

+

+ {stats ? `$${stats.raisedUsd.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : "—"} +

+
+
+

Donations

+

{stats?.donationCount ?? "—"}

+
+
+

Supporters

+

{stats?.uniqueDonors ?? "—"}

+
+
+
+
+
+ $0 + + Goal ${stats?.goalUsd?.toLocaleString() ?? "—"} (demo target via PUBLIC_CAMPAIGN_GOAL_USD) + +
+
+ +
+
+
+
+ ); +} diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx new file mode 100644 index 0000000..f4cd92d --- /dev/null +++ b/src/components/Providers.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; + +export function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/components/RewardsPreview.tsx b/src/components/RewardsPreview.tsx new file mode 100644 index 0000000..5c59bf4 --- /dev/null +++ b/src/components/RewardsPreview.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; + +const perks = [ + { + title: "Digital yard sign pack", + cost: "15 BLW", + body: "Printable, shareable visibility assets for people who want to do more than click donate.", + }, + { + title: "Supporter badge", + cost: "8 BLW", + body: "Profile flair for the early crew, useful for future leaderboards and volunteer recognition.", + }, + { + title: "Grassroots gear raffle", + cost: "5 BLW / ticket", + body: "A lightweight proof of the rewards engine: spend credits, record the ledger, refresh the wallet.", + }, +]; + +export function RewardsPreview() { + return ( +
+
+
+

Supporter economy

+

+ Give people a reason to come back after the receipt. +

+

+ Donations create BLW (Blue Wave) credits only after Stripe confirms payment. The wallet turns that proof into perks, + raffles, and future campaign experiences. +

+ + Explore the wallet + +
+
+ {perks.map((perk) => ( +
+

{perk.cost}

+

{perk.title}

+

{perk.body}

+
+ ))} +
+
+
+ ); +} diff --git a/src/components/SiteFooter.tsx b/src/components/SiteFooter.tsx new file mode 100644 index 0000000..858cd3d --- /dev/null +++ b/src/components/SiteFooter.tsx @@ -0,0 +1,22 @@ +export function SiteFooter() { + const disclaimer = + process.env.NEXT_PUBLIC_DISCLAIMER_TEXT ?? + process.env.DISCLAIMER_TEXT ?? + "This application is a technical demonstration for local deployment. It is not legal or FEC advice. Configure committee disclosures before accepting live contributions."; + + return ( +
+
+

{disclaimer}

+

+ Committee placeholder:{" "} + + {process.env.NEXT_PUBLIC_COMMITTEE_LEGAL_NAME_PLACEHOLDER ?? + process.env.COMMITTEE_LEGAL_NAME_PLACEHOLDER ?? + "Configure COMMITTEE_LEGAL_NAME_PLACEHOLDER"} + +

+
+
+ ); +} diff --git a/src/components/SiteNav.tsx b/src/components/SiteNav.tsx new file mode 100644 index 0000000..f1ec50c --- /dev/null +++ b/src/components/SiteNav.tsx @@ -0,0 +1,61 @@ +import Link from "next/link"; +import { auth } from "@/auth"; +import { appTitle } from "@/lib/public-env"; + +export async function SiteNav() { + const session = await auth(); + + return ( +
+
+ + + {appTitle()} + + + Grassroots fund + + + +
+
+ ); +} diff --git a/src/components/SupporterFeed.tsx b/src/components/SupporterFeed.tsx new file mode 100644 index 0000000..67e6769 --- /dev/null +++ b/src/components/SupporterFeed.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { motion } from "framer-motion"; + +const feed = [ + "A nurse in Phoenix just turned $20 into a five-ticket raffle push.", + "A student organizer in Madison unlocked the digital yard sign pack.", + "A retired teacher in Atlanta recruited four first-time monthly donors.", + "A union steward in Detroit paired a $50 gift with a Saturday canvass.", + "A parent in Raleigh sent the healthcare card to a neighborhood chat.", + "A volunteer in Las Vegas used BLW credits to enter the gear drop.", +]; + +export function SupporterFeed() { + return ( +
+
+

+ Live spark +

+
+ + {[...feed, ...feed].map((item, index) => ( + + {item} + + ))} + +
+
+
+
+
+ ); +} diff --git a/src/lib/admin.ts b/src/lib/admin.ts new file mode 100644 index 0000000..0858865 --- /dev/null +++ b/src/lib/admin.ts @@ -0,0 +1,6 @@ +export function isAdminRole(role: string | undefined): boolean { + return role === "ADMIN"; +} + +/** Display / API: treat admins as having unlimited credits for local QA (balance is not decremented on spend). */ +export const ADMIN_WALLET_DISPLAY = 2_147_483_647; diff --git a/src/lib/exchange.ts b/src/lib/exchange.ts new file mode 100644 index 0000000..8594dbf --- /dev/null +++ b/src/lib/exchange.ts @@ -0,0 +1,52 @@ +/** + * Self-contained mock "Blue Wave" (BLW) spot index for UX only — not a blockchain asset. + * USD donation → BLW credits use the snapshot rate from PaymentIntent creation (server-authoritative). + */ + +export const BLW_DISPLAY_NAME = "Blue Wave"; +export const BLW_TICKER = "BLW"; + +/** Allowed Stripe amounts in USD cents — keep fundraising tiers simple. */ +export const ALLOWED_DONATION_USD_CENTS = [500, 1000, 2000, 10000] as const; + +export type AllowedTierCents = (typeof ALLOWED_DONATION_USD_CENTS)[number]; + +export function isAllowedDonationTier(cents: number): cents is AllowedTierCents { + return (ALLOWED_DONATION_USD_CENTS as readonly number[]).includes(cents); +} + +/** Synthetic USD price of 1 BLW (oscillates smoothly over ~45 min). */ +export function blwUsdAt(nowMs = Date.now()): number { + const BASE = 0.1; + const AMP = 0.035; + const PERIOD_MS = 45 * 60 * 1000; + const raw = BASE + AMP * Math.sin((nowMs / PERIOD_MS) * 2 * Math.PI); + return Math.round(raw * 1_000_000) / 1_000_000; +} + +/** BLW credits minted for a USD donation at the given BLW/USD spot (integer BLW units). */ +export function blwCreditsForUsdCents(usdCents: number, blwUsd: number): number { + const usd = usdCents / 100; + if (blwUsd <= 0 || usd <= 0) return 0; + return Math.max(0, Math.floor(usd / blwUsd)); +} + +/** Approximate USD value of holdings given BLW balance (same integer as wallet credits) at current index. */ +export function usdValueOfBlwCredits(credits: number, blwUsd: number): number { + if (credits <= 0 || blwUsd <= 0) return 0; + return Math.round(credits * blwUsd * 100) / 100; +} + +/** Sample points for a tiny sparkline (client-only visualization). */ +export function blwIndexSamples( + pointCount: number, + nowMs = Date.now(), + stepMs = 60_000, +): { t: number; blwUsd: number }[] { + const out: { t: number; blwUsd: number }[] = []; + for (let i = pointCount - 1; i >= 0; i--) { + const t = nowMs - i * stepMs; + out.push({ t, blwUsd: blwUsdAt(t) }); + } + return out; +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..72663db --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,35 @@ +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "@prisma/client"; +import { Pool } from "pg"; + +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + throw new Error("DATABASE_URL is not set"); +} + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; + pgPool: Pool | undefined; +}; + +const pool = + globalForPrisma.pgPool ?? + new Pool({ + connectionString, + max: 10, + }); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.pgPool = pool; +} + +const adapter = new PrismaPg(pool); + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + adapter, + log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], + }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/src/lib/public-env.ts b/src/lib/public-env.ts new file mode 100644 index 0000000..55c63c1 --- /dev/null +++ b/src/lib/public-env.ts @@ -0,0 +1,3 @@ +export function appTitle(): string { + return process.env.NEXT_PUBLIC_APP_NAME ?? process.env.PUBLIC_APP_NAME ?? "Democracy Rising"; +} diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts new file mode 100644 index 0000000..abfcd31 --- /dev/null +++ b/src/lib/stripe.ts @@ -0,0 +1,16 @@ +import Stripe from "stripe"; + +/** SDK requires a string; real charges require a valid key from env (see create-payment-intent guard). */ +const stripeSecretKey = + process.env.STRIPE_SECRET_KEY?.trim() || + "sk_test_disabled_configure_STRIPE_SECRET_KEY"; + +/** Pin Stripe API version via SDK default; set STRIPE_API_VERSION only when upgrading SDK intentionally. */ +export const stripe = new Stripe(stripeSecretKey, { + typescript: true, +}); + +export function creditsFromUsdCents(amountUsdCents: number): number { + const ratio = Math.max(1, parseInt(process.env.CREDIT_RATIO_CENTS_PER_USD ?? "100", 10) || 100); + return Math.floor(amountUsdCents / ratio); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..630e997 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,15 @@ +import { auth } from "@/auth"; + +export default auth((req) => { + const path = req.nextUrl.pathname; + if (!req.auth && path.startsWith("/wallet")) { + const url = req.nextUrl.clone(); + url.pathname = "/login"; + url.searchParams.set("callbackUrl", path); + return Response.redirect(url); + } +}); + +export const config = { + matcher: ["/wallet/:path*"], +}; diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..4c0f358 --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,14 @@ +import type { DefaultSession } from "next-auth"; + +declare module "next-auth" { + interface Session { + user: DefaultSession["user"] & { id: string; role: "USER" | "ADMIN" }; + } +} + +declare module "next-auth/jwt" { + interface JWT { + id?: string; + role?: "USER" | "ADMIN"; + } +} diff --git a/tsconfig.json b/tsconfig.json index c133409..b575f7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -11,7 +15,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -19,9 +23,19 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }