128 lines
3.3 KiB
TypeScript
128 lines
3.3 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { orpc } from "@/utils/orpc";
|
|
|
|
interface QrCodeCanvasProps {
|
|
token: string;
|
|
}
|
|
|
|
function QrCodeCanvas({ token }: QrCodeCanvasProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!canvasRef.current || !token) return;
|
|
// Dynamic import to avoid SSR issues
|
|
import("qrcode").then((QRCode) => {
|
|
if (canvasRef.current) {
|
|
QRCode.toCanvas(canvasRef.current, token, {
|
|
width: 280,
|
|
margin: 2,
|
|
color: { dark: "#214e51", light: "#ffffff" },
|
|
});
|
|
}
|
|
});
|
|
}, [token]);
|
|
|
|
return <canvas ref={canvasRef} className="rounded-xl" />;
|
|
}
|
|
|
|
function useCountdown(expiresAt: Date | undefined) {
|
|
const [secondsLeft, setSecondsLeft] = useState<number>(0);
|
|
|
|
useEffect(() => {
|
|
if (!expiresAt) return;
|
|
|
|
const tick = () => {
|
|
const diff = Math.max(
|
|
0,
|
|
Math.floor((expiresAt.getTime() - Date.now()) / 1000),
|
|
);
|
|
setSecondsLeft(diff);
|
|
};
|
|
|
|
tick();
|
|
const interval = setInterval(tick, 1000);
|
|
return () => clearInterval(interval);
|
|
}, [expiresAt]);
|
|
|
|
return secondsLeft;
|
|
}
|
|
|
|
export function QrCodeDisplay() {
|
|
const { data, refetch, isLoading, isError } = useQuery(
|
|
orpc.drinkkaart.getMyQrToken.queryOptions(),
|
|
);
|
|
|
|
const expiresAt = data?.expiresAt ? new Date(data.expiresAt) : undefined;
|
|
const secondsLeft = useCountdown(expiresAt);
|
|
|
|
// Auto-refresh 60s before expiry
|
|
useEffect(() => {
|
|
if (!expiresAt) return;
|
|
const refreshAt = expiresAt.getTime() - 60_000;
|
|
const delay = refreshAt - Date.now();
|
|
if (delay <= 0) {
|
|
refetch();
|
|
return;
|
|
}
|
|
const timer = setTimeout(() => refetch(), delay);
|
|
return () => clearTimeout(timer);
|
|
}, [expiresAt, refetch]);
|
|
|
|
// Refresh on tab focus if token is about to expire
|
|
useEffect(() => {
|
|
const onVisibility = () => {
|
|
if (document.visibilityState === "visible" && expiresAt) {
|
|
if (Date.now() >= expiresAt.getTime() - 10_000) refetch();
|
|
}
|
|
};
|
|
document.addEventListener("visibilitychange", onVisibility);
|
|
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
}, [expiresAt, refetch]);
|
|
|
|
const minutes = Math.floor(secondsLeft / 60);
|
|
const seconds = secondsLeft % 60;
|
|
const countdownLabel = `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-[280px] w-[280px] items-center justify-center rounded-xl bg-white/10">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-white/20 border-t-white" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isError || !data?.token) {
|
|
return (
|
|
<div className="flex h-[280px] w-[280px] flex-col items-center justify-center gap-2 rounded-xl bg-white/10">
|
|
<p className="text-sm text-white/60">Kon QR-code niet laden</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => refetch()}
|
|
className="text-sm text-white underline"
|
|
>
|
|
Opnieuw proberen
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="rounded-xl bg-white p-3 shadow-lg">
|
|
<QrCodeCanvas token={data.token} />
|
|
</div>
|
|
<p className="text-sm text-white/60">
|
|
{secondsLeft > 0 ? (
|
|
<>
|
|
Vervalt over{" "}
|
|
<span className="text-white/80 tabular-nums">{countdownLabel}</span>
|
|
</>
|
|
) : (
|
|
<span className="text-yellow-400">Verlopen — vernieuwen...</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|