V1: Next.js-App mit QR-Typ-Registry, Website-Generator und dynamischen Codes

- Erweiterbare Registry aller 15 QR-Typen; nur "Website" ist aktiv, die
  uebrigen erscheinen als Platzhalter mit Badge "Bald verfuegbar".
- defineQrType() kapselt Zod-Schema, Kodierung und Redirect-Ziel je Typ,
  sodass ein V2-Typ nur aus einem Registry-Eintrag plus Formular besteht.
- Serverseitige QR-Erzeugung (PNG/SVG) mit Farben, Groesse, Fehlerkorrektur
  und Rand; Live-Vorschau ueber /api/qr/preview.
- Dynamische Codes: Kurz-URL {BASE_URL}/r/{slug}, 302-Redirect mit
  Scan-Zaehler, CRUD unter /api/codes.
- Prisma 7 mit pg-Driver-Adapter und Initial-Migration fuer PostgreSQL.
- Deutsche Oberflaeche mit Tailwind CSS, helles Theme, responsiv.
This commit is contained in:
Florian Wilde
2026-08-13 15:54:47 +02:00
commit ad4cd92f8e
52 changed files with 12348 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
/** Kleine Hilfen für Aufrufe der eigenen API aus Client-Komponenten. */
async function readError(response: Response): Promise<string> {
try {
const body: unknown = await response.json();
if (
body !== null &&
typeof body === "object" &&
"error" in body &&
typeof (body as { error: unknown }).error === "string"
) {
return (body as { error: string }).error;
}
} catch {
// Antwort ohne JSON-Körper — unten kommt die allgemeine Meldung.
}
return `Die Anfrage ist fehlgeschlagen (HTTP ${response.status}).`;
}
export class ApiError extends Error {}
export async function apiJson<T>(
url: string,
init?: RequestInit & { json?: unknown },
): Promise<T> {
const { json, ...rest } = init ?? {};
const response = await fetch(url, {
...rest,
headers:
json === undefined
? rest.headers
: { "Content-Type": "application/json", ...rest.headers },
body: json === undefined ? rest.body : JSON.stringify(json),
});
if (!response.ok) {
throw new ApiError(await readError(response));
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
/** Holt eine Datei von der API und stößt den Browser-Download an. */
export async function downloadFile(
url: string,
json: unknown,
fallbackName: string,
): Promise<void> {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(json),
});
if (!response.ok) {
throw new ApiError(await readError(response));
}
const disposition = response.headers.get("Content-Disposition") ?? "";
const match = /filename="([^"]+)"/.exec(disposition);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = objectUrl;
anchor.download = match?.[1] ?? fallbackName;
document.body.append(anchor);
anchor.click();
anchor.remove();
// Der Browser braucht den Blob noch einen Moment für den Download.
setTimeout(() => URL.revokeObjectURL(objectUrl), 10_000);
}