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
+86
View File
@@ -0,0 +1,86 @@
import type { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { getQrType } from "@/lib/qr/registry";
import { isValidSlug } from "@/lib/slug";
export const dynamic = "force-dynamic";
type Context = { params: Promise<{ slug: string }> };
function notFound(message: string): Response {
return new Response(
`<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>QR-Code nicht gefunden</title>
</head>
<body style="font-family: system-ui, sans-serif; margin: 0; display: grid; place-items: center; min-height: 100vh; background: #f8fafc; color: #0f172a;">
<main style="text-align: center; padding: 2rem; max-width: 32rem;">
<p style="font-size: 3rem; margin: 0;">🔍</p>
<h1 style="font-size: 1.5rem; margin: 1rem 0 0.5rem;">QR-Code nicht gefunden</h1>
<p style="color: #475569; line-height: 1.6;">${message}</p>
</main>
</body>
</html>`,
{ status: 404, headers: { "Content-Type": "text/html; charset=utf-8" } },
);
}
/**
* Weiterleitung eines dynamischen QR-Codes.
*
* Bewusst 302 (temporär): Das Ziel ist jederzeit änderbar, ein permanenter
* Redirect würde in Browsern und Zwischenspeichern hängen bleiben.
*/
export async function GET(
_request: NextRequest,
context: Context,
): Promise<Response> {
const { slug } = await context.params;
if (!isValidSlug(slug)) {
return notFound("Diese Kurz-Adresse ist ungültig.");
}
const code = await prisma.qrCode.findUnique({ where: { slug } });
if (!code) {
return notFound(
"Der Code wurde gelöscht oder hat nie existiert. Bitte wende dich an denjenigen, der ihn erstellt hat.",
);
}
const module = getQrType(code.type);
if (!module) {
return notFound("Der Typ dieses Codes wird nicht mehr unterstützt.");
}
let target: string;
try {
target = module.redirectTarget(code.data);
} catch {
return notFound("Das Ziel dieses Codes ist nicht mehr gültig.");
}
// Der Zähler darf die Weiterleitung nicht ausbremsen oder blockieren.
try {
await prisma.qrCode.update({
where: { id: code.id },
data: { scans: { increment: 1 }, lastScanAt: new Date() },
});
} catch (error) {
console.error("Scan-Zähler konnte nicht erhöht werden:", error);
}
return new Response(null, {
status: 302,
headers: {
Location: target,
// Ohne dies würden Browser die Weiterleitung zwischenspeichern und
// spätere Zieländerungen sowie der Scan-Zähler blieben wirkungslos.
"Cache-Control": "no-store, no-cache, must-revalidate",
},
});
}