Docker-Stack, README und Korrekturen aus dem ersten Durchlauf

- Dockerfile (4 Stufen) und docker-compose.yml mit Postgres-Healthcheck,
  Volume und direktem Port-Mapping 3000:3000, ohne Reverse Proxy.
- Entrypoint wendet beim Start "prisma migrate deploy" an; die Prisma-CLI
  bekommt eine eigene Build-Stufe, weil sie mehr als nur @prisma/* braucht.
- prisma.config.ts durch prisma.config.js ersetzt: laedt ohne TypeScript und
  ohne dotenv, damit sie im Laufzeit-Image funktioniert.
- Prisma-Client wird erst beim ersten Zugriff erzeugt. Vorher scheiterte
  "next build" beim Einsammeln der Routen-Konfiguration ohne DATABASE_URL.
- Postgres-18-Volume auf /var/lib/postgresql gemountet (neues Datenlayout).
- npm 11 blockiert Installationsskripte: erlaubte Pakete stehen jetzt im
  Feld "allowScripts" der package.json.
- Lokale Variable "module" ueberall in "qrType" umbenannt (verdeckte sonst
  die CommonJS-Variable) und Vorschau-Effekt ohne synchrones setState.
- README.md auf Deutsch: Setup, Konfiguration, Bedienung, Aufbau, API und
  Ausblick auf Version 2.
This commit is contained in:
Florian Wilde
2026-08-13 16:48:28 +02:00
parent ad4cd92f8e
commit 5806dc4ce0
26 changed files with 751 additions and 129 deletions
+16
View File
@@ -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
+76
View File
@@ -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"]
+317 -21
View File
@@ -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 <http://localhost:3000> 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.
+39
View File
@@ -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:
+9
View File
@@ -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 "$@"
+8
View File
@@ -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;
+138 -7
View File
@@ -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",
+16 -6
View File
@@ -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
}
}
+26
View File
@@ -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,
},
});
-14
View File
@@ -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"],
},
});
+3 -3
View File
@@ -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}"`;
}
+3 -3
View File
@@ -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);
}
+6 -6
View File
@@ -51,20 +51,20 @@ export async function POST(request: NextRequest): Promise<Response> {
}
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<Response> {
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,
+2 -2
View File
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest): Promise<Response> {
);
}
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<Response> {
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",
},
});
+7 -7
View File
@@ -15,15 +15,15 @@ export function generateStaticParams() {
export async function generateMetadata({ params }: Props): Promise<Metadata> {
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 (
<div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6">
<span className="mx-auto flex size-14 items-center justify-center rounded-2xl bg-slate-100 text-slate-400">
<Icon name={module.icon} className="size-7" />
<Icon name={qrType.icon} className="size-7" />
</span>
<h1 className="mt-5 text-2xl font-bold text-slate-900">
„{module.title}“ ist bald verfügbar
„{qrType.title}“ ist bald verfügbar
</h1>
<p className="mt-2 text-slate-600">
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 <QrGenerator typeId={module.id} />;
return <QrGenerator typeId={qrType.id} />;
}
+1 -1
View File
@@ -49,7 +49,7 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
<footer className="border-t border-slate-200 bg-white">
<div className="mx-auto max-w-6xl px-4 py-6 text-sm text-slate-500 sm:px-6">
QR-Code-Generator · Version 1 — QR-Typ „Website/URL"
QR-Code-Generator · Version 1 — QR-Typ „Website/URL“
</div>
</footer>
</body>
+2 -2
View File
@@ -73,7 +73,7 @@ export default function HomePage() {
) : (
<div
aria-disabled="true"
title={`„${type.title}" folgt in Version 2`}
title={`„${type.title}“ folgt in Version 2`}
className="flex h-full cursor-not-allowed items-start gap-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50/60 p-4"
>
{tile}
@@ -85,7 +85,7 @@ export default function HomePage() {
</ul>
<p className="mt-8 text-sm text-slate-500">
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.
</p>
</div>
+3 -3
View File
@@ -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.");
}
+3
View File
@@ -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]);
+22 -20
View File
@@ -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<QrFormData>(module.defaultData);
const [data, setData] = useState<QrFormData>(qrType.defaultData);
const [design, setDesign] = useState<QrDesign>(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<string | null>(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<PreviewState>("/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 }) {
<div className="mb-7 flex flex-wrap items-center gap-3">
<span className="flex size-11 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600">
<Icon name={module.icon} className="size-6" />
<Icon name={qrType.icon} className="size-6" />
</span>
<div>
<h1 className="text-2xl font-bold tracking-tight text-slate-900">
{module.title}-QR-Code erstellen
{qrType.title}-QR-Code erstellen
</h1>
<p className="text-sm text-slate-600">{module.description}</p>
<p className="text-sm text-slate-600">{qrType.description}</p>
</div>
</div>
@@ -279,7 +281,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
<button
type="button"
aria-pressed={isDynamic}
disabled={!module.supportsDynamic}
disabled={!qrType.supportsDynamic}
onClick={() => setIsDynamic(true)}
className={cx(
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
@@ -353,7 +355,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
href="/meine-codes"
className="inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-indigo-600 hover:text-indigo-700"
>
Zu „Meine Codes"
Zu „Meine Codes“
<Icon name="external" className="size-3.5" />
</Link>
</div>
@@ -401,11 +403,11 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
<Card>
<CardTitle>Vorschau</CardTitle>
<QrPreview
dataUrl={preview?.dataUrl ?? null}
content={preview?.content ?? null}
dataUrl={shownPreview?.dataUrl ?? null}
content={shownPreview?.content ?? null}
size={design.size}
error={previewError ?? downloadError}
loading={previewLoading}
error={shownPreviewError ?? downloadError}
loading={shownPreviewLoading}
downloadDisabledReason={downloadDisabledReason}
onDownload={handleDownload}
downloading={downloading}
+4 -4
View File
@@ -28,14 +28,14 @@ function asRecord(value: unknown): Record<string, unknown> {
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.
+32 -12
View File
@@ -5,20 +5,40 @@ import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@/generated/prisma/client";
import { getDatabaseUrl } from "@/lib/env";
type Client = InstanceType<typeof PrismaClient>;
// 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<typeof createPrismaClient>;
};
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<string | symbol, unknown>;
const value = instance[property];
return typeof value === "function" ? value.bind(instance) : value;
},
});
+1 -1
View File
@@ -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.
*/
+3 -3
View File
@@ -105,7 +105,7 @@ const MODULES: readonly QrTypeModule[] = [
];
export const qrTypes: Record<QrTypeId, QrTypeModule> = Object.fromEntries(
MODULES.map((module) => [module.id, module]),
MODULES.map((qrType) => [qrType.id, qrType]),
) as Record<QrTypeId, QrTypeModule>;
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 };
+11 -11
View File
@@ -41,7 +41,7 @@ export class QrRequestError extends Error {
}
export type ResolvedQrRequest = {
module: QrTypeModule;
qrType: QrTypeModule;
data: Record<string, unknown>;
/** 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")
+3 -3
View File
@@ -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,