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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user