feat(frontend): QR-Code-Dialog auf Objekt-Detailseiten (Modul 22)

Wiederverwendbarer QrCodeDialog (authentifizierter Blob-Fetch)
auf Volk-, Bienenstand-, Koenigin-, Honigcharge-Detailseiten und
je Lagerbehaelter-Zeile eingebunden.
This commit is contained in:
Nick Szaunig committed 2026-08-22 10:58:40 +02:00
1 parent dd88b6a92c
commit 8b7fd61c1c
8 files changed
+102 -3

No files matched your search

@@ -0,0 +1,65 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { QrCode } from "lucide-react";
import { fetchQrCodeSvg, QrEntityType } from "@/lib/qr/api";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
interface QrCodeDialogProps {
entityType: QrEntityType;
id: string;
label?: string;
}
export function QrCodeDialog({ entityType, id, label = "QR-Code" }: QrCodeDialogProps) {
const [open, setOpen] = useState(false);
const { data: blob, isLoading, isError } = useQuery({
queryKey: ["qr", entityType, id],
queryFn: () => fetchQrCodeSvg(entityType, id),
enabled: open,
});
const objectUrl = useMemo(() => (blob ? URL.createObjectURL(blob) : null), [blob]);
useEffect(() => {
return () => {
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [objectUrl]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="outline" size="sm">
<QrCode className="size-4" />
{label}
</Button>
}
/>
<DialogContent className="flex flex-col items-center gap-4">
<DialogHeader>
<DialogTitle>{label}</DialogTitle>
</DialogHeader>
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
{isError ? (
<p role="alert" className="text-sm text-destructive">
QR-Code konnte nicht geladen werden.
</p>
) : null}
{objectUrl ? (
// eslint-disable-next-line @next/next/no-img-element -- blob: object URL, next/image cannot handle this
<img src={objectUrl} alt="QR-Code" className="size-64 rounded-lg border bg-white p-3" />
) : null}
</DialogContent>
</Dialog>
);
}