diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..897bc17 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +node_modules +.next +.git +.gitignore +*.md +.env +.env.* +!.env.example +Dockerfile* +docker-compose*.yml +.dockerignore +*.db +*.db-journal +coverage +.vscode +.idea +src/generated/prisma + +# Uploads (volume en prod) — pas les embarquer dans l’image +public/uploads/*.png +public/uploads/*.jpg +public/uploads/*.jpeg +public/uploads/**/*.png +public/uploads/**/*.jpg +public/uploads/**/*.jpeg diff --git a/.gitignore b/.gitignore index 5ef6a52..ac503f9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,9 +33,19 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +# sqlite local +*.db +*.db-journal + +# photos uploadées (fichiers générés) +/public/uploads/* +!/public/uploads/.gitkeep + # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts + +/src/generated/prisma diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95fb4a3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-bookworm-slim AS deps +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +FROM node:22-bookworm-slim AS builder +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +ENV DATABASE_URL="file:./build-placeholder.db" +RUN npm rebuild better-sqlite3 +RUN npx prisma generate +RUN npx prisma migrate deploy +RUN npm run build + +FROM node:22-bookworm-slim AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates gosu \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/prisma.config.ts ./prisma.config.ts +COPY --from=builder /app/src/lib/password.ts ./src/lib/password.ts +COPY --from=builder /app/src/lib/constants.ts ./src/lib/constants.ts +COPY --from=builder /app/src/lib/site-content-defaults.ts ./src/lib/site-content-defaults.ts +COPY --from=builder /app/src/lib/week-calendar.ts ./src/lib/week-calendar.ts +COPY --from=builder /app/src/generated/prisma ./src/generated/prisma +COPY docker/entrypoint.sh /entrypoint.sh +RUN npm install prisma@7.5.0 dotenv@17.3.1 --omit=dev \ + && npm install -g tsx@4.21.0 \ + && chmod +x /entrypoint.sh \ + && mkdir -p /app/data /app/public/uploads \ + && chown -R nextjs:nodejs /app + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index e215bc4..59d617c 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,129 @@ -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). +# Gaufrement Bon — site vitrine -## Getting Started +Site vitrine et back-office pour **Gaufrement Bon** (gaufres artisanales à Bagnols-sur-Cèze) : page d’accueil, carte des gaufres, galerie photo, contenus éditables, et interface d’administration. -First, run the development server: +## Fonctionnalités + +- **Site public** : hero, menu par catégories (sucrées, salées, du mois), galerie, horaires et contact. +- **Administration** (`/admin`) : connexion par session, gestion du menu, de la galerie et des textes / images du site. +- **Upload d’images** : redimensionnement via Sharp, stockage dans `public/uploads/`. +- **Base SQLite** : Prisma 7, migrations versionnées, seed au premier démarrage Docker. + +## Stack + +- [Next.js 15](https://nextjs.org) (App Router, Turbopack, sortie `standalone`) +- React 19, Tailwind CSS 4 +- Prisma + SQLite (`better-sqlite3`) +- Auth JWT en cookie (`jose`), rate limiting (mémoire ou [Upstash Redis](https://upstash.com)) + +## Prérequis + +- Node.js 22+ +- npm + +## Développement local ```bash +npm install +npx prisma migrate deploy # crée dev.db si besoin +npm run db:seed # optionnel : données de démo + compte admin 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. +Ouvrir [http://localhost:3000](http://localhost:3000). L’admin est sur [http://localhost:3000/admin](http://localhost:3000/admin). -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Sans fichier `.env`, les valeurs par défaut suivantes s’appliquent : -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. +| Variable | Défaut local | +|----------|----------------| +| `DATABASE_URL` | `file:./dev.db` | +| `AUTH_SECRET` | secret de dev (à changer en prod) | +| `ADMIN_EMAIL` | `admin@lagaufredor.local` | +| `ADMIN_PASSWORD` | `changeme123` | -## Learn More +Exemple `.env` : -To learn more about Next.js, take a look at the following resources: +```env +DATABASE_URL="file:./dev.db" +AUTH_SECRET="remplacer-par-une-longue-chaine-aleatoire" +ADMIN_EMAIL="admin@example.com" +ADMIN_PASSWORD="mot-de-passe-fort" +# Optionnel — rate limiting distribué +# UPSTASH_REDIS_REST_URL= +# UPSTASH_REDIS_REST_TOKEN= +``` -- [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. +## Scripts npm -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +| Commande | Description | +|----------|-------------| +| `npm run dev` | Serveur de dev (port 3000) | +| `npm run build` | Build production (migrations + generate Prisma) | +| `npm run start` | Serveur Next en production | +| `npm run db:seed` | Seed manuel (menu, galerie, contenus, admin) | +| `npm run lint` | ESLint | -## Deploy on Vercel +## Docker -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. +Image publiée : `foufure/gauffre:latest`. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +### Compose local (build + run) + +```bash +export AUTH_SECRET="$(openssl rand -base64 32)" +docker compose up -d --build +``` + +Application sur [http://localhost:3007](http://localhost:3007) (port hôte `3007` → conteneur `3000`). + +Volumes : base SQLite (`gauffre-data`) et fichiers uploadés (`gauffre-uploads`). + +### Déploiement serveur + +Fichier dédié : `docker-compose.server.yml` (HTTPS assuré par Nginx + Certbot sur l’hôte). + +```bash +export AUTH_SECRET="$(openssl rand -base64 32)" +export ADMIN_EMAIL="admin@example.com" +export ADMIN_PASSWORD="mot-de-passe-fort" +docker compose -f docker-compose.server.yml up -d +``` + +Exemple de proxy Nginx : `proxy_pass http://127.0.0.1:3007;` +Port hôte configurable via `HOST_PORT` (défaut `3007`). + +Au démarrage du conteneur, `docker/entrypoint.sh` exécute les migrations Prisma et le seed, puis lance `node server.js`. + +### Publier l’image sur Docker Hub + +```bash +docker login +./scripts/docker-hub-push.sh # tag latest +./scripts/docker-hub-push.sh v1.0.0 # tag personnalisé +``` + +Variables optionnelles : `DOCKER_USER` (défaut `foufure`), `IMAGE_NAME` (défaut `gauffre`). + +## Structure (aperçu) + +``` +src/app/ # Pages et routes API (public + /admin) +src/components/site/ # UI vitrine +src/components/admin/ # UI back-office +src/lib/ # Prisma, auth, uploads, contenus +prisma/ # Schéma, migrations, seed +public/gallery/ # Images statiques par défaut +public/uploads/ # Images uploadées (persistées en Docker) +docker/ # Entrypoint conteneur +``` + +## Sécurité en production + +- Définir un `AUTH_SECRET` long et aléatoire (`openssl rand -base64 32`). +- Changer `ADMIN_EMAIL` et `ADMIN_PASSWORD` avant le premier seed, ou créer l’admin puis modifier le mot de passe. +- Servir le site derrière HTTPS (Nginx ou équivalent). +- Configurer Upstash si plusieurs instances partagent le rate limiting. + +## Licence + +Projet privé. diff --git a/docker-compose.server.yml b/docker-compose.server.yml new file mode 100644 index 0000000..6900916 --- /dev/null +++ b/docker-compose.server.yml @@ -0,0 +1,31 @@ +# Déploiement serveur : app seule ; HTTPS géré par Nginx + Certbot sur l’hôte. +# Exemple Nginx : proxy_pass http://127.0.0.1:3007; +# +# Usage : +# export AUTH_SECRET="$(openssl rand -base64 32)" +# docker compose -f docker-compose.server.yml up -d + +name: gauffre-server + +services: + app: + image: foufure/gauffre:latest + build: . + ports: + - "${HOST_PORT:-3007}:3000" + environment: + DATABASE_URL: file:/app/data/app.db + AUTH_SECRET: ${AUTH_SECRET:-change-me-in-production-use-long-random-string} + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@lagaufredor.local} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-changeme123} + NODE_ENV: production + UPSTASH_REDIS_REST_URL: ${UPSTASH_REDIS_REST_URL:-} + UPSTASH_REDIS_REST_TOKEN: ${UPSTASH_REDIS_REST_TOKEN:-} + volumes: + - gauffre-data:/app/data + - gauffre-uploads:/app/public/uploads + restart: unless-stopped + +volumes: + gauffre-data: + gauffre-uploads: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4dcf1a9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + app: + image: foufure/gauffre:latest + build: . + ports: + - "3007:3000" + environment: + DATABASE_URL: file:/app/data/app.db + AUTH_SECRET: ${AUTH_SECRET:-change-me-in-production-use-long-random-string} + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@lagaufredor.local} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-changeme123} + # Optionnel : rate limiting distribué (sinon fallback mémoire) + UPSTASH_REDIS_REST_URL: ${UPSTASH_REDIS_REST_URL:-} + UPSTASH_REDIS_REST_TOKEN: ${UPSTASH_REDIS_REST_TOKEN:-} + volumes: + - gauffre-data:/app/data + - gauffre-uploads:/app/public/uploads + restart: unless-stopped + +volumes: + gauffre-data: + gauffre-uploads: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..d172f06 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -e +if [ "$(id -u)" = 0 ]; then + mkdir -p /app/data /app/public/uploads + chown -R nextjs:nodejs /app/data /app/public/uploads + exec gosu nextjs:nodejs "$0" "$@" +fi + +if [ -z "$DATABASE_URL" ]; then + echo "DATABASE_URL doit être défini (ex. file:/app/data/app.db)" >&2 + exit 1 +fi +case "$DATABASE_URL" in + file:*) + dbpath="${DATABASE_URL#file:}" + dir=$(dirname "$dbpath") + if [ "$dir" != "." ] && [ -n "$dir" ]; then + mkdir -p "$dir" + fi + ;; +esac +npx prisma migrate deploy +tsx prisma/seed.ts +exec node server.js diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 1757ccc..ef4a675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,20 +7,34 @@ "": { "name": "gauffre", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { + "@prisma/adapter-better-sqlite3": "^7.5.0", + "@prisma/client": "^7.5.0", + "@upstash/ratelimit": "^2.0.8", + "@upstash/redis": "^1.37.0", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.8.0", + "dotenv": "^17.3.1", + "jose": "^6.2.2", "next": "15.5.14", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "sharp": "^0.34.5" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.5.14", + "prisma": "^7.5.0", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } }, @@ -37,6 +51,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", + "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", + "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", + "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -70,6 +151,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "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 +737,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -271,7 +807,6 @@ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -782,6 +1317,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", + "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, "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", @@ -987,6 +1536,193 @@ "node": ">=12.4.0" } }, + "node_modules/@prisma/adapter-better-sqlite3": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-better-sqlite3/-/adapter-better-sqlite3-7.5.0.tgz", + "integrity": "sha512-ThP6y1cAZW/BdHuuTKzO+j8vzEzXDMZaDPmboJyrkdbJvO9LRiHdnG5LNKAht8YYwjHgQoq7G7NtKbaW7NebVQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.5.0", + "better-sqlite3": "^12.6.0" + } + }, + "node_modules/@prisma/client": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.5.0.tgz", + "integrity": "sha512-h4hF9ctp+kSRs7ENHGsFQmHAgHcfkOCxbYt6Ti9Xi8x7D+kP4tTi9x51UKmiTH/OqdyJAO+8V+r+JA5AWdav7w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.5.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.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.5.0.tgz", + "integrity": "sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.5.0.tgz", + "integrity": "sha512-1J/9YEX7A889xM46PYg9e8VAuSL1IUmXJW3tEhMv7XQHDWlfC9YSkIw9sTYRaq5GswGlxZ+GnnyiNsUZ9JJhSQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.5.0.tgz", + "integrity": "sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", + "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.15", + "@electric-sql/pglite-socket": "0.0.20", + "@electric-sql/pglite-tools": "0.2.20", + "@hono/node-server": "1.19.9", + "@mrleebo/prisma-ast": "0.13.1", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "4.11.4", + "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.5.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.5.0.tgz", + "integrity": "sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.5.0.tgz", + "integrity": "sha512-ondGRhzoaVpRWvFaQ5wH5zS1BIbhzbKqczKjCn6j3L0Zfe/LInjcEg8+xtB49AuZBX30qyx1ZtGoootUohz2pw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0", + "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", + "@prisma/fetch-engine": "7.5.0", + "@prisma/get-platform": "7.5.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e.tgz", + "integrity": "sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.5.0.tgz", + "integrity": "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.5.0.tgz", + "integrity": "sha512-kZCl2FV54qnyrVdnII8MI6qvt7HfU6Cbiz8dZ8PXz4f4lbSw45jEB9/gEMK2SGdiNhBKyk/Wv95uthoLhGMLYA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.0", + "@prisma/engines-version": "7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e", + "@prisma/get-platform": "7.5.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.5.0.tgz", + "integrity": "sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.5.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/studio-core": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.21.1.tgz", + "integrity": "sha512-bOGqG/eMQtKC0XVvcVLRmhWWzm/I+0QUWqAEhEBtetpuS3k3V4IWqKGUONkAIT223DNXJMxMtZp36b1FmcdPeg==", + "devOptional": true, + "license": "Apache-2.0", + "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/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1001,6 +1737,13 @@ "dev": true, "license": "MIT" }, + "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/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1292,6 +2035,23 @@ "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/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1327,7 +2087,7 @@ "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" @@ -1894,6 +2654,39 @@ "win32" ] }, + "node_modules/@upstash/core-analytics": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.10.tgz", + "integrity": "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==", + "license": "MIT", + "dependencies": { + "@upstash/redis": "^1.28.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@upstash/ratelimit": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-2.0.8.tgz", + "integrity": "sha512-YSTMBJ1YIxsoPkUMX/P4DDks/xV5YYCswWMamU8ZIfK9ly6ppjRnVOyBhMDXBmzjODm4UQKcxsJPvaeFAijp5w==", + "license": "MIT", + "dependencies": { + "@upstash/core-analytics": "^0.0.10" + }, + "peerDependencies": { + "@upstash/redis": "^1.34.3" + } + }, + "node_modules/@upstash/redis": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.37.0.tgz", + "integrity": "sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2160,6 +2953,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.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", @@ -2187,6 +2990,69 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/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-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2211,6 +3077,72 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2308,6 +3240,53 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2341,11 +3320,28 @@ "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/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/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", @@ -2360,7 +3356,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": { @@ -2442,6 +3438,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2449,6 +3469,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", @@ -2485,11 +3515,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "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", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2508,6 +3561,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "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", @@ -2523,6 +3588,17 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2530,6 +3606,25 @@ "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -2722,6 +3817,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3166,6 +4303,45 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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", @@ -3240,6 +4416,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3307,6 +4489,44 @@ "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/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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", @@ -3348,6 +4568,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", @@ -3383,6 +4613,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", @@ -3428,6 +4665,30 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3488,9 +4749,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", @@ -3585,6 +4860,60 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3622,6 +4951,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3894,6 +5235,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", @@ -4050,7 +5398,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": { @@ -4075,12 +5423,21 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4456,6 +5813,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4472,6 +5839,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4479,6 +5853,13 @@ "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", @@ -4492,6 +5873,22 @@ "loose-envify": "cli.js" } }, + "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", @@ -4536,6 +5933,18 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4553,12 +5962,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4566,6 +5980,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.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4584,6 +6032,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -4687,6 +6141,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -4716,6 +6182,38 @@ "semver": "bin/semver.js" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4839,6 +6337,22 @@ "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4934,7 +6448,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" @@ -4947,6 +6461,20 @@ "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": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4966,6 +6494,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/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", @@ -5005,6 +6545,47 @@ "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/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5015,6 +6596,40 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.5.0.tgz", + "integrity": "sha512-n30qZpWehaYQzigLjmuPisyEsvOzHt7bZeRyg8gZ5DvJo9FGjD+gNaY59Ns3hlLD5/jZH5GBeftIss0jDbUoLg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.5.0", + "@prisma/dev": "0.20.0", + "@prisma/engines": "7.5.0", + "@prisma/studio-core": "0.21.1", + "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", @@ -5027,6 +6642,35 @@ "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/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5037,6 +6681,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", @@ -5058,6 +6719,41 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -5086,6 +6782,34 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5109,6 +6833,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -5130,6 +6861,16 @@ "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/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -5171,6 +6912,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", @@ -5226,6 +6977,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5261,6 +7032,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.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -5271,7 +7049,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5280,6 +7057,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", @@ -5335,7 +7118,6 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -5378,7 +7160,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" @@ -5391,7 +7173,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" @@ -5473,6 +7255,64 @@ "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5482,6 +7322,16 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5489,6 +7339,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", @@ -5503,6 +7360,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.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", @@ -5709,6 +7575,44 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5802,6 +7706,38 @@ "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5897,7 +7833,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", @@ -5926,6 +7862,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -5978,11 +7920,32 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/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" @@ -6093,6 +8056,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6105,6 +8074,17 @@ "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" + } } } } diff --git a/package.json b/package.json index 96e7d60..1aabe94 100644 --- a/package.json +++ b/package.json @@ -4,24 +4,43 @@ "private": true, "scripts": { "dev": "next dev --turbopack", + "prebuild": "prisma migrate deploy && prisma generate", "build": "next build --turbopack", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "db:seed": "tsx prisma/seed.ts", + "postinstall": "prisma generate" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" }, "dependencies": { + "@prisma/adapter-better-sqlite3": "^7.5.0", + "@prisma/client": "^7.5.0", + "@upstash/ratelimit": "^2.0.8", + "@upstash/redis": "^1.37.0", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.8.0", + "dotenv": "^17.3.1", + "jose": "^6.2.2", + "next": "15.5.14", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.5.14" + "sharp": "^0.34.5" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.14", - "@eslint/eslintrc": "^3" + "prisma": "^7.5.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..846b38d --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,15 @@ +// 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", + seed: "tsx prisma/seed.ts", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/prisma/migrations/20260324210509_init/migration.sql b/prisma/migrations/20260324210509_init/migration.sql new file mode 100644 index 0000000..d3c9cbf --- /dev/null +++ b/prisma/migrations/20260324210509_init/migration.sql @@ -0,0 +1,22 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "Waffle" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "ingredients" TEXT NOT NULL, + "price" REAL NOT NULL, + "isNew" BOOLEAN NOT NULL DEFAULT false, + "category" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20260324213633_waffle_image_url/migration.sql b/prisma/migrations/20260324213633_waffle_image_url/migration.sql new file mode 100644 index 0000000..06be147 --- /dev/null +++ b/prisma/migrations/20260324213633_waffle_image_url/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Waffle" ADD COLUMN "imageUrl" TEXT; diff --git a/prisma/migrations/20260324220033_gallery_images/migration.sql b/prisma/migrations/20260324220033_gallery_images/migration.sql new file mode 100644 index 0000000..bfe948b --- /dev/null +++ b/prisma/migrations/20260324220033_gallery_images/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "GalleryImage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "imageUrl" TEXT NOT NULL, + "alt" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); diff --git a/prisma/migrations/20260326120000_site_content/migration.sql b/prisma/migrations/20260326120000_site_content/migration.sql new file mode 100644 index 0000000..2c7e051 --- /dev/null +++ b/prisma/migrations/20260326120000_site_content/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "SiteContent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "heroEyebrow" TEXT NOT NULL, + "heroTitle" TEXT NOT NULL, + "heroSubtitle" TEXT NOT NULL, + "menuIntro" TEXT NOT NULL, + "galleryIntro" TEXT NOT NULL, + "hours" TEXT NOT NULL, + "updatedAt" DATETIME NOT NULL +); diff --git a/prisma/migrations/20260326150000_hero_background_image/migration.sql b/prisma/migrations/20260326150000_hero_background_image/migration.sql new file mode 100644 index 0000000..f19baf1 --- /dev/null +++ b/prisma/migrations/20260326150000_hero_background_image/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "SiteContent" ADD COLUMN "heroBackgroundImageUrl" TEXT; + diff --git a/prisma/migrations/20260326180000_upload_image_resolution/migration.sql b/prisma/migrations/20260326180000_upload_image_resolution/migration.sql new file mode 100644 index 0000000..b91c9ea --- /dev/null +++ b/prisma/migrations/20260326180000_upload_image_resolution/migration.sql @@ -0,0 +1,4 @@ +-- Add configurable default upload image resolution. +ALTER TABLE "SiteContent" ADD COLUMN "uploadImageWidth" INTEGER NOT NULL DEFAULT 1920; +ALTER TABLE "SiteContent" ADD COLUMN "uploadImageHeight" INTEGER NOT NULL DEFAULT 1080; + diff --git a/prisma/migrations/20260518130000_hero_text_over_image/migration.sql b/prisma/migrations/20260518130000_hero_text_over_image/migration.sql new file mode 100644 index 0000000..7bf41cd --- /dev/null +++ b/prisma/migrations/20260518130000_hero_text_over_image/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "SiteContent" ADD COLUMN "heroTextOverImage" BOOLEAN NOT NULL DEFAULT true; diff --git a/prisma/migrations/20260518140000_truck_schedule/migration.sql b/prisma/migrations/20260518140000_truck_schedule/migration.sql new file mode 100644 index 0000000..356cf7e --- /dev/null +++ b/prisma/migrations/20260518140000_truck_schedule/migration.sql @@ -0,0 +1,16 @@ +-- AlterTable +ALTER TABLE "SiteContent" ADD COLUMN "scheduleIntro" TEXT NOT NULL DEFAULT ''; + +-- CreateTable +CREATE TABLE "TruckEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "eventDate" TEXT NOT NULL, + "timeLabel" TEXT NOT NULL DEFAULT '', + "location" TEXT NOT NULL, + "note" TEXT NOT NULL DEFAULT '', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateIndex +CREATE INDEX "TruckEvent_eventDate_idx" ON "TruckEvent"("eventDate"); diff --git a/prisma/migrations/20260518150000_truck_event_recurrence/migration.sql b/prisma/migrations/20260518150000_truck_event_recurrence/migration.sql new file mode 100644 index 0000000..9a43f5a --- /dev/null +++ b/prisma/migrations/20260518150000_truck_event_recurrence/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "TruckEventRecurrence" ( + "id" TEXT NOT NULL PRIMARY KEY, + "dayOfWeek" INTEGER NOT NULL, + "timeLabel" TEXT NOT NULL DEFAULT '', + "location" TEXT NOT NULL, + "note" TEXT NOT NULL DEFAULT '', + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateIndex +CREATE INDEX "TruckEventRecurrence_dayOfWeek_idx" ON "TruckEventRecurrence"("dayOfWeek"); diff --git a/prisma/migrations/20260529120000_ingredients/migration.sql b/prisma/migrations/20260529120000_ingredients/migration.sql new file mode 100644 index 0000000..d6d5fca --- /dev/null +++ b/prisma/migrations/20260529120000_ingredients/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "Ingredient" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "price" REAL NOT NULL, + "imageUrl" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "_IngredientToWaffle" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_IngredientToWaffle_A_fkey" FOREIGN KEY ("A") REFERENCES "Ingredient" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_IngredientToWaffle_B_fkey" FOREIGN KEY ("B") REFERENCES "Waffle" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "_IngredientToWaffle_AB_unique" ON "_IngredientToWaffle"("A", "B"); + +-- CreateIndex +CREATE INDEX "_IngredientToWaffle_B_index" ON "_IngredientToWaffle"("B"); diff --git a/prisma/migrations/20260611121500_site_logo_url/migration.sql b/prisma/migrations/20260611121500_site_logo_url/migration.sql new file mode 100644 index 0000000..527f001 --- /dev/null +++ b/prisma/migrations/20260611121500_site_logo_url/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "SiteContent" ADD COLUMN "logoUrl" TEXT; diff --git a/prisma/migrations/20260611140000_custom_waffle_site_content/migration.sql b/prisma/migrations/20260611140000_custom_waffle_site_content/migration.sql new file mode 100644 index 0000000..b65e96a --- /dev/null +++ b/prisma/migrations/20260611140000_custom_waffle_site_content/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "SiteContent" ADD COLUMN "customWaffleTitle" TEXT NOT NULL DEFAULT 'La gaufre sur mesure'; +ALTER TABLE "SiteContent" ADD COLUMN "customWaffleDescription" TEXT NOT NULL DEFAULT 'Composez votre gaufre en choisissant parmi nos ingrédients : chacun avec sa photo et son prix.'; +ALTER TABLE "SiteContent" ADD COLUMN "customWaffleBasePrice" REAL NOT NULL DEFAULT 3; +ALTER TABLE "SiteContent" ADD COLUMN "customWaffleImageUrl" TEXT; +ALTER TABLE "SiteContent" ADD COLUMN "customWaffleVisible" BOOLEAN NOT NULL DEFAULT true; diff --git a/prisma/migrations/20260611150000_truck_event_image/migration.sql b/prisma/migrations/20260611150000_truck_event_image/migration.sql new file mode 100644 index 0000000..17b685a --- /dev/null +++ b/prisma/migrations/20260611150000_truck_event_image/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "TruckEvent" ADD COLUMN "imageUrl" TEXT; +ALTER TABLE "TruckEventRecurrence" ADD COLUMN "imageUrl" TEXT; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /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 = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..c56db88 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,105 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "sqlite" +} + +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String + createdAt DateTime @default(now()) +} + +model Ingredient { + id String @id @default(cuid()) + name String + price Float + imageUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + waffles Waffle[] +} + +model Waffle { + id String @id @default(cuid()) + name String + /** Description textuelle affichée sur la fiche menu. */ + ingredients String + price Float + isNew Boolean @default(false) + category String + imageUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ingredientItems Ingredient[] +} + +model GalleryImage { + id String @id @default(cuid()) + imageUrl String + alt String + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +/** Une seule ligne (id = default) : textes affichés sur la page d’accueil et horaires. */ +model SiteContent { + id String @id @default("default") + heroEyebrow String + heroTitle String + heroSubtitle String + heroBackgroundImageUrl String? + /** Logo affiché dans l’en-tête du site public */ + logoUrl String? + /** true : texte sur l’image (héros) ; false : image seule, texte au-dessus du menu */ + heroTextOverImage Boolean @default(true) + menuIntro String + galleryIntro String + scheduleIntro String @default("") + customWaffleTitle String @default("La gaufre sur mesure") + customWaffleDescription String @default("Composez votre gaufre en choisissant parmi nos ingrédients : chacun avec sa photo et son prix.") + customWaffleBasePrice Float @default(3) + customWaffleImageUrl String? + customWaffleVisible Boolean @default(true) + uploadImageWidth Int @default(1920) + uploadImageHeight Int @default(1080) + hours Json + updatedAt DateTime @updatedAt +} + +/** Présence du food truck (calendrier « Où me trouver »). */ +model TruckEvent { + id String @id @default(cuid()) + eventDate String + timeLabel String @default("") + location String + note String @default("") + imageUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([eventDate]) +} + +/** Règle hebdomadaire : répétée chaque semaine (0 = lundi … 6 = dimanche). */ +model TruckEventRecurrence { + id String @id @default(cuid()) + dayOfWeek Int + timeLabel String @default("") + location String + note String @default("") + imageUrl String? + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([dayOfWeek]) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..1717dd5 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,178 @@ +import "dotenv/config"; +import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"; +import { PrismaClient } from "../src/generated/prisma/client"; +import { hashPassword } from "../src/lib/password"; +import { DEFAULT_PUBLIC_SITE_COPY } from "../src/lib/site-content-defaults"; +import { WAFFLE_CATEGORIES } from "../src/lib/constants"; +import { formatDateIso } from "../src/lib/week-calendar"; + +const url = process.env.DATABASE_URL ?? "file:./dev.db"; +const adapter = new PrismaBetterSqlite3({ url }); +const prisma = new PrismaClient({ adapter }); + +async function main() { + const email = + process.env.ADMIN_EMAIL?.trim().toLowerCase() || "admin@lagaufredor.local"; + const password = process.env.ADMIN_PASSWORD || "changeme123"; + + const existing = await prisma.user.findUnique({ where: { email } }); + if (!existing) { + await prisma.user.create({ + data: { + email, + passwordHash: await hashPassword(password), + }, + }); + console.log( + `Compte admin créé : ${email} (mot de passe : variable ADMIN_PASSWORD ou défaut changeme123)` + ); + } + + const count = await prisma.waffle.count(); + if (count === 0) { + await prisma.waffle.createMany({ + data: [ + { + name: "Coco-Belge", + ingredients: + "Pâte Liège, perles de sucre, noix de coco râpée, touche de vanille Bourbon.", + price: 6.5, + isNew: true, + category: WAFFLE_CATEGORIES.DU_MOIS, + imageUrl: "/gallery/hero-coco.png", + }, + { + name: "Classique dorée", + ingredients: "Sucre perlé, beurre AOP, sirop d’érable ou chocolat.", + price: 5.9, + isNew: false, + category: WAFFLE_CATEGORIES.SUCREE, + imageUrl: "/gallery/gaufres-sucrees.png", + }, + { + name: "Bubble chantilly", + ingredients: "Bubble waffle, chantilly maison, copeaux de chocolat noir.", + price: 7.2, + isNew: false, + category: WAFFLE_CATEGORIES.SUCREE, + imageUrl: "/gallery/bubble-waffle.png", + }, + { + name: "Montagnarde", + ingredients: + "Pâte aux herbes, comté fondant, jambon cru, poivre noir concassé.", + price: 8.5, + isNew: false, + category: WAFFLE_CATEGORIES.SALEE, + imageUrl: "/gallery/gaufre-salee.png", + }, + { + name: "Bacon & ciboulette", + ingredients: "Crème légère, bacon croustillant, ciboulette fraîche.", + price: 7.9, + isNew: false, + category: WAFFLE_CATEGORIES.SALEE, + imageUrl: "/gallery/gaufre-salee.png", + }, + ], + }); + console.log("Gaufres d’exemple insérées."); + } + + const galleryCount = await prisma.galleryImage.count(); + if (galleryCount === 0) { + await prisma.galleryImage.createMany({ + data: [ + { + imageUrl: "/gallery/hero-coco.png", + alt: "Gaufres belges caramélisées sur ardoise, noix de coco et drapeau belge.", + sortOrder: 0, + }, + { + imageUrl: "/gallery/gaufres-sucrees.png", + alt: "Stack de gaufres dorées, sucre glace et sirop.", + sortOrder: 1, + }, + { + imageUrl: "/gallery/bubble-waffle.png", + alt: "Bubble waffles en cornet, chantilly et chocolat.", + sortOrder: 2, + }, + { + imageUrl: "/gallery/gaufre-salee.png", + alt: "Gaufre salée aux herbes, crème, bacon et ciboulette.", + sortOrder: 3, + }, + ], + }); + console.log("Galerie d’exemple insérée."); + } + + await prisma.siteContent.upsert({ + where: { id: "default" }, + create: { + id: "default", + ...DEFAULT_PUBLIC_SITE_COPY, + }, + update: {}, + }); + + const truckCount = await prisma.truckEvent.count(); + if (truckCount === 0) { + const start = new Date(); + const day = start.getDay(); + const diff = day === 0 ? -6 : 1 - day; + start.setDate(start.getDate() + diff); + + const dateAt = (offset: number) => { + const d = new Date(start); + d.setDate(start.getDate() + offset); + return formatDateIso(d); + }; + + await prisma.truckEvent.createMany({ + data: [ + { + eventDate: dateAt(1), + timeLabel: "9h — 13h", + location: "Marché de Bagnols-sur-Cèze", + note: "Place du marché", + }, + { + eventDate: dateAt(3), + timeLabel: "11h — 19h", + location: "Festival gourmand — Uzès", + note: "", + }, + { + eventDate: dateAt(5), + timeLabel: "10h — 18h", + location: "Place de la République, Alès", + note: "Food truck zone nord", + }, + ], + }); + console.log("Événements food truck d’exemple insérés."); + } + + const recurCount = await prisma.truckEventRecurrence.count(); + if (recurCount === 0) { + await prisma.truckEventRecurrence.create({ + data: { + dayOfWeek: 1, + timeLabel: "9h — 13h", + location: "Marché hebdomadaire — Bagnols-sur-Cèze", + note: "Place du marché", + }, + }); + console.log("Récurrence food truck d’exemple insérée."); + } +} + +main() + .then(() => prisma.$disconnect()) + .catch((e) => { + console.error(e); + prisma.$disconnect(); + process.exit(1); + }); diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..648aeb0 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/gallery/bubble-waffle.png b/public/gallery/bubble-waffle.png new file mode 100644 index 0000000..c82dcb4 Binary files /dev/null and b/public/gallery/bubble-waffle.png differ diff --git a/public/gallery/gaufre-salee.png b/public/gallery/gaufre-salee.png new file mode 100644 index 0000000..98135b3 Binary files /dev/null and b/public/gallery/gaufre-salee.png differ diff --git a/public/gallery/gaufres-sucrees.png b/public/gallery/gaufres-sucrees.png new file mode 100644 index 0000000..98be522 Binary files /dev/null and b/public/gallery/gaufres-sucrees.png differ diff --git a/public/gallery/hero-coco.png b/public/gallery/hero-coco.png new file mode 100644 index 0000000..0386698 Binary files /dev/null and b/public/gallery/hero-coco.png differ diff --git a/public/gallery/menu-button-texture.png b/public/gallery/menu-button-texture.png new file mode 100644 index 0000000..ca619bd Binary files /dev/null and b/public/gallery/menu-button-texture.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..9ccd026 Binary files /dev/null and b/public/logo.png differ diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/docker-hub-push.sh b/scripts/docker-hub-push.sh new file mode 100755 index 0000000..f27f304 --- /dev/null +++ b/scripts/docker-hub-push.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Publie l’image sur Docker Hub (compte par défaut : foufure/gauffre). +# Prérequis : docker login (une fois) +# Usage : +# ./scripts/docker-hub-push.sh # tag latest +# ./scripts/docker-hub-push.sh v1.0.0 # tag personnalisé +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +DOCKER_USER="${DOCKER_USER:-foufure}" +IMAGE_NAME="${IMAGE_NAME:-gauffre}" +TAG="${1:-latest}" +FULL_IMAGE="${DOCKER_USER}/${IMAGE_NAME}:${TAG}" + +echo "Build : ${FULL_IMAGE}" +docker build -t "${FULL_IMAGE}" . + +echo "Push : ${FULL_IMAGE}" +docker push "${FULL_IMAGE}" + +echo "Terminé : ${FULL_IMAGE}" diff --git a/src/app/admin/(dashboard)/gallery/page.tsx b/src/app/admin/(dashboard)/gallery/page.tsx new file mode 100644 index 0000000..d010d1d --- /dev/null +++ b/src/app/admin/(dashboard)/gallery/page.tsx @@ -0,0 +1,5 @@ +import { AdminGallery } from "@/components/admin/AdminGallery"; + +export default function AdminGalleryPage() { + return ; +} diff --git a/src/app/admin/(dashboard)/ingredients/page.tsx b/src/app/admin/(dashboard)/ingredients/page.tsx new file mode 100644 index 0000000..334d476 --- /dev/null +++ b/src/app/admin/(dashboard)/ingredients/page.tsx @@ -0,0 +1,5 @@ +import { AdminIngredients } from "@/components/admin/AdminIngredients"; + +export default function AdminIngredientsPage() { + return ; +} diff --git a/src/app/admin/(dashboard)/layout.tsx b/src/app/admin/(dashboard)/layout.tsx new file mode 100644 index 0000000..1acceef --- /dev/null +++ b/src/app/admin/(dashboard)/layout.tsx @@ -0,0 +1,14 @@ +import { AdminNav } from "@/components/admin/AdminNav"; + +export default function AdminDashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/app/admin/(dashboard)/page.tsx b/src/app/admin/(dashboard)/page.tsx new file mode 100644 index 0000000..f450d0e --- /dev/null +++ b/src/app/admin/(dashboard)/page.tsx @@ -0,0 +1,5 @@ +import { AdminDashboard } from "@/components/admin/AdminDashboard"; + +export default function AdminPage() { + return ; +} diff --git a/src/app/admin/(dashboard)/schedule/page.tsx b/src/app/admin/(dashboard)/schedule/page.tsx new file mode 100644 index 0000000..e58b9fb --- /dev/null +++ b/src/app/admin/(dashboard)/schedule/page.tsx @@ -0,0 +1,5 @@ +import { AdminTruckSchedule } from "@/components/admin/AdminTruckSchedule"; + +export default function AdminSchedulePage() { + return ; +} diff --git a/src/app/admin/(dashboard)/site/page.tsx b/src/app/admin/(dashboard)/site/page.tsx new file mode 100644 index 0000000..a332f97 --- /dev/null +++ b/src/app/admin/(dashboard)/site/page.tsx @@ -0,0 +1,5 @@ +import { AdminSiteContent } from "@/components/admin/AdminSiteContent"; + +export default function AdminSitePage() { + return ; +} diff --git a/src/app/admin/api/auth/login/route.ts b/src/app/admin/api/auth/login/route.ts new file mode 100644 index 0000000..c623b70 --- /dev/null +++ b/src/app/admin/api/auth/login/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from "next/server"; +import { createSessionToken, SESSION_COOKIE } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { verifyPassword } from "@/lib/password"; +import { getClientIp, rateLimitFailedLogin } from "@/lib/rate-limit"; +import { + clearLegacyRootSessionCookie, + sessionCookieBase, +} from "@/lib/session-cookie"; + +export async function POST(request: Request) { + const ip = getClientIp(request); + + let body: { email?: string; password?: string }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const email = String(body.email ?? "") + .trim() + .toLowerCase(); + const password = String(body.password ?? ""); + + if (!email || !password) { + return NextResponse.json( + { error: "Email et mot de passe requis" }, + { status: 400 } + ); + } + + const user = await prisma.user.findUnique({ where: { email } }); + const valid = + !!user && (await verifyPassword(password, user.passwordHash)); + if (!valid) { + const limited = await rateLimitFailedLogin(ip); + if (!limited.ok) { + return NextResponse.json( + { + error: "Trop de tentatives. Réessayez plus tard.", + retryAfterSec: limited.retryAfterSec, + }, + { + status: 429, + headers: { "Retry-After": String(limited.retryAfterSec) }, + } + ); + } + return NextResponse.json({ error: "Identifiants invalides" }, { status: 401 }); + } + + const token = await createSessionToken(user.id); + const res = NextResponse.json({ ok: true }); + res.cookies.set(SESSION_COOKIE, token, { + ...sessionCookieBase(), + maxAge: 60 * 60 * 24 * 7, + }); + clearLegacyRootSessionCookie(res); + return res; +} diff --git a/src/app/admin/api/auth/logout/route.ts b/src/app/admin/api/auth/logout/route.ts new file mode 100644 index 0000000..6518276 --- /dev/null +++ b/src/app/admin/api/auth/logout/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from "next/server"; +import { clearAllSessionCookieVariants } from "@/lib/session-cookie"; + +export async function POST() { + const res = NextResponse.json({ ok: true }); + clearAllSessionCookieVariants(res); + return res; +} diff --git a/src/app/admin/api/auth/me/route.ts b/src/app/admin/api/auth/me/route.ts new file mode 100644 index 0000000..fa05107 --- /dev/null +++ b/src/app/admin/api/auth/me/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ user: null }); + } + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { email: true }, + }); + if (!user) { + return NextResponse.json({ user: null }); + } + return NextResponse.json({ user: { email: user.email } }); +} diff --git a/src/app/admin/api/custom-waffle/route.ts b/src/app/admin/api/custom-waffle/route.ts new file mode 100644 index 0000000..986e07e --- /dev/null +++ b/src/app/admin/api/custom-waffle/route.ts @@ -0,0 +1,66 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { + getCustomWaffleConfig, + parseCustomWafflePayload, +} from "@/lib/custom-waffle-content"; +import { DEFAULT_PUBLIC_SITE_COPY, SITE_CONTENT_ID } from "@/lib/site-content-defaults"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + return NextResponse.json(await getCustomWaffleConfig()); +} + +export async function PUT(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + if (body === null || typeof body !== "object") { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const parsed = parseCustomWafflePayload(body as Record); + if (parsed.error || !parsed.data) { + return NextResponse.json({ error: parsed.error }, { status: 400 }); + } + + const { data } = parsed; + + await prisma.siteContent.upsert({ + where: { id: SITE_CONTENT_ID }, + create: { + id: SITE_CONTENT_ID, + ...DEFAULT_PUBLIC_SITE_COPY, + customWaffleTitle: data.title, + customWaffleDescription: data.description, + customWaffleBasePrice: data.basePrice, + customWaffleImageUrl: data.imageUrl, + customWaffleVisible: data.visible, + }, + update: { + customWaffleTitle: data.title, + customWaffleDescription: data.description, + customWaffleBasePrice: data.basePrice, + customWaffleImageUrl: data.imageUrl, + customWaffleVisible: data.visible, + }, + }); + + revalidatePath("/"); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/admin/api/gallery/[id]/route.ts b/src/app/admin/api/gallery/[id]/route.ts new file mode 100644 index 0000000..b5bc47b --- /dev/null +++ b/src/app/admin/api/gallery/[id]/route.ts @@ -0,0 +1,81 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { prisma } from "@/lib/prisma"; +import { prismaMutationErrorResponse } from "@/lib/prisma-route-error"; +import { getSessionUserId } from "@/lib/session"; +import { deleteUploadAssetIfAny } from "@/lib/upload-asset"; + +type Params = { params: Promise<{ id: string }> }; + +export async function PUT(request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + let body: { imageUrl?: string; alt?: string; sortOrder?: number }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const imageUrl = normalizeImageUrl(body.imageUrl); + const alt = String(body.alt ?? "").trim(); + const sortOrder = Number.isFinite(Number(body.sortOrder)) + ? Number(body.sortOrder) + : 0; + + if (!imageUrl || !alt) { + return NextResponse.json( + { error: "Image et texte alternatif requis" }, + { status: 400 } + ); + } + if ( + body.imageUrl != null && + String(body.imageUrl).trim() !== "" && + imageUrl === null + ) { + return NextResponse.json({ error: "URL d’image invalide" }, { status: 400 }); + } + + try { + const previous = await prisma.galleryImage.findUnique({ + where: { id }, + select: { imageUrl: true }, + }); + const row = await prisma.galleryImage.update({ + where: { id }, + data: { imageUrl, alt, sortOrder }, + }); + if (previous?.imageUrl && previous.imageUrl !== row.imageUrl) { + await deleteUploadAssetIfAny(previous.imageUrl); + } + revalidatePath("/"); + return NextResponse.json(row); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} + +export async function DELETE(_request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + try { + const row = await prisma.galleryImage.delete({ where: { id } }); + await deleteUploadAssetIfAny(row.imageUrl); + revalidatePath("/"); + return NextResponse.json({ ok: true }); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} diff --git a/src/app/admin/api/gallery/route.ts b/src/app/admin/api/gallery/route.ts new file mode 100644 index 0000000..89b3e0d --- /dev/null +++ b/src/app/admin/api/gallery/route.ts @@ -0,0 +1,57 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const images = await prisma.galleryImage.findMany({ + orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], + }); + return NextResponse.json(images); +} + +export async function POST(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: { imageUrl?: string; alt?: string; sortOrder?: number }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const imageUrl = normalizeImageUrl(body.imageUrl); + const alt = String(body.alt ?? "").trim(); + const sortOrder = Number.isFinite(Number(body.sortOrder)) + ? Number(body.sortOrder) + : 0; + + if (!imageUrl || !alt) { + return NextResponse.json( + { error: "Image et texte alternatif requis" }, + { status: 400 } + ); + } + if ( + body.imageUrl != null && + String(body.imageUrl).trim() !== "" && + imageUrl === null + ) { + return NextResponse.json({ error: "URL d’image invalide" }, { status: 400 }); + } + + const row = await prisma.galleryImage.create({ + data: { imageUrl, alt, sortOrder }, + }); + revalidatePath("/"); + return NextResponse.json(row); +} diff --git a/src/app/admin/api/ingredients/[id]/route.ts b/src/app/admin/api/ingredients/[id]/route.ts new file mode 100644 index 0000000..11d8725 --- /dev/null +++ b/src/app/admin/api/ingredients/[id]/route.ts @@ -0,0 +1,76 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { prisma } from "@/lib/prisma"; +import { prismaMutationErrorResponse } from "@/lib/prisma-route-error"; +import { getSessionUserId } from "@/lib/session"; +import { deleteUploadAssetIfAny } from "@/lib/upload-asset"; + +type Params = { params: Promise<{ id: string }> }; + +export async function PUT(request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + let body: { name?: string; price?: number; imageUrl?: string | null }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const name = String(body.name ?? "").trim(); + const price = Number(body.price); + const imageUrl = normalizeImageUrl(body.imageUrl); + + if (!name || Number.isNaN(price) || price < 0) { + return NextResponse.json({ error: "Données invalides" }, { status: 400 }); + } + if ( + body.imageUrl != null && + String(body.imageUrl).trim() !== "" && + imageUrl === null + ) { + return NextResponse.json({ error: "URL d’image invalide" }, { status: 400 }); + } + + try { + const previous = await prisma.ingredient.findUnique({ + where: { id }, + select: { imageUrl: true }, + }); + const ingredient = await prisma.ingredient.update({ + where: { id }, + data: { name, price, imageUrl }, + }); + if (previous?.imageUrl && previous.imageUrl !== ingredient.imageUrl) { + await deleteUploadAssetIfAny(previous.imageUrl); + } + revalidatePath("/"); + return NextResponse.json(ingredient); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} + +export async function DELETE(_request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + try { + const row = await prisma.ingredient.delete({ where: { id } }); + await deleteUploadAssetIfAny(row.imageUrl); + revalidatePath("/"); + return NextResponse.json({ ok: true }); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} diff --git a/src/app/admin/api/ingredients/route.ts b/src/app/admin/api/ingredients/route.ts new file mode 100644 index 0000000..e87abfe --- /dev/null +++ b/src/app/admin/api/ingredients/route.ts @@ -0,0 +1,52 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const ingredients = await prisma.ingredient.findMany({ + orderBy: { name: "asc" }, + }); + return NextResponse.json(ingredients); +} + +export async function POST(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: { name?: string; price?: number; imageUrl?: string | null }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const name = String(body.name ?? "").trim(); + const price = Number(body.price); + const imageUrl = normalizeImageUrl(body.imageUrl); + + if (!name || Number.isNaN(price) || price < 0) { + return NextResponse.json({ error: "Données invalides" }, { status: 400 }); + } + if ( + body.imageUrl != null && + String(body.imageUrl).trim() !== "" && + imageUrl === null + ) { + return NextResponse.json({ error: "URL d’image invalide" }, { status: 400 }); + } + + const ingredient = await prisma.ingredient.create({ + data: { name, price, imageUrl }, + }); + revalidatePath("/"); + return NextResponse.json(ingredient); +} diff --git a/src/app/admin/api/site-content/route.ts b/src/app/admin/api/site-content/route.ts new file mode 100644 index 0000000..cde18e3 --- /dev/null +++ b/src/app/admin/api/site-content/route.ts @@ -0,0 +1,123 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { + DEFAULT_PUBLIC_SITE_COPY, + SITE_CONTENT_ID, +} from "@/lib/site-content-defaults"; +import { parseHoursPayload } from "@/lib/site-content"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const row = await prisma.siteContent.findUnique({ + where: { id: SITE_CONTENT_ID }, + }); + if (!row) { + return NextResponse.json({ + ...DEFAULT_PUBLIC_SITE_COPY, + heroTextOverImage: DEFAULT_PUBLIC_SITE_COPY.heroTextOverImage, + uploadImageWidth: 1920, + uploadImageHeight: 1080, + }); + } + + return NextResponse.json({ + logoUrl: row.logoUrl ?? DEFAULT_PUBLIC_SITE_COPY.logoUrl, + heroEyebrow: row.heroEyebrow, + heroTitle: row.heroTitle, + heroSubtitle: row.heroSubtitle, + heroBackgroundImageUrl: + row.heroBackgroundImageUrl ?? DEFAULT_PUBLIC_SITE_COPY.heroBackgroundImageUrl, + heroTextOverImage: row.heroTextOverImage ?? true, + menuIntro: row.menuIntro, + galleryIntro: row.galleryIntro, + scheduleIntro: row.scheduleIntro ?? "", + uploadImageWidth: row.uploadImageWidth ?? 1920, + uploadImageHeight: row.uploadImageHeight ?? 1080, + hours: row.hours, + }); +} + +export async function PUT(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + if (body === null || typeof body !== "object") { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const b = body as Record; + const hours = parseHoursPayload(b.hours); + if (hours === null) { + return NextResponse.json( + { error: "Horaires : tableau de { days, hours } attendu" }, + { status: 400 } + ); + } + + const rawW = typeof b.uploadImageWidth === "number" + ? b.uploadImageWidth + : Number(String(b.uploadImageWidth ?? "")); + const rawH = typeof b.uploadImageHeight === "number" + ? b.uploadImageHeight + : Number(String(b.uploadImageHeight ?? "")); + const uploadImageWidth = Number.isFinite(rawW) ? Math.trunc(rawW) : 1920; + const uploadImageHeight = Number.isFinite(rawH) ? Math.trunc(rawH) : 1080; + if ( + uploadImageWidth < 320 || + uploadImageWidth > 8000 || + uploadImageHeight < 240 || + uploadImageHeight > 8000 + ) { + return NextResponse.json( + { error: "Résolution upload invalide (ex: 1920×1080)" }, + { status: 400 } + ); + } + + const data = { + logoUrl: + normalizeImageUrl(String(b.logoUrl ?? "")) ?? + DEFAULT_PUBLIC_SITE_COPY.logoUrl, + heroEyebrow: String(b.heroEyebrow ?? "").trim(), + heroTitle: String(b.heroTitle ?? "").trim(), + heroSubtitle: String(b.heroSubtitle ?? "").trim(), + heroBackgroundImageUrl: + normalizeImageUrl(String(b.heroBackgroundImageUrl ?? "")) ?? + DEFAULT_PUBLIC_SITE_COPY.heroBackgroundImageUrl, + heroTextOverImage: b.heroTextOverImage !== false, + menuIntro: String(b.menuIntro ?? "").trim(), + galleryIntro: String(b.galleryIntro ?? "").trim(), + scheduleIntro: String(b.scheduleIntro ?? "").trim(), + uploadImageWidth, + uploadImageHeight, + hours, + }; + + await prisma.siteContent.upsert({ + where: { id: SITE_CONTENT_ID }, + create: { + id: SITE_CONTENT_ID, + ...data, + }, + update: data, + }); + + revalidatePath("/"); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/admin/api/truck-events/[id]/route.ts b/src/app/admin/api/truck-events/[id]/route.ts new file mode 100644 index 0000000..0a2ec19 --- /dev/null +++ b/src/app/admin/api/truck-events/[id]/route.ts @@ -0,0 +1,64 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { parseTruckEventPayload } from "@/lib/truck-schedule-shared"; +import { prisma } from "@/lib/prisma"; +import { prismaMutationErrorResponse } from "@/lib/prisma-route-error"; +import { getSessionUserId } from "@/lib/session"; + +type Params = { params: Promise<{ id: string }> }; + +export async function PUT(request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + if (body === null || typeof body !== "object") { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const data = parseTruckEventPayload(body as Record); + if (!data) { + return NextResponse.json( + { error: "Date (AAAA-MM-JJ) et lieu requis" }, + { status: 400 } + ); + } + + try { + const row = await prisma.truckEvent.update({ + where: { id }, + data, + }); + revalidatePath("/"); + return NextResponse.json(row); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} + +export async function DELETE(_request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + try { + await prisma.truckEvent.delete({ where: { id } }); + revalidatePath("/"); + return NextResponse.json({ ok: true }); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} diff --git a/src/app/admin/api/truck-events/route.ts b/src/app/admin/api/truck-events/route.ts new file mode 100644 index 0000000..c70b613 --- /dev/null +++ b/src/app/admin/api/truck-events/route.ts @@ -0,0 +1,47 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { parseTruckEventPayload } from "@/lib/truck-schedule-shared"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const events = await prisma.truckEvent.findMany({ + orderBy: [{ eventDate: "asc" }, { createdAt: "asc" }], + }); + return NextResponse.json(events); +} + +export async function POST(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + if (body === null || typeof body !== "object") { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const data = parseTruckEventPayload(body as Record); + if (!data) { + return NextResponse.json( + { error: "Date (AAAA-MM-JJ) et lieu requis" }, + { status: 400 } + ); + } + + const row = await prisma.truckEvent.create({ data }); + revalidatePath("/"); + return NextResponse.json(row); +} diff --git a/src/app/admin/api/truck-recurrences/[id]/route.ts b/src/app/admin/api/truck-recurrences/[id]/route.ts new file mode 100644 index 0000000..8ee2380 --- /dev/null +++ b/src/app/admin/api/truck-recurrences/[id]/route.ts @@ -0,0 +1,64 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { parseTruckRecurrencePayload } from "@/lib/truck-schedule-shared"; +import { prisma } from "@/lib/prisma"; +import { prismaMutationErrorResponse } from "@/lib/prisma-route-error"; +import { getSessionUserId } from "@/lib/session"; + +type Params = { params: Promise<{ id: string }> }; + +export async function PUT(request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + if (body === null || typeof body !== "object") { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const data = parseTruckRecurrencePayload(body as Record); + if (!data) { + return NextResponse.json( + { error: "Jour de la semaine et lieu requis" }, + { status: 400 } + ); + } + + try { + const row = await prisma.truckEventRecurrence.update({ + where: { id }, + data, + }); + revalidatePath("/"); + return NextResponse.json(row); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} + +export async function DELETE(_request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + try { + await prisma.truckEventRecurrence.delete({ where: { id } }); + revalidatePath("/"); + return NextResponse.json({ ok: true }); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} diff --git a/src/app/admin/api/truck-recurrences/route.ts b/src/app/admin/api/truck-recurrences/route.ts new file mode 100644 index 0000000..01bf40d --- /dev/null +++ b/src/app/admin/api/truck-recurrences/route.ts @@ -0,0 +1,47 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { parseTruckRecurrencePayload } from "@/lib/truck-schedule-shared"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const rows = await prisma.truckEventRecurrence.findMany({ + orderBy: [{ dayOfWeek: "asc" }, { createdAt: "asc" }], + }); + return NextResponse.json(rows); +} + +export async function POST(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + if (body === null || typeof body !== "object") { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const data = parseTruckRecurrencePayload(body as Record); + if (!data) { + return NextResponse.json( + { error: "Jour de la semaine et lieu requis" }, + { status: 400 } + ); + } + + const row = await prisma.truckEventRecurrence.create({ data }); + revalidatePath("/"); + return NextResponse.json(row); +} diff --git a/src/app/admin/api/upload/route.ts b/src/app/admin/api/upload/route.ts new file mode 100644 index 0000000..00309e5 --- /dev/null +++ b/src/app/admin/api/upload/route.ts @@ -0,0 +1,106 @@ +import { randomBytes } from "crypto"; +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; +import { NextResponse } from "next/server"; +import sharp from "sharp"; +import { detectImageMime } from "@/lib/image-magic"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; +import { SITE_CONTENT_ID } from "@/lib/site-content-defaults"; + +const MAX_BYTES = 10 * 1024 * 1024; +const ALLOWED = new Map([ + ["image/jpeg", ".jpg"], + ["image/png", ".png"], + ["image/webp", ".webp"], + ["image/gif", ".gif"], +]); + +export async function POST(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let formData: FormData; + try { + formData = await request.formData(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const file = formData.get("file"); + if (!file || !(file instanceof File)) { + return NextResponse.json({ error: "Fichier manquant" }, { status: 400 }); + } + + if (file.size > MAX_BYTES) { + return NextResponse.json( + { error: "Fichier trop volumineux (max 10 Mo)" }, + { status: 400 } + ); + } + + const ab = await file.arrayBuffer(); + const originalBuf: Buffer = Buffer.from(new Uint8Array(ab)); + const detected = detectImageMime(originalBuf); + if (!detected || !ALLOWED.has(detected)) { + return NextResponse.json( + { + error: + "Fichier non reconnu comme image (JPEG, PNG, WebP ou GIF attendu)", + }, + { status: 400 } + ); + } + const ext = ALLOWED.get(detected)!; + + const settings = await prisma.siteContent.findUnique({ + where: { id: SITE_CONTENT_ID }, + select: { uploadImageWidth: true, uploadImageHeight: true }, + }); + const width = + typeof settings?.uploadImageWidth === "number" && settings.uploadImageWidth > 0 + ? settings.uploadImageWidth + : 1920; + const height = + typeof settings?.uploadImageHeight === "number" && settings.uploadImageHeight > 0 + ? settings.uploadImageHeight + : 1080; + + let buf: Buffer = originalBuf; + if (detected !== "image/gif") { + try { + let pipeline = sharp(originalBuf, { failOn: "none" }).rotate(); + pipeline = pipeline.resize({ + width, + height, + fit: "cover", + withoutEnlargement: true, + }); + + if (detected === "image/jpeg") { + buf = await pipeline.jpeg({ quality: 82 }).toBuffer(); + } else if (detected === "image/png") { + buf = await pipeline.png({ compressionLevel: 9 }).toBuffer(); + } else if (detected === "image/webp") { + buf = await pipeline.webp({ quality: 82 }).toBuffer(); + } else { + buf = await pipeline.toBuffer(); + } + } catch { + return NextResponse.json( + { error: "Impossible de traiter cette image" }, + { status: 400 } + ); + } + } + + const name = `${randomBytes(16).toString("hex")}${ext}`; + const dir = join(process.cwd(), "public", "uploads"); + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, name), buf); + + const url = `/uploads/${name}`; + return NextResponse.json({ url }); +} diff --git a/src/app/admin/api/waffles/[id]/route.ts b/src/app/admin/api/waffles/[id]/route.ts new file mode 100644 index 0000000..34e4925 --- /dev/null +++ b/src/app/admin/api/waffles/[id]/route.ts @@ -0,0 +1,123 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { WAFFLE_CATEGORIES } from "@/lib/constants"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { parseStrictBoolean } from "@/lib/json-fields"; +import { prisma } from "@/lib/prisma"; +import { prismaMutationErrorResponse } from "@/lib/prisma-route-error"; +import { getSessionUserId } from "@/lib/session"; +import { deleteUploadAssetIfAny } from "@/lib/upload-asset"; +import { + parseIngredientIds, + validateIngredientIds, + waffleIngredientInclude, +} from "@/lib/waffle-ingredients"; + +const CATEGORIES: Set = new Set(Object.values(WAFFLE_CATEGORIES)); + +type Params = { params: Promise<{ id: string }> }; + +export async function PUT(request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + let body: { + name?: string; + ingredients?: string; + price?: number; + isNew?: boolean; + category?: string; + imageUrl?: string | null; + ingredientIds?: unknown; + }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const name = String(body.name ?? "").trim(); + const ingredients = String(body.ingredients ?? "").trim(); + const price = Number(body.price); + const isNewParsed = parseStrictBoolean(body.isNew, "isNew", false); + if (!isNewParsed.ok) { + return NextResponse.json({ error: isNewParsed.error }, { status: 400 }); + } + const isNew = isNewParsed.value; + const category = String(body.category ?? "").trim(); + const imageUrl = normalizeImageUrl(body.imageUrl); + + if (!name || !ingredients || Number.isNaN(price) || price < 0) { + return NextResponse.json({ error: "Données invalides" }, { status: 400 }); + } + if (!CATEGORIES.has(category)) { + return NextResponse.json({ error: "Catégorie invalide" }, { status: 400 }); + } + if ( + body.imageUrl != null && + String(body.imageUrl).trim() !== "" && + imageUrl === null + ) { + return NextResponse.json({ error: "URL d’image invalide" }, { status: 400 }); + } + + const parsedIds = parseIngredientIds(body.ingredientIds); + if (parsedIds !== null && !(await validateIngredientIds(parsedIds))) { + return NextResponse.json({ error: "Ingrédient inconnu" }, { status: 400 }); + } + + try { + const previous = await prisma.waffle.findUnique({ + where: { id }, + select: { imageUrl: true }, + }); + const waffle = await prisma.waffle.update({ + where: { id }, + data: { + name, + ingredients, + price, + isNew, + category, + imageUrl, + ...(parsedIds !== null + ? { + ingredientItems: { + set: parsedIds.map((ingredientId) => ({ id: ingredientId })), + }, + } + : {}), + }, + include: waffleIngredientInclude, + }); + if (previous?.imageUrl && previous.imageUrl !== waffle.imageUrl) { + await deleteUploadAssetIfAny(previous.imageUrl); + } + revalidatePath("/"); + return NextResponse.json(waffle); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} + +export async function DELETE(_request: Request, { params }: Params) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { id } = await params; + + try { + const row = await prisma.waffle.delete({ where: { id } }); + await deleteUploadAssetIfAny(row.imageUrl); + revalidatePath("/"); + return NextResponse.json({ ok: true }); + } catch (e) { + return prismaMutationErrorResponse(e); + } +} diff --git a/src/app/admin/api/waffles/route.ts b/src/app/admin/api/waffles/route.ts new file mode 100644 index 0000000..583b3bf --- /dev/null +++ b/src/app/admin/api/waffles/route.ts @@ -0,0 +1,96 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { WAFFLE_CATEGORIES } from "@/lib/constants"; +import { normalizeImageUrl } from "@/lib/image-url"; +import { parseStrictBoolean } from "@/lib/json-fields"; +import { prisma } from "@/lib/prisma"; +import { getSessionUserId } from "@/lib/session"; +import { + parseIngredientIds, + validateIngredientIds, + waffleIngredientInclude, +} from "@/lib/waffle-ingredients"; + +const CATEGORIES: Set = new Set(Object.values(WAFFLE_CATEGORIES)); + +export async function GET() { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const waffles = await prisma.waffle.findMany({ + orderBy: [{ category: "asc" }, { name: "asc" }], + include: waffleIngredientInclude, + }); + return NextResponse.json(waffles); +} + +export async function POST(request: Request) { + const userId = await getSessionUserId(); + if (!userId) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + let body: { + name?: string; + ingredients?: string; + price?: number; + isNew?: boolean; + category?: string; + imageUrl?: string | null; + ingredientIds?: unknown; + }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Corps invalide" }, { status: 400 }); + } + + const name = String(body.name ?? "").trim(); + const ingredients = String(body.ingredients ?? "").trim(); + const price = Number(body.price); + const isNewParsed = parseStrictBoolean(body.isNew, "isNew", false); + if (!isNewParsed.ok) { + return NextResponse.json({ error: isNewParsed.error }, { status: 400 }); + } + const isNew = isNewParsed.value; + const category = String(body.category ?? "").trim(); + const imageUrl = normalizeImageUrl(body.imageUrl); + + if (!name || !ingredients || Number.isNaN(price) || price < 0) { + return NextResponse.json({ error: "Données invalides" }, { status: 400 }); + } + if (!CATEGORIES.has(category)) { + return NextResponse.json({ error: "Catégorie invalide" }, { status: 400 }); + } + if ( + body.imageUrl != null && + String(body.imageUrl).trim() !== "" && + imageUrl === null + ) { + return NextResponse.json({ error: "URL d’image invalide" }, { status: 400 }); + } + + const ingredientIds = parseIngredientIds(body.ingredientIds) ?? []; + if (!(await validateIngredientIds(ingredientIds))) { + return NextResponse.json({ error: "Ingrédient inconnu" }, { status: 400 }); + } + + const waffle = await prisma.waffle.create({ + data: { + name, + ingredients, + price, + isNew, + category, + imageUrl, + ...(ingredientIds.length > 0 + ? { ingredientItems: { connect: ingredientIds.map((id) => ({ id })) } } + : {}), + }, + include: waffleIngredientInclude, + }); + revalidatePath("/"); + return NextResponse.json(waffle); +} diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx new file mode 100644 index 0000000..d6f9ca2 --- /dev/null +++ b/src/app/admin/login/page.tsx @@ -0,0 +1,217 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { FormEvent, useEffect, useState } from "react"; +import { ADMIN_API } from "@/lib/constants"; + +function formatLockoutRemaining(totalSec: number): string { + if (totalSec < 60) return `${totalSec} s`; + const m = Math.floor(totalSec / 60); + const s = totalSec % 60; + if (s === 0) return `${m} min`; + return `${m} min ${s} s`; +} + +export default function AdminLoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + /** Compte à rebours anti force brute (secondes restantes), null si pas bloqué. */ + const [lockoutRemainingSec, setLockoutRemainingSec] = useState( + null + ); + + useEffect(() => { + fetch(`${ADMIN_API}/auth/me`, { credentials: "include" }) + .then((r) => r.json()) + .then((d) => { + if (d.user) router.replace("/admin"); + }) + .catch(() => {}); + }, [router]); + + useEffect(() => { + if (lockoutRemainingSec == null || lockoutRemainingSec <= 0) return; + const t = setTimeout(() => { + setLockoutRemainingSec((r) => + r != null && r > 1 ? r - 1 : null + ); + }, 1000); + return () => clearTimeout(t); + }, [lockoutRemainingSec]); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const res = await fetch(`${ADMIN_API}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ email, password }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + if (res.status === 429) { + const fromBody = + typeof data.retryAfterSec === "number" && + Number.isFinite(data.retryAfterSec) + ? Math.max(0, Math.ceil(data.retryAfterSec)) + : null; + const headerRaw = res.headers.get("Retry-After"); + const fromHeader = headerRaw + ? Math.max(0, parseInt(headerRaw, 10)) + : NaN; + const sec = + fromBody ?? + (Number.isFinite(fromHeader) ? fromHeader : null); + if (sec != null && sec > 0) { + setLockoutRemainingSec(sec); + setError(null); + } else { + setLockoutRemainingSec(null); + setError( + typeof data.error === "string" + ? data.error + : "Trop de tentatives. Réessayez plus tard." + ); + } + } else { + setLockoutRemainingSec(null); + setError( + typeof data.error === "string" + ? data.error + : "Connexion impossible" + ); + } + return; + } + setLockoutRemainingSec(null); + window.location.href = "/admin"; + } finally { + setLoading(false); + } + } + + return ( +
+
+

+ Administration +

+

+ Connexion réservée au gérant +

+
+ + + {lockoutRemainingSec != null && lockoutRemainingSec > 0 && ( +
+

Connexion temporairement bloquée

+

+ Trop de tentatives infructueuses. Temps restant avant un nouvel + essai :{" "} + + {formatLockoutRemaining(lockoutRemainingSec)} + +

+
+ )} + {error && ( +

+ {error} +

+ )} + +
+

+ + Retour au site + +

+
+
+ ); +} diff --git a/src/app/api/public/gallery/route.ts b/src/app/api/public/gallery/route.ts new file mode 100644 index 0000000..71e6593 --- /dev/null +++ b/src/app/api/public/gallery/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +/** Données vitrine uniquement (pas de métadonnées internes). */ +export async function GET() { + const rows = await prisma.galleryImage.findMany({ + orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], + select: { + id: true, + imageUrl: true, + alt: true, + sortOrder: true, + }, + }); + return NextResponse.json(rows); +} diff --git a/src/app/api/public/waffles/route.ts b/src/app/api/public/waffles/route.ts new file mode 100644 index 0000000..7906292 --- /dev/null +++ b/src/app/api/public/waffles/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { waffleIngredientInclude } from "@/lib/waffle-ingredients"; + +/** Données vitrine uniquement (pas de métadonnées internes). */ +export async function GET() { + const rows = await prisma.waffle.findMany({ + orderBy: [{ category: "asc" }, { name: "asc" }], + select: { + id: true, + name: true, + ingredients: true, + price: true, + isNew: true, + category: true, + imageUrl: true, + ingredientItems: waffleIngredientInclude.ingredientItems, + }, + }); + return NextResponse.json(rows); +} diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png new file mode 100644 index 0000000..277e838 Binary files /dev/null and b/src/app/apple-icon.png differ diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..1bd0429 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..df06c26 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,27 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #fff8f0; + --foreground: #3d2b1f; + --muted: #6b5344; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + --color-muted: var(--muted); + --color-gaufre: #f39c12; + --color-chocolat: #3d2b1f; + --color-cream: #fff8f0; + --font-sans: var(--font-outfit); } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), system-ui, sans-serif; +} + +html { + scroll-behavior: smooth; } diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000..e6fea7f Binary files /dev/null and b/src/app/icon.png differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..94bada5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,17 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Outfit } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const outfit = Outfit({ + variable: "--font-outfit", subsets: ["latin"], + display: "swap", }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Gaufrement Bon", + description: + "Gaufres sucrées et salées artisanales. Horaires, adresse et menu à Lille.", }; export default function RootLayout({ @@ -23,10 +20,8 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index a932894..3f270df 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,76 @@ -import Image from "next/image"; +import { Footer } from "@/components/site/Footer"; +import { GallerySection } from "@/components/site/GallerySection"; +import { ScheduleSection } from "@/components/site/ScheduleSection"; +import { Hero } from "@/components/site/Hero"; +import { HeroIntro } from "@/components/site/HeroIntro"; +import { MenuSection } from "@/components/site/MenuSection"; +import { SiteHeader } from "@/components/site/SiteHeader"; +import { prisma } from "@/lib/prisma"; +import { getCustomWaffleConfig } from "@/lib/custom-waffle-content"; +import { getPublicSiteContent } from "@/lib/site-content"; +import { getPublicTruckEvents } from "@/lib/truck-event"; +import { getPublicTruckRecurrences } from "@/lib/truck-recurrence"; +import { waffleIngredientInclude } from "@/lib/waffle-ingredients"; + +/** Page mise en cache (ISR) ; invalidation via `revalidatePath("/")` après modifs admin. */ +export const revalidate = 60; + +export default async function Home() { + const [ + waffles, + ingredientCatalog, + galleryImages, + site, + customWaffle, + truckEvents, + truckRecurrences, + ] = await Promise.all([ + prisma.waffle.findMany({ include: waffleIngredientInclude }), + prisma.ingredient.findMany({ + orderBy: { name: "asc" }, + select: { id: true, name: true, price: true, imageUrl: true }, + }), + prisma.galleryImage.findMany({ + orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], + }), + getPublicSiteContent(), + getCustomWaffleConfig(), + getPublicTruckEvents(), + getPublicTruckRecurrences(), + ]); -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. -
- - + {!site.heroTextOverImage ? ( + + ) : null} + + +
- -
+