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