Files
qrcode-generator/src/components/qr-preview.tsx
T
Florian Wilde ad4cd92f8e 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.
2026-08-13 15:54:47 +02:00

95 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}