- 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.
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
import { Icon } from "@/components/icons";
|
||
import { Button, Notice } from "@/components/ui";
|
||
|
||
export function QrPreview({
|
||
dataUrl,
|
||
content,
|
||
size,
|
||
error,
|
||
loading,
|
||
downloadDisabledReason,
|
||
onDownload,
|
||
downloading,
|
||
}: {
|
||
dataUrl: string | null;
|
||
content: string | null;
|
||
size: number;
|
||
error: string | null;
|
||
loading: boolean;
|
||
downloadDisabledReason: string | null;
|
||
onDownload: (format: "png" | "svg") => void;
|
||
downloading: "png" | "svg" | null;
|
||
}) {
|
||
return (
|
||
<div className="space-y-4">
|
||
<div
|
||
className="qr-canvas relative grid aspect-square w-full place-items-center overflow-hidden rounded-xl border border-slate-200 p-4"
|
||
aria-busy={loading}
|
||
>
|
||
{dataUrl ? (
|
||
/* eslint-disable-next-line @next/next/no-img-element -- Data-URL, keine Optimierung nötig oder möglich */
|
||
<img
|
||
src={dataUrl}
|
||
alt="Vorschau des QR-Codes"
|
||
className="h-full w-full object-contain"
|
||
/>
|
||
) : (
|
||
<div className="px-6 text-center text-sm text-slate-400">
|
||
<Icon name="qr" className="mx-auto mb-2 size-10" />
|
||
{error ? "Keine Vorschau verfügbar" : "Gib Daten ein, um die Vorschau zu sehen"}
|
||
</div>
|
||
)}
|
||
{loading ? (
|
||
<span className="absolute top-3 right-3 rounded-full bg-white/90 px-2 py-0.5 text-xs font-medium text-slate-500 shadow-sm">
|
||
aktualisiert …
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
|
||
{error ? <Notice tone="error">{error}</Notice> : null}
|
||
|
||
{content ? (
|
||
<div className="rounded-lg bg-slate-50 px-3 py-2.5">
|
||
<p className="text-xs font-medium text-slate-500">Kodierter Inhalt</p>
|
||
<p className="mt-0.5 font-mono text-xs break-all text-slate-700">
|
||
{content}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<Button
|
||
type="button"
|
||
variant="primary"
|
||
disabled={!dataUrl || Boolean(downloadDisabledReason) || downloading !== null}
|
||
onClick={() => onDownload("png")}
|
||
>
|
||
<Icon name="download" className="size-4" />
|
||
{downloading === "png" ? "PNG …" : "PNG"}
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="secondary"
|
||
disabled={!dataUrl || Boolean(downloadDisabledReason) || downloading !== null}
|
||
onClick={() => onDownload("svg")}
|
||
>
|
||
<Icon name="download" className="size-4" />
|
||
{downloading === "svg" ? "SVG …" : "SVG"}
|
||
</Button>
|
||
</div>
|
||
|
||
{downloadDisabledReason ? (
|
||
<p className="text-center text-xs text-slate-500">
|
||
{downloadDisabledReason}
|
||
</p>
|
||
) : (
|
||
<p className="text-center text-xs text-slate-500">
|
||
PNG mit {size} × {size} px · SVG frei skalierbar
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|