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:
@@ -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
@@ -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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
@@ -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 "$@"
|
||||
@@ -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;
|
||||
|
||||
Generated
+138
-7
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
});
|
||||
@@ -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}"`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
+30
-10
@@ -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({
|
||||
let client: Client | undefined;
|
||||
|
||||
function getClient(): Client {
|
||||
if (!client) {
|
||||
client =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: getDatabaseUrl() }),
|
||||
});
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
globalForPrisma.prisma = client;
|
||||
}
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user