feat: NFC-Tags beschreiben mit demselben Deep-Link wie der QR-Code

Neuer Endpunkt GET /api/v1/qr/:entityType/:id/link liefert den vom
QR-Dienst bereits genutzten Deep-Link als reine URL statt als SVG,
damit die Web-NFC-API (client-seitig, Chrome/Android) ihn per
NDEFReader.write() auf einen physischen NFC-Tag schreiben kann.
QrService.resolveUrl() ist aus generateSvg() herausgezogen, damit
beide Endpunkte exakt denselben Link erzeugen.

Der bestehende QrCodeDialog bekommt dadurch für alle sechs
QR-fähigen Entitäten (Völker, Bienenstände, Königinnen, Honigchargen,
Lagerbehälter, Abfüllungen) automatisch einen "Auf NFC-Tag
schreiben"-Button, der nur erscheint, wenn window.NDEFReader
tatsächlich existiert – kein Fallback für andere Browser oder iOS,
wie in der Lastenheft vorgesehen.
This commit is contained in:
Nick Szaunig committed 2026-08-25 01:50:14 +02:00
1 parent f346859121
commit 53b781aa04
8 files changed
+106 -5

No files matched your search

@@ -2,8 +2,10 @@
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 { QrCode, Nfc } from "lucide-react";
import { toast } from "sonner";
import { fetchQrCodeSvg, fetchQrLink, QrEntityType } from "@/lib/qr/api";
import { isNfcSupported, writeNfcTag } from "@/lib/nfc/write";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -21,6 +23,8 @@ interface QrCodeDialogProps {
export function QrCodeDialog({ entityType, id, label = "QR-Code" }: QrCodeDialogProps) {
const [open, setOpen] = useState(false);
const [isWritingNfc, setIsWritingNfc] = useState(false);
const nfcSupported = useMemo(() => isNfcSupported(), []);
const { data: blob, isLoading, isError } = useQuery({
queryKey: ["qr", entityType, id],
@@ -35,6 +39,19 @@ export function QrCodeDialog({ entityType, id, label = "QR-Code" }: QrCodeDialog
};
}, [objectUrl]);
async function handleWriteNfc() {
setIsWritingNfc(true);
try {
const url = await fetchQrLink(entityType, id);
await writeNfcTag(url);
toast.success("NFC-Tag beschrieben");
} catch {
toast.error("NFC-Tag konnte nicht beschrieben werden.");
} finally {
setIsWritingNfc(false);
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
@@ -59,6 +76,17 @@ export function QrCodeDialog({ entityType, id, label = "QR-Code" }: QrCodeDialog
// 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}
{nfcSupported ? (
<Button
variant="outline"
size="sm"
onClick={() => void handleWriteNfc()}
disabled={isWritingNfc}
>
<Nfc className="size-4" />
{isWritingNfc ? "Tag bereithalten…" : "Auf NFC-Tag schreiben"}
</Button>
) : null}
</DialogContent>
</Dialog>
);
+11
View File
@@ -0,0 +1,11 @@
export function isNfcSupported(): boolean {
return typeof window !== "undefined" && "NDEFReader" in window;
}
export async function writeNfcTag(url: string): Promise<void> {
if (!window.NDEFReader) {
throw new Error("Web NFC wird von diesem Browser nicht unterstützt.");
}
const reader = new window.NDEFReader();
await reader.write({ records: [{ recordType: "url", data: url }] });
}
+6 -1
View File
@@ -1,4 +1,4 @@
import { apiFetchBlob } from "@/lib/auth/api-client";
import { apiFetch, apiFetchBlob } from "@/lib/auth/api-client";
export type QrEntityType =
| "colonies"
@@ -12,3 +12,8 @@ export async function fetchQrCodeSvg(entityType: QrEntityType, id: string): Prom
const { blob } = await apiFetchBlob(`/qr/${entityType}/${id}.svg`, `${entityType}.svg`);
return blob;
}
export async function fetchQrLink(entityType: QrEntityType, id: string): Promise<string> {
const { url } = await apiFetch<{ url: string }>(`/qr/${entityType}/${id}/link`);
return url;
}
+16
View File
@@ -0,0 +1,16 @@
interface NDEFRecordInit {
recordType: string;
data?: string;
}
interface NDEFWriteOptions {
records: NDEFRecordInit[];
}
interface NDEFReader {
write(options: NDEFWriteOptions): Promise<void>;
}
interface Window {
NDEFReader?: { new (): NDEFReader };
}