diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e0ef784 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +.next +.git +.gitignore +.env +.env.* +!.env.example +npm-debug.log* +README.md +AGENTS.md +CLAUDE.md +Dockerfile +docker-compose.yml +.dockerignore +src/generated +*.tsbuildinfo diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8164e42 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1 + +# ============================================================================= +# QR-Code-Generator — Mehrstufiger Build +# 1. deps — Abhängigkeiten installieren +# 2. builder — Prisma Client erzeugen und Next.js standalone bauen +# 3. prismacli — Prisma-CLI samt Abhängigkeiten für die Start-Migration +# 4. runner — schlankes Laufzeit-Image +# ============================================================================= + +FROM node:24-alpine AS base +# openssl wird von den Prisma-Engines benötigt, libc6-compat von einigen +# vorkompilierten Node-Modulen. +RUN apk add --no-cache libc6-compat openssl +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 + + +# --- 1. Abhängigkeiten ------------------------------------------------------- +FROM base AS deps +COPY package.json package-lock.json ./ +# npm 11 blockiert Installationsskripte standardmäßig. Die Pakete, die welche +# ausführen dürfen, stehen im Feld "allowScripts" der package.json — Prisma +# lädt darüber seine Engine-Binaries. +RUN npm ci + + +# --- 2. Build ---------------------------------------------------------------- +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN node ./node_modules/prisma/build/index.js generate +RUN npm run build + + +# --- 3. Prisma-CLI ----------------------------------------------------------- +# Der Container wendet beim Start selbst die Migrationen an und braucht dafür +# die CLI. Sie hat mehr Abhängigkeiten als nur @prisma/* — deshalb wird ihre +# vollständige Abhängigkeitshülle hier separat und ohne den Rest des Projekts +# installiert. +FROM base AS prismacli +WORKDIR /prisma-cli +COPY package.json ./ +RUN PRISMA_VERSION="$(node -p "require('./package.json').devDependencies.prisma")" \ + && printf '%s' '{"name":"prisma-cli","private":true,"allowScripts":{"prisma":true,"@prisma/engines":true}}' > package.json \ + && npm install --omit=dev --no-package-lock "prisma@${PRISMA_VERSION}" + + +# --- 4. Laufzeit ------------------------------------------------------------- +FROM base AS runner +ENV NODE_ENV=production \ + PORT=3000 \ + HOSTNAME=0.0.0.0 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Migrationen, Prisma-Konfiguration und die CLI. Letztere ergänzt die +# node_modules des standalone-Builds, damit prisma.config.js ihre Module +# auflösen kann. +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/prisma.config.js ./prisma.config.js +COPY --from=prismacli /prisma-cli/node_modules ./node_modules + +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER nextjs +EXPOSE 3000 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/README.md b/README.md index e215bc4..03f74f5 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,332 @@ -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). +# QR-Code-Generator -## Getting Started +Eine Webanwendung zum Erzeugen von QR-Codes — als Docker-Compose-Stack aus +zwei Containern: der Next.js-Anwendung und einer PostgreSQL-Datenbank. -First, run the development server: +**Version 1** unterstützt den QR-Typ **Website/URL**, statisch wie dynamisch. +Die übrigen 14 Typen sind bereits in der Oberfläche sichtbar und für Version 2 +vorbereitet. + +--- + +## Inhalt + +- [Schnellstart](#schnellstart) +- [Konfiguration](#konfiguration) +- [Bedienung](#bedienung) +- [Technischer Aufbau](#technischer-aufbau) +- [Entwicklung ohne Docker](#entwicklung-ohne-docker) +- [API](#api) +- [Ausblick Version 2](#ausblick-version-2) + +--- + +## Schnellstart + +Voraussetzung: Docker mit Docker Compose. ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +cp .env.example .env # anschließend POSTGRES_PASSWORD und BASE_URL anpassen +docker compose up -d --build ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Danach ist die Anwendung unter erreichbar. Die +Datenbankmigrationen laufen beim Start des App-Containers automatisch — es +sind keine weiteren Schritte nötig. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Nützliche Befehle: -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. +```bash +docker compose logs -f app # Protokoll der Anwendung +docker compose ps # Status beider Container +docker compose down # Stack stoppen (Daten bleiben erhalten) +docker compose down -v # Stack stoppen und Datenbank löschen +``` -## Learn More +--- -To learn more about Next.js, take a look at the following resources: +## Konfiguration -- [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. +Alle Einstellungen stehen in der `.env` im Projektverzeichnis (Vorlage: +`.env.example`). -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +| Variable | Bedeutung | Standard | +| ------------------- | ------------------------------------------------------------- | ------------------------ | +| `BASE_URL` | Öffentliche Adresse der App — landet in dynamischen QR-Codes | `http://localhost:3000` | +| `APP_PORT` | Host-Port, auf dem die App veröffentlicht wird | `3000` | +| `DATABASE_URL` | Verbindungsstring der App zur Datenbank | — | +| `POSTGRES_USER` | Datenbankbenutzer | `qrcode` | +| `POSTGRES_PASSWORD` | Datenbankpasswort (**muss gesetzt sein**) | — | +| `POSTGRES_DB` | Name der Datenbank | `qrcode` | -## Deploy on Vercel +### Wichtig: `BASE_URL` richtig setzen -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. +Ein dynamischer QR-Code enthält nicht das eigentliche Ziel, sondern die +Kurz-URL `{BASE_URL}/r/{slug}`. Diese Adresse muss von den Geräten erreichbar +sein, die den Code später scannen. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- Nur am selben Rechner testen → `http://localhost:3000` genügt. +- Im LAN mit dem Handy scannen → die LAN-Adresse des Servers eintragen, z. B. + `http://192.168.1.50:3000`. + +Wird `BASE_URL` nachträglich geändert, zeigen bereits **gedruckte** dynamische +Codes weiterhin auf die alte Adresse. Am besten also vor dem ersten +produktiven Code festlegen. + +Nach einer Änderung an der `.env`: + +```bash +docker compose up -d +``` + +Ändern sich `POSTGRES_*`-Werte, muss `DATABASE_URL` entsprechend mitgezogen +werden — der Hostname darin bleibt `db` (der Servicename aus +`docker-compose.yml`). + +--- + +## Bedienung + +### 1. Typ wählen + +Die Startseite zeigt unter „Was möchtest du erstellen?“ ein Kachel-Grid aller +15 QR-Typen. In Version 1 ist **Website** aktiv (Badge „Beliebt“); die übrigen +Kacheln sind sichtbar, aber deaktiviert (Badge „Bald verfügbar“). + +### 2. Statisch oder dynamisch + +| | Statisch | Dynamisch | +| ---------------------- | -------------------------------------------- | ---------------------------------------------------- | +| Im Code kodiert | die Ziel-URL selbst | die Kurz-URL `{BASE_URL}/r/{slug}` | +| Ziel später änderbar | nein | ja, jederzeit | +| Braucht diesen Server | nein | ja — er leitet weiter | +| Scans werden gezählt | nein | ja | +| Wird gespeichert | nein | ja, in PostgreSQL | + +Ein dynamischer Code muss einmal **gespeichert** werden, denn erst dabei +entsteht seine Kurz-URL. Bis dahin zeigt die Vorschau einen Platzhalter und +der Download ist gesperrt — so kann kein Code entstehen, der ins Leere führt. + +### 3. Gestalten + +- **Farben**: Vorder- und Hintergrund per Farbwähler oder als Hex-Wert. + Fünf Voreinstellungen und ein Knopf zum Tauschen der Farben stehen bereit. +- **Kontrastwarnung**: Die Anwendung berechnet das Kontrastverhältnis und + warnt bei schlecht scanbaren Kombinationen — ebenso bei hellem Vordergrund + auf dunklem Grund, den viele Scanner-Apps nicht erkennen. +- **Größe**: 256 bis 2048 px (gilt für das PNG; das SVG skaliert verlustfrei). +- **Fehlerkorrektur**: L, M (Standard), Q oder H. Höhere Level machen den Code + robuster gegen Beschädigung, aber auch dichter. +- **Rand**: Breite der Ruhezone in Modulen. Weniger als 4 kann das Scannen + erschweren. + +Die Vorschau wird bei jeder Änderung serverseitig neu erzeugt — sie zeigt also +exakt das, was auch heruntergeladen wird. + +### 4. Herunterladen + +**PNG** für Druck und Office-Dokumente, **SVG** für verlustfreie Skalierung +(Plakate, Schilder, Druckvorstufe). + +### 5. Meine Codes + +Unter „Meine Codes“ stehen alle dynamischen Codes mit Vorschaubild, +Kurz-URL, aktuellem Ziel, Scan-Anzahl, Erstellungsdatum und letztem Scan. +Dort lässt sich + +- das **Ziel ändern** — die Kurz-URL und damit der gedruckte Code bleiben + gültig, +- der Code **löschen** (mit Bestätigungsdialog), +- das Bild erneut als PNG oder SVG **herunterladen**. + +Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden. + +> Version 1 ist für den Einzelplatz-/LAN-Betrieb gedacht und hat **keine +> Benutzerverwaltung**. Wer die Anwendung erreicht, sieht und ändert alle +> Codes. Die App gehört daher nicht ungeschützt ins offene Internet. + +--- + +## Technischer Aufbau + +### Stack + +| Baustein | Verwendet | +| ------------ | ------------------------------------------------------- | +| Framework | Next.js 16 (App Router, TypeScript), Frontend + API | +| Oberfläche | React 19, Tailwind CSS 4 | +| Datenbank | PostgreSQL 18 | +| ORM | Prisma 7 mit `@prisma/adapter-pg` | +| QR-Erzeugung | `qrcode`, serverseitig, Ausgabe als PNG und SVG | +| Validierung | Zod 4, im Browser und noch einmal auf dem Server | + +Der Stack läuft ohne Reverse Proxy: Der App-Container wird direkt per +Port-Mapping veröffentlicht. + +### Verzeichnisse + +``` +prisma/ + schema.prisma Datenmodell (Modell QrCode) + migrations/ Versionierte Migrationen +docker/ + entrypoint.sh Migration + Serverstart im Container +src/ + app/ + page.tsx Startseite mit dem Kachel-Grid + erstellen/[type]/ Generator je QR-Typ + meine-codes/ Übersicht der dynamischen Codes + r/[slug]/route.ts Weiterleitung dynamischer Codes (302) + api/ + qr/preview/ Live-Vorschau (SVG als Data-URL) + qr/download/ Download als PNG oder SVG + codes/ Anlegen und Auflisten dynamischer Codes + codes/[id]/ Ändern und Löschen + codes/[id]/qr/ Bild eines gespeicherten Codes + components/ + qr-forms/ Formulare je QR-Typ + deren Registry + qr-generator.tsx Generator-Oberfläche + codes-list.tsx „Meine Codes“ + design-panel.tsx Farben, Größe, Fehlerkorrektur, Rand + lib/ + qr/ + type-module.ts Vertrag eines QR-Typs (defineQrType) + registry.ts Registry aller 15 Typen + types/website.ts Der einzige aktive Typ in V1 + design.ts Gestaltungsoptionen + Zod-Schema + contrast.ts Kontrastbewertung + render.ts PNG-/SVG-Erzeugung + request.ts Anfrage-Schema und Auflösung des Code-Inhalts + db.ts Prisma-Client + env.ts Laufzeit-Konfiguration +``` + +### Datenmodell + +Nur dynamische Codes werden gespeichert. Das Modell `QrCode` hält neben Slug, +Zähler und Gestaltung ein generisches `data`-Feld (JSONB) sowie den Typ — so +brauchen die Typen aus Version 2 **keine** Schemaänderung. + +### Wie ein QR-Typ aufgebaut ist + +Jeder Typ ist ein Modul, das über `defineQrType` erzeugt wird und vier Dinge +beisteuert: Metadaten (Titel, Beschreibung, Icon), ein Zod-Schema, die +Kodierung des Inhalts und eine Kurzfassung für Listen. + +```ts +export const websiteType = defineQrType({ + id: "website", + title: "Website", + description: "Verlinke eine beliebige Webadresse.", + icon: "globe", + active: true, + supportsDynamic: true, + schema: websiteDataSchema, + defaultData: { url: "" }, + encode: (data) => data.url, + summary: (data) => describe(data.url), +}); +``` + +Innerhalb des Moduls arbeitet man mit dem echten, aus dem Zod-Schema +abgeleiteten Datentyp; nach außen zeigt die Registry eine einheitliche +Schnittstelle. Deshalb kennen Vorschau, Download, Speicherung und +Weiterleitung keinen einzigen typspezifischen Sonderfall. + +### Warum 302 und kein 301? + +Die Weiterleitung unter `/r/{slug}` antwortet mit **302 (temporär)** und +`Cache-Control: no-store`. Ein permanenter Redirect würde in Browsern hängen +bleiben: Zieländerungen kämen nicht an und der Scan-Zähler bliebe stehen. + +--- + +## Entwicklung ohne Docker + +Voraussetzung: Node.js 20.9+ und eine erreichbare PostgreSQL-Instanz. + +```bash +npm install + +# Datenbank z. B. als Einzelcontainer starten +docker run -d --name qr-db -p 5432:5432 \ + -e POSTGRES_USER=qrcode -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=qrcode \ + postgres:18-alpine + +# .env anlegen und DATABASE_URL auf localhost zeigen lassen: +# DATABASE_URL=postgresql://qrcode:devpass@localhost:5432/qrcode?schema=public + +npm run db:migrate # Migrationen anwenden und Prisma Client erzeugen +npm run dev # http://localhost:3000 +``` + +Weitere Skripte: + +```bash +npm run build # Produktionsbuild +npm run typecheck # TypeScript prüfen +npm run lint # ESLint +npm run db:studio # Prisma Studio (Datenbank ansehen) +``` + +> **Hinweis zu npm 11:** Installationsskripte sind standardmäßig blockiert. +> Die Pakete, die welche ausführen dürfen (Prisma lädt darüber seine +> Engine-Binaries), stehen im Feld `allowScripts` der `package.json`. + +--- + +## API + +Alle Endpunkte antworten im Fehlerfall mit `{ "error": "…" }` und einer +deutschen Meldung. + +| Methode | Pfad | Zweck | +| -------- | ----------------------- | ---------------------------------------------- | +| `POST` | `/api/qr/preview` | Vorschau als SVG-Data-URL | +| `POST` | `/api/qr/download` | PNG- oder SVG-Datei | +| `GET` | `/api/codes` | Alle dynamischen Codes | +| `POST` | `/api/codes` | Dynamischen Code anlegen | +| `GET` | `/api/codes/{id}` | Einzelnen Code lesen | +| `PATCH` | `/api/codes/{id}` | Ziel, Name oder Gestaltung ändern | +| `DELETE` | `/api/codes/{id}` | Code löschen | +| `GET` | `/api/codes/{id}/qr` | Bild des Codes (`?format=png\|svg&download=1`) | +| `GET` | `/r/{slug}` | Weiterleitung, erhöht den Scan-Zähler | + +Beispiel: + +```bash +curl -X POST http://localhost:3000/api/codes \ + -H 'Content-Type: application/json' \ + -d '{ + "type": "website", + "data": { "url": "https://example.com" }, + "design": { "fgColor": "#0F172A", "bgColor": "#FFFFFF", + "size": 512, "ecLevel": "M", "margin": 4 }, + "label": "Plakat Eingang" + }' +``` + +--- + +## Ausblick Version 2 + +Geplant sind die übrigen 14 Typen: vCard, App-Link, Mehrere Links, Social +Media, WhatsApp, PDF, Bild, Video, E-Mail, SMS, Standort, Zahlung, WLAN und +Feedback. + +Die Architektur ist darauf ausgelegt — ein neuer Typ besteht aus: + +1. **einem Modul** in `src/lib/qr/types/` (Zod-Schema, Kodierung, Kurzfassung), +2. **einer Formular-Komponente** in `src/components/qr-forms/`, +3. **je einem Eintrag** in `src/lib/qr/registry.ts` (ersetzt dort den + `defineComingSoonType`-Platzhalter) und in + `src/components/qr-forms/index.ts`. + +Am bestehenden Code aus Version 1 ist dafür nichts zu ändern: Vorschau, +Download, Gestaltung, Speicherung dynamischer Codes und die Weiterleitung +arbeiten für alle Typen gleich. + +Typen, die statt einer Weiterleitung eine eigene Landingpage brauchen (etwa +„Mehrere Links“ oder „Feedback“), setzen dafür `redirectTarget` auf eine +interne Adresse — auch dafür ist keine Änderung am Datenmodell nötig, weil +`data` als JSONB gespeichert wird. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f9383ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + db: + image: postgres:18-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-qrcode} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt — bitte .env aus .env.example anlegen} + POSTGRES_DB: ${POSTGRES_DB:-qrcode} + volumes: + # Ab PostgreSQL 18 liegt das Datenverzeichnis in einem Unterordner mit + # Versionsnummer — der Mountpunkt ist deshalb /var/lib/postgresql und + # nicht mehr /var/lib/postgresql/data. + - db-data:/var/lib/postgresql + healthcheck: + # Die App startet erst, wenn die Datenbank wirklich Anfragen annimmt. + test: + ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-qrcode} -d ${POSTGRES_DB:-qrcode}"] + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + + app: + build: + context: . + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + # Öffentliche Adresse der App — landet in jedem dynamischen QR-Code. + BASE_URL: ${BASE_URL:-http://localhost:3000} + DATABASE_URL: ${DATABASE_URL} + ports: + # Direktes Port-Mapping, bewusst ohne Reverse Proxy. + - "${APP_PORT:-3000}:3000" + +volumes: + db-data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..6452bf1 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Startskript des App-Containers: erst Migrationen, dann der Server. +set -e + +echo "→ Datenbankmigrationen werden angewendet …" +node /app/node_modules/prisma/build/index.js migrate deploy + +echo "→ Anwendung wird gestartet …" +exec "$@" diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..d7506a0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,7 +12,15 @@ const eslintConfig = defineConfig([ "out/**", "build/**", "next-env.d.ts", + // Von Prisma erzeugter Client. + "src/generated/**", ]), + { + // Die Prisma-Konfiguration ist bewusst CommonJS: so lässt sie sich im + // Laufzeit-Image ohne TypeScript-Loader einlesen. + files: ["prisma.config.js"], + rules: { "@typescript-eslint/no-require-imports": "off" }, + }, ]); export default eslintConfig; diff --git a/package-lock.json b/package-lock.json index e314074..4ad8623 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,17 @@ { "name": "qrcode-generator", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qrcode-generator", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@prisma/adapter-pg": "^7.9.1", "@prisma/client": "^7.9.1", "next": "16.3.0", - "pg": "^8.23.0", - "prisma": "^7.9.1", + "pg": "^8.16.3", "qrcode": "^1.5.4", "react": "19.2.8", "react-dom": "19.2.8", @@ -22,13 +21,14 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/pg": "^8.21.0", + "@types/pg": "^8.15.6", "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", - "dotenv": "^17.4.2", + "dotenv": "^17.2.4", "eslint": "^9", "eslint-config-next": "16.3.0", + "prisma": "^7.9.1", "tailwindcss": "^4", "typescript": "^5" } @@ -290,12 +290,14 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", + "devOptional": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" @@ -308,6 +310,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", + "devOptional": true, "license": "Apache-2.0", "peerDependencies": { "@electric-sql/pglite": "0.4.3" @@ -1460,6 +1463,7 @@ "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.9.1.tgz", "integrity": "sha512-4znKhxTmXmuPye9Z6pbIyYb5VZlkZ05qG1L6Dr4g+7oTwc6V50Bs9XirFBDdjWt+H/AabMn9aUnxBcvj8z05aA==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.3.4", @@ -1478,6 +1482,7 @@ "version": "0.24.17", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==", + "devOptional": true, "license": "ISC", "dependencies": { "@electric-sql/pglite": "0.4.3", @@ -1510,6 +1515,7 @@ "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz", "integrity": "sha512-UprXSMNXx2NF5ow4pqaQtE8OuBz6K78B0wc0tn2L28G5r933iWp1DR9Do2qWrsNvvFIP3x6mpEWnQtckMO0Uhg==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1523,12 +1529,14 @@ "version": "7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad.tgz", "integrity": "sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", "integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.9.1" @@ -1538,6 +1546,7 @@ "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.9.1.tgz", "integrity": "sha512-9DwxrNTeT25Orbu9CWh0CZvVlyY1lmscpbaeLZcOnuR7zcuFrt91YSmmOfIm7zJ08YOZ6mVzURKwLoMwEBcK8w==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.9.1", @@ -1549,6 +1558,7 @@ "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.9.1.tgz", "integrity": "sha512-PK8R60YZRQvYxBrGG9i7l2/rFyzy+2MuI1dKtmtrCqPH8YpiJx/MfiC7LRzX5786rZDEv7BngcjfIJW4/9ADuw==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.9.1" @@ -1558,6 +1568,7 @@ "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" @@ -1567,18 +1578,21 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/streams-local": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "ajv": "^8.12.0", @@ -1595,6 +1609,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1611,12 +1626,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, "license": "MIT" }, "node_modules/@prisma/studio-core": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@radix-ui/react-toggle": "1.1.10", @@ -1645,12 +1662,14 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, "license": "MIT" }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1666,6 +1685,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -1689,6 +1709,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -1707,6 +1728,7 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -1732,6 +1754,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -1751,6 +1774,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -1769,6 +1793,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1791,6 +1816,7 @@ "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": { @@ -2100,30 +2126,35 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-delaunay": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -2133,6 +2164,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -2142,12 +2174,14 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -2157,6 +2191,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -2166,12 +2201,14 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -2185,6 +2222,7 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -2205,6 +2243,7 @@ "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/node": { @@ -2241,6 +2280,7 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2909,6 +2949,7 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "devOptional": true, "license": "MIT", "dependencies": { "@visx/vendor": "4.0.0-alpha.0" @@ -2918,6 +2959,7 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -2928,6 +2970,7 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -2946,6 +2989,7 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -2959,12 +3003,14 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "devOptional": true, "license": "MIT" }, "node_modules/@visx/responsive": { "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.13", @@ -2979,6 +3025,7 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "devOptional": true, "license": "MIT", "dependencies": { "@visx/vendor": "4.0.0-alpha.0" @@ -2988,6 +3035,7 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "devOptional": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.13", @@ -3007,6 +3055,7 @@ "version": "4.0.0-alpha.0", "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "devOptional": true, "license": "MIT and ISC", "dependencies": { "@types/d3-array": "3.0.3", @@ -3038,6 +3087,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "devOptional": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -3324,6 +3374,7 @@ "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" @@ -3372,6 +3423,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", + "devOptional": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -3436,6 +3488,7 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -3570,6 +3623,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -3585,6 +3639,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "devOptional": true, "license": "MIT" }, "node_modules/client-only": { @@ -3633,6 +3688,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -3646,6 +3702,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3660,12 +3717,14 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "devOptional": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -3678,6 +3737,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -3687,6 +3747,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "devOptional": true, "license": "ISC", "dependencies": { "delaunator": "5" @@ -3699,6 +3760,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -3708,6 +3770,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" @@ -3720,6 +3783,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "devOptional": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -3732,6 +3796,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -3741,6 +3806,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -3757,6 +3823,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "devOptional": true, "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -3769,6 +3836,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -3781,6 +3849,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "devOptional": true, "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -3888,6 +3957,7 @@ "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" @@ -3933,12 +4003,14 @@ "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, "license": "MIT" }, "node_modules/delaunator": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "devOptional": true, "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -3948,6 +4020,7 @@ "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" @@ -3957,6 +4030,7 @@ "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": { @@ -3992,6 +4066,7 @@ "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -4019,6 +4094,7 @@ "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4036,6 +4112,7 @@ "version": "0.11.1", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "devOptional": true, "license": "EPL-2.0" }, "node_modules/emoji-regex": { @@ -4049,6 +4126,7 @@ "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" @@ -4072,6 +4150,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -4712,12 +4791,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "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", @@ -4740,12 +4821,14 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "devOptional": true, "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -4796,6 +4879,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "devOptional": true, "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" @@ -4805,6 +4889,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "devOptional": true, "funding": [ { "type": "github", @@ -4857,6 +4942,7 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4925,6 +5011,7 @@ "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", @@ -4985,6 +5072,7 @@ "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" @@ -5048,6 +5136,7 @@ "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": { @@ -5099,6 +5188,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", + "devOptional": true, "license": "MIT", "bin": { "giget": "dist/cli.mjs" @@ -5164,18 +5254,21 @@ "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==", + "devOptional": true, "license": "ISC" }, "node_modules/grammex": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", + "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": { @@ -5293,6 +5386,7 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -5361,6 +5455,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -5665,6 +5760,7 @@ "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": { @@ -5823,6 +5919,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -5847,6 +5944,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -6282,6 +6380,7 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "devOptional": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -6295,6 +6394,7 @@ "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": { @@ -6324,6 +6424,7 @@ "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", @@ -6413,6 +6514,7 @@ "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", @@ -6433,6 +6535,7 @@ "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" @@ -6719,6 +6822,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, "license": "MIT" }, "node_modules/optionator": { @@ -6825,6 +6929,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -6841,12 +6946,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, "license": "MIT" }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, "license": "MIT" }, "node_modules/pg": { @@ -6970,6 +7077,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.2.4", @@ -7029,6 +7137,7 @@ "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" @@ -7091,6 +7200,7 @@ "version": "7.9.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz", "integrity": "sha512-aPqePoZIqwlAchbgbFDO/wHqGB+7H1nj9gaM+OsL9h77S5S3TnLd9BgD3LnoeDikULo7cl2HSUrEyQ55Z7DYbg==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -7136,6 +7246,7 @@ "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", @@ -7147,6 +7258,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, "license": "ISC" }, "node_modules/punycode": { @@ -7163,6 +7275,7 @@ "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", @@ -7217,6 +7330,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, "license": "MIT", "dependencies": { "defu": "^6.1.6", @@ -7255,6 +7369,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -7312,6 +7427,7 @@ "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" @@ -7330,6 +7446,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7389,6 +7506,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -7398,6 +7516,7 @@ "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" @@ -7418,6 +7537,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "devOptional": true, "license": "Unlicense" }, "node_modules/run-parallel": { @@ -7503,6 +7623,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "devOptional": true, "funding": [ { "type": "github", @@ -7525,6 +7646,7 @@ "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": { @@ -7546,7 +7668,8 @@ "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==" + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true }, "node_modules/server-only": { "version": "0.0.1", @@ -7676,6 +7799,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7688,6 +7812,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -7773,6 +7898,7 @@ "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" @@ -7803,6 +7929,7 @@ "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" @@ -7819,6 +7946,7 @@ "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": { @@ -8417,6 +8545,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "devOptional": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -8431,6 +8560,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -8687,6 +8817,7 @@ "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", diff --git a/package.json b/package.json index ddfa187..e498a2c 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,23 @@ { "name": "qrcode-generator", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "typecheck": "tsc --noEmit", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:deploy": "prisma migrate deploy", + "db:studio": "prisma studio" }, "dependencies": { "@prisma/adapter-pg": "^7.9.1", "@prisma/client": "^7.9.1", "next": "16.3.0", - "pg": "^8.23.0", - "prisma": "^7.9.1", + "pg": "^8.16.3", "qrcode": "^1.5.4", "react": "19.2.8", "react-dom": "19.2.8", @@ -23,14 +27,20 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/pg": "^8.21.0", + "@types/pg": "^8.15.6", "@types/qrcode": "^1.5.6", "@types/react": "^19", "@types/react-dom": "^19", - "dotenv": "^17.4.2", + "dotenv": "^17.2.4", "eslint": "^9", "eslint-config-next": "16.3.0", + "prisma": "^7.9.1", "tailwindcss": "^4", "typescript": "^5" + }, + "allowScripts": { + "prisma": true, + "@prisma/engines": true, + "unrs-resolver": true } } diff --git a/prisma.config.js b/prisma.config.js new file mode 100644 index 0000000..4e366f0 --- /dev/null +++ b/prisma.config.js @@ -0,0 +1,26 @@ +// Prisma-Konfiguration (CommonJS, damit sie ohne TypeScript-Loader auch im +// schlanken Laufzeit-Image des Containers gelesen werden kann). +// +// Prisma 7 liest Verbindungsdaten nicht mehr aus schema.prisma, sondern von +// hier — die Anwendung selbst verbindet sich über den pg-Driver-Adapter in +// src/lib/db.ts. + +// Im Container kommt DATABASE_URL aus der Umgebung; lokal ist .env bequemer. +// dotenv ist nur eine Dev-Abhängigkeit und im Image bewusst nicht vorhanden. +try { + require("dotenv/config"); +} catch { + // Kein dotenv vorhanden — die Umgebungsvariablen sind bereits gesetzt. +} + +const { defineConfig } = require("prisma/config"); + +module.exports = defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env.DATABASE_URL, + }, +}); diff --git a/prisma.config.ts b/prisma.config.ts deleted file mode 100644 index 831a20f..0000000 --- a/prisma.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This file was generated by Prisma, and assumes you have installed the following: -// npm install --save-dev prisma dotenv -import "dotenv/config"; -import { defineConfig } from "prisma/config"; - -export default defineConfig({ - schema: "prisma/schema.prisma", - migrations: { - path: "prisma/migrations", - }, - datasource: { - url: process.env["DATABASE_URL"], - }, -}); diff --git a/src/app/api/codes/[id]/qr/route.ts b/src/app/api/codes/[id]/qr/route.ts index ed44d50..e3806fa 100644 --- a/src/app/api/codes/[id]/qr/route.ts +++ b/src/app/api/codes/[id]/qr/route.ts @@ -55,9 +55,9 @@ export async function GET( }; if (isDownload) { - const module = getQrType(code.type); - const name = module - ? downloadFilename(module, code.data, format) + const qrType = getQrType(code.type); + const name = qrType + ? downloadFilename(qrType, code.data, format) : `qr-${code.slug}.${format}`; headers["Content-Disposition"] = `attachment; filename="${name}"`; } diff --git a/src/app/api/codes/[id]/route.ts b/src/app/api/codes/[id]/route.ts index 0ce6566..50ce503 100644 --- a/src/app/api/codes/[id]/route.ts +++ b/src/app/api/codes/[id]/route.ts @@ -60,13 +60,13 @@ export async function PATCH( const update: Prisma.QrCodeUpdateInput = {}; if (parsed.data.data !== undefined) { - const module = getActiveQrType(existing.type); - if (!module) { + const qrType = getActiveQrType(existing.type); + if (!qrType) { throw new QrRequestError( "Der Typ dieses Codes wird in dieser Version nicht unterstützt.", ); } - const validated = module.validate(parsed.data.data); + const validated = qrType.validate(parsed.data.data); if (!validated.ok) { return jsonError(validated.message); } diff --git a/src/app/api/codes/route.ts b/src/app/api/codes/route.ts index ef682cd..a3d88ce 100644 --- a/src/app/api/codes/route.ts +++ b/src/app/api/codes/route.ts @@ -51,20 +51,20 @@ export async function POST(request: NextRequest): Promise { } const { type, design, label } = parsed.data; - const module = getActiveQrType(type); - if (!module) { + const qrType = getActiveQrType(type); + if (!qrType) { throw new QrRequestError( "Dieser QR-Typ ist in dieser Version noch nicht verfügbar.", 404, ); } - if (!module.supportsDynamic) { + if (!qrType.supportsDynamic) { throw new QrRequestError( - `Der QR-Typ „${module.title}" unterstützt keine dynamischen Codes.`, + `Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`, ); } - const validated = module.validate(parsed.data.data); + const validated = qrType.validate(parsed.data.data); if (!validated.ok) { return jsonError(validated.message); } @@ -75,7 +75,7 @@ export async function POST(request: NextRequest): Promise { const created = await prisma.qrCode.create({ data: { slug: generateSlug(), - type: module.id, + type: qrType.id, label: label && label.length > 0 ? label : null, data: toJsonInput(validated.data), ...design, diff --git a/src/app/api/qr/download/route.ts b/src/app/api/qr/download/route.ts index e7f1095..b3c5bc4 100644 --- a/src/app/api/qr/download/route.ts +++ b/src/app/api/qr/download/route.ts @@ -31,7 +31,7 @@ export async function POST(request: NextRequest): Promise { ); } - const { module, data, content } = resolveQrRequest(parsed.data); + const { qrType, data, content } = resolveQrRequest(parsed.data); const body = format === "png" @@ -41,7 +41,7 @@ export async function POST(request: NextRequest): Promise { return new Response(body, { headers: { "Content-Type": CONTENT_TYPES[format], - "Content-Disposition": `attachment; filename="${downloadFilename(module, data, format)}"`, + "Content-Disposition": `attachment; filename="${downloadFilename(qrType, data, format)}"`, "Cache-Control": "no-store", }, }); diff --git a/src/app/erstellen/[type]/page.tsx b/src/app/erstellen/[type]/page.tsx index 713a0e0..d549b7c 100644 --- a/src/app/erstellen/[type]/page.tsx +++ b/src/app/erstellen/[type]/page.tsx @@ -15,15 +15,15 @@ export function generateStaticParams() { export async function generateMetadata({ params }: Props): Promise { const { type } = await params; - const module = getQrType(type); - return { title: module ? `${module.title}-QR-Code` : "QR-Code erstellen" }; + const qrType = getQrType(type); + return { title: qrType ? `${qrType.title}-QR-Code` : "QR-Code erstellen" }; } export default async function CreatePage({ params }: Props) { const { type } = await params; - const module = getQrType(type); + const qrType = getQrType(type); - if (!module) { + if (!qrType) { notFound(); } @@ -33,10 +33,10 @@ export default async function CreatePage({ params }: Props) { return (
- +

- „{module.title}“ ist bald verfügbar + „{qrType.title}“ ist bald verfügbar

Dieser QR-Typ folgt in Version 2. In Version 1 steht der Typ „Website“ @@ -52,5 +52,5 @@ export default async function CreatePage({ params }: Props) { ); } - return ; + return ; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index aeb58cf..8e2c8ad 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -49,7 +49,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {

- QR-Code-Generator · Version 1 — QR-Typ „Website/URL" + QR-Code-Generator · Version 1 — QR-Typ „Website/URL“
diff --git a/src/app/page.tsx b/src/app/page.tsx index 529e0e8..fef005c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -73,7 +73,7 @@ export default function HomePage() { ) : (
{tile} @@ -85,7 +85,7 @@ export default function HomePage() {

- In Version 1 ist der Typ „Website" verfügbar. Die übrigen Typen folgen in + In Version 1 ist der Typ „Website“ verfügbar. Die übrigen Typen folgen in Version 2.

diff --git a/src/app/r/[slug]/route.ts b/src/app/r/[slug]/route.ts index 5098dd4..79d7bf3 100644 --- a/src/app/r/[slug]/route.ts +++ b/src/app/r/[slug]/route.ts @@ -52,14 +52,14 @@ export async function GET( ); } - const module = getQrType(code.type); - if (!module) { + const qrType = getQrType(code.type); + if (!qrType) { return notFound("Der Typ dieses Codes wird nicht mehr unterstützt."); } let target: string; try { - target = module.redirectTarget(code.data); + target = qrType.redirectTarget(code.data); } catch { return notFound("Das Ziel dieses Codes ist nicht mehr gültig."); } diff --git a/src/components/codes-list.tsx b/src/components/codes-list.tsx index da281d4..f0a77d4 100644 --- a/src/components/codes-list.tsx +++ b/src/components/codes-list.tsx @@ -54,6 +54,9 @@ export function CodesList() { }, []); useEffect(() => { + // `load` setzt den Zustand erst nach dem await der Netzwerkantwort, also + // nie synchron während des Effekts — die Regel kann das nicht erkennen. + // eslint-disable-next-line react-hooks/set-state-in-effect void load(); }, [load]); diff --git a/src/components/qr-generator.tsx b/src/components/qr-generator.tsx index 7f46d42..c1c35a1 100644 --- a/src/components/qr-generator.tsx +++ b/src/components/qr-generator.tsx @@ -43,10 +43,10 @@ function sameData(a: unknown, b: unknown): boolean { } export function QrGenerator({ typeId }: { typeId: QrTypeId }) { - const module = qrTypes[typeId]; + const qrType = qrTypes[typeId]; const Form = qrForms[typeId]; - const [data, setData] = useState(module.defaultData); + const [data, setData] = useState(qrType.defaultData); const [design, setDesign] = useState(defaultDesign); const [isDynamic, setIsDynamic] = useState(false); const [label, setLabel] = useState(""); @@ -62,14 +62,14 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) { const [downloading, setDownloading] = useState<"png" | "svg" | null>(null); const [downloadError, setDownloadError] = useState(null); - const validation = useMemo(() => module.validate(data), [module, data]); + const validation = useMemo(() => qrType.validate(data), [qrType, data]); const designValid = useMemo( () => designSchema.safeParse(design).success, [design], ); // Fehler erst zeigen, wenn tatsächlich etwas eingegeben wurde. - const touched = !sameData(data, module.defaultData); + const touched = !sameData(data, qrType.defaultData); const dataError = !validation.ok && touched ? validation.message : null; const mode = isDynamic ? "dynamic" : "static"; @@ -87,18 +87,13 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) { const requestId = useRef(0); useEffect(() => { - if (!ready) { - setPreview(null); - setPreviewError(null); - setPreviewLoading(false); - return; - } + if (!ready) return; const id = (requestId.current += 1); const controller = new AbortController(); - setPreviewLoading(true); const timer = setTimeout(() => { + setPreviewLoading(true); apiJson("/api/qr/preview", { method: "POST", json: requestBody, @@ -129,6 +124,13 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) { }; }, [ready, requestBody]); + // Solange die Eingaben unvollständig sind, gibt es nichts anzuzeigen. Das + // wird abgeleitet statt im Effekt zurückgesetzt — so bleibt beim Tippen die + // zuletzt gültige Vorschau stehen, statt bei jedem Zeichen zu flackern. + const shownPreview = ready ? preview : null; + const shownPreviewError = ready ? previewError : null; + const shownPreviewLoading = ready && previewLoading; + /* ------------------------------------------------------------ Download */ const downloadDisabledReason = @@ -224,13 +226,13 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
- +

- {module.title}-QR-Code erstellen + {qrType.title}-QR-Code erstellen

-

{module.description}

+

{qrType.description}

@@ -279,7 +281,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
@@ -401,11 +403,11 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) { Vorschau { export function toQrCodeDto(code: QrCode): QrCodeDto { const data = asRecord(code.data); - const module = getQrType(code.type); + const qrType = getQrType(code.type); let target = ""; let summary = ""; - if (module) { + if (qrType) { try { - target = module.redirectTarget(data); - summary = module.summary(data); + target = qrType.redirectTarget(data); + summary = qrType.summary(data); } catch { // Datensatz passt nicht mehr zum Typ-Schema (z. B. nach einem // Schema-Wechsel). Die Liste soll trotzdem anzeigbar bleiben. diff --git a/src/lib/db.ts b/src/lib/db.ts index 99c1e72..9836556 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -5,20 +5,40 @@ import { PrismaPg } from "@prisma/adapter-pg"; import { PrismaClient } from "@/generated/prisma/client"; import { getDatabaseUrl } from "@/lib/env"; +type Client = InstanceType; + // Im Entwicklungsmodus wird das Modul bei jedem Hot-Reload neu ausgewertet. // Ohne diesen Cache entstünde pro Reload ein neuer Verbindungspool. -const globalForPrisma = globalThis as unknown as { - prisma?: ReturnType; -}; +const globalForPrisma = globalThis as unknown as { prisma?: Client }; -function createPrismaClient() { - return new PrismaClient({ - adapter: new PrismaPg({ connectionString: getDatabaseUrl() }), - }); +let client: Client | undefined; + +function getClient(): Client { + if (!client) { + client = + globalForPrisma.prisma ?? + new PrismaClient({ + adapter: new PrismaPg({ connectionString: getDatabaseUrl() }), + }); + if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = client; + } + } + return client; } -export const prisma = globalForPrisma.prisma ?? createPrismaClient(); - -if (process.env.NODE_ENV !== "production") { - globalForPrisma.prisma = prisma; -} +/** + * Der Client wird erst beim ersten Zugriff erzeugt. + * + * Beim Produktionsbuild lädt Next.js die Routen-Module, um ihre Konfiguration + * einzusammeln. Zu diesem Zeitpunkt gibt es weder eine Datenbank noch eine + * DATABASE_URL — ein sofort verbindender Client würde den Build scheitern + * lassen. + */ +export const prisma: Client = new Proxy({} as Client, { + get(_target, property) { + const instance = getClient() as unknown as Record; + const value = instance[property]; + return typeof value === "function" ? value.bind(instance) : value; + }, +}); diff --git a/src/lib/qr/contrast.ts b/src/lib/qr/contrast.ts index b18202e..28378fd 100644 --- a/src/lib/qr/contrast.ts +++ b/src/lib/qr/contrast.ts @@ -3,7 +3,7 @@ * * Scanner erkennen einen Code über den Helligkeitsunterschied zwischen * Modul- und Hintergrundfarbe. Zu geringer Kontrast — und ebenso ein heller - * Vordergrund auf dunklem Hintergrund („invertierter" Code) — führt bei + * Vordergrund auf dunklem Hintergrund („invertierter“ Code) — führt bei * vielen Kameras zu Lesefehlern. */ diff --git a/src/lib/qr/registry.ts b/src/lib/qr/registry.ts index 9b39077..1ed785e 100644 --- a/src/lib/qr/registry.ts +++ b/src/lib/qr/registry.ts @@ -105,7 +105,7 @@ const MODULES: readonly QrTypeModule[] = [ ]; export const qrTypes: Record = Object.fromEntries( - MODULES.map((module) => [module.id, module]), + MODULES.map((qrType) => [qrType.id, qrType]), ) as Record; const missing = QR_TYPE_IDS.filter((id) => !qrTypes[id]); @@ -122,8 +122,8 @@ export function getQrType(id: string): QrTypeModule | undefined { /** Liefert den Typ nur, wenn er in dieser Version nutzbar ist. */ export function getActiveQrType(id: string): QrTypeModule | undefined { - const module = getQrType(id); - return module?.active ? module : undefined; + const qrType = getQrType(id); + return qrType?.active ? qrType : undefined; } export { QR_TYPE_IDS, isQrTypeId }; diff --git a/src/lib/qr/request.ts b/src/lib/qr/request.ts index 673b5c7..9ed1b83 100644 --- a/src/lib/qr/request.ts +++ b/src/lib/qr/request.ts @@ -41,7 +41,7 @@ export class QrRequestError extends Error { } export type ResolvedQrRequest = { - module: QrTypeModule; + qrType: QrTypeModule; data: Record; /** Der Text, der tatsächlich in den QR-Code kodiert wird. */ content: string; @@ -52,23 +52,23 @@ export type ResolvedQrRequest = { * bei statischen Codes die Nutzdaten selbst, bei dynamischen die Kurz-URL. */ export function resolveQrRequest(request: QrRequest): ResolvedQrRequest { - const module = getActiveQrType(request.type); - if (!module) { + const qrType = getActiveQrType(request.type); + if (!qrType) { throw new QrRequestError( "Dieser QR-Typ ist in dieser Version noch nicht verfügbar.", 404, ); } - const validated = module.validate(request.data); + const validated = qrType.validate(request.data); if (!validated.ok) { throw new QrRequestError(validated.message); } if (request.mode === "dynamic") { - if (!module.supportsDynamic) { + if (!qrType.supportsDynamic) { throw new QrRequestError( - `Der QR-Typ „${module.title}" unterstützt keine dynamischen Codes.`, + `Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`, ); } const slug = request.slug; @@ -76,28 +76,28 @@ export function resolveQrRequest(request: QrRequest): ResolvedQrRequest { throw new QrRequestError("Ungültige Kurz-URL."); } return { - module, + qrType, data: validated.data, content: shortUrl(slug ?? PREVIEW_SLUG), }; } return { - module, + qrType, data: validated.data, - content: module.encode(validated.data), + content: qrType.encode(validated.data), }; } /** Dateiname für den Download, abgeleitet aus dem Inhalt des Codes. */ export function downloadFilename( - module: QrTypeModule, + qrType: QrTypeModule, data: unknown, extension: string, ): string { let base = "qr-code"; try { - const summary = module + const summary = qrType .summary(data) .toLowerCase() .replaceAll("ä", "ae") diff --git a/src/lib/qr/type-module.ts b/src/lib/qr/type-module.ts index bc0b25f..a76c7e1 100644 --- a/src/lib/qr/type-module.ts +++ b/src/lib/qr/type-module.ts @@ -5,7 +5,7 @@ import type { IconName } from "@/components/icons"; /** * Alle QR-Typen, die die Anwendung kennt. V1 implementiert davon nur * "website"; die übrigen Einträge sind als Platzhalter registriert und - * erscheinen in der Oberfläche mit dem Badge „Bald verfügbar". + * erscheinen in der Oberfläche mit dem Badge „Bald verfügbar“. */ export const QR_TYPE_IDS = [ "website", @@ -148,7 +148,7 @@ export function defineComingSoonType(spec: { }): QrTypeModule { const notAvailable = (): never => { throw new QrTypeError( - `Der QR-Typ „${spec.title}" ist in dieser Version noch nicht verfügbar.`, + `Der QR-Typ „${spec.title}“ ist in dieser Version noch nicht verfügbar.`, ); }; @@ -162,7 +162,7 @@ export function defineComingSoonType(spec: { defaultData: {}, validate: () => ({ ok: false, - message: `Der QR-Typ „${spec.title}" ist in dieser Version noch nicht verfügbar.`, + message: `Der QR-Typ „${spec.title}“ ist in dieser Version noch nicht verfügbar.`, }), encode: notAvailable, redirectTarget: notAvailable,