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,115 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { designSchema } from "@/lib/qr/design";
|
||||
import { getActiveQrType } from "@/lib/qr/registry";
|
||||
import { QR_TYPE_IDS, type QrTypeModule } from "@/lib/qr/type-module";
|
||||
import { shortUrl } from "@/lib/env";
|
||||
import { isValidSlug } from "@/lib/slug";
|
||||
|
||||
/**
|
||||
* Platzhalter-Slug für die Vorschau eines noch nicht gespeicherten
|
||||
* dynamischen Codes. Gleiche Länge wie ein echter Slug, damit die Vorschau
|
||||
* die spätere Modul-Dichte korrekt zeigt.
|
||||
*/
|
||||
export const PREVIEW_SLUG = "vorschau";
|
||||
|
||||
export const qrModeSchema = z.enum(["static", "dynamic"], {
|
||||
error: "Ungültiger Modus.",
|
||||
});
|
||||
|
||||
export type QrMode = z.infer<typeof qrModeSchema>;
|
||||
|
||||
export const qrRequestSchema = z.object({
|
||||
type: z.enum(QR_TYPE_IDS, { error: "Unbekannter QR-Typ." }),
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
design: designSchema,
|
||||
mode: qrModeSchema.default("static"),
|
||||
/** Slug eines bereits gespeicherten dynamischen Codes. */
|
||||
slug: z.string().trim().optional(),
|
||||
});
|
||||
|
||||
export type QrRequest = z.infer<typeof qrRequestSchema>;
|
||||
|
||||
/** Fehler mit einer Meldung, die unverändert an die Oberfläche geht. */
|
||||
export class QrRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number = 400,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export type ResolvedQrRequest = {
|
||||
module: QrTypeModule;
|
||||
data: Record<string, unknown>;
|
||||
/** Der Text, der tatsächlich in den QR-Code kodiert wird. */
|
||||
content: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prüft eine Anfrage vollständig und liefert den zu kodierenden Inhalt:
|
||||
* bei statischen Codes die Nutzdaten selbst, bei dynamischen die Kurz-URL.
|
||||
*/
|
||||
export function resolveQrRequest(request: QrRequest): ResolvedQrRequest {
|
||||
const module = getActiveQrType(request.type);
|
||||
if (!module) {
|
||||
throw new QrRequestError(
|
||||
"Dieser QR-Typ ist in dieser Version noch nicht verfügbar.",
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
const validated = module.validate(request.data);
|
||||
if (!validated.ok) {
|
||||
throw new QrRequestError(validated.message);
|
||||
}
|
||||
|
||||
if (request.mode === "dynamic") {
|
||||
if (!module.supportsDynamic) {
|
||||
throw new QrRequestError(
|
||||
`Der QR-Typ „${module.title}" unterstützt keine dynamischen Codes.`,
|
||||
);
|
||||
}
|
||||
const slug = request.slug;
|
||||
if (slug !== undefined && !isValidSlug(slug)) {
|
||||
throw new QrRequestError("Ungültige Kurz-URL.");
|
||||
}
|
||||
return {
|
||||
module,
|
||||
data: validated.data,
|
||||
content: shortUrl(slug ?? PREVIEW_SLUG),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
module,
|
||||
data: validated.data,
|
||||
content: module.encode(validated.data),
|
||||
};
|
||||
}
|
||||
|
||||
/** Dateiname für den Download, abgeleitet aus dem Inhalt des Codes. */
|
||||
export function downloadFilename(
|
||||
module: QrTypeModule,
|
||||
data: unknown,
|
||||
extension: string,
|
||||
): string {
|
||||
let base = "qr-code";
|
||||
try {
|
||||
const summary = module
|
||||
.summary(data)
|
||||
.toLowerCase()
|
||||
.replaceAll("ä", "ae")
|
||||
.replaceAll("ö", "oe")
|
||||
.replaceAll("ü", "ue")
|
||||
.replaceAll("ß", "ss")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 60);
|
||||
if (summary) base = `qr-${summary}`;
|
||||
} catch {
|
||||
// Zusammenfassung ist optional — im Zweifel bleibt der Standardname.
|
||||
}
|
||||
return `${base}.${extension}`;
|
||||
}
|
||||
Reference in New Issue
Block a user