/** * Check-in station — scan QR code or search bidder by name / paddle number. * Uses the browser BarcodeDetector API for camera scanning (Chrome/Edge mobile). * Falls back to manual search if BarcodeDetector is unavailable. */ import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "../../lib/api.js"; interface BidderEnrollment { id: string; bidderId: string; paddleNumber: number | null; checkInStatus: string; checkInAt: string | null; bidder: { id: string; firstName: string; lastName: string; email: string | null; phone: string | null; }; } declare global { interface Window { BarcodeDetector?: new (opts: { formats: string[] }) => { detect: (source: HTMLVideoElement) => Promise<{ rawValue: string }[]>; }; } } type CheckInResult = { enrollment: BidderEnrollment; alreadyCheckedIn: boolean } | null; export default function CheckInPage() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [checkInResult, setCheckInResult] = useState(null); const [scanMode, setScanMode] = useState(false); const [scanError, setScanError] = useState(null); const [eventId, setEventId] = useState(null); const videoRef = useRef(null); const scanLoopRef = useRef(null); // Fetch active event for this staff session useEffect(() => { api .get<{ id: string; status: string }[]>("/api/events") .then((events) => { const active = events.find((e) => e.status === "active") ?? events[0]; if (active) setEventId(active.id); }) .catch(console.error); }, []); // Debounced bidder search useEffect(() => { if (!eventId || query.trim().length < 2) { setResults([]); return; } setSearching(true); const timer = setTimeout(() => { api .get(`/api/bidders?eventId=${eventId}&q=${encodeURIComponent(query)}`) .then(setResults) .catch(console.error) .finally(() => setSearching(false)); }, 300); return () => clearTimeout(timer); }, [query, eventId]); const doCheckIn = useCallback( async (enrollmentId: string) => { try { const result = await api.post<{ enrollment: BidderEnrollment; alreadyCheckedIn: boolean }>( `/api/check-in/${enrollmentId}`, {}, ); setCheckInResult(result); setResults([]); setQuery(""); } catch (err) { console.error(err); } }, [], ); const doScanCheckIn = useCallback( async (bidderId: string, eid: string) => { try { const result = await api.post<{ enrollment: BidderEnrollment; alreadyCheckedIn: boolean }>( "/api/check-in/scan", { bidderId, eventId: eid }, ); setCheckInResult(result); } catch { setScanError("Bidder not found for this event"); } }, [], ); // Camera QR scanning loop const startScan = useCallback(async () => { if (!window.BarcodeDetector) { setScanError("Camera QR scanning not supported in this browser. Use manual search."); return; } setScanMode(true); setScanError(null); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, }); if (videoRef.current) { videoRef.current.srcObject = stream; await videoRef.current.play(); } const detector = new window.BarcodeDetector!({ formats: ["qr_code"] }); const scan = async () => { if (!videoRef.current) return; try { const barcodes = await detector.detect(videoRef.current); if (barcodes.length > 0) { const raw = barcodes[0]!.rawValue; // Parse /check-in?b=&e= try { const url = new URL(raw); const b = url.searchParams.get("b"); const e = url.searchParams.get("e"); if (b && e) { stopScan(stream); await doScanCheckIn(b, e); return; } } catch { // Not a valid URL — ignore } } } catch { // Detector error on a frame — continue } scanLoopRef.current = requestAnimationFrame(scan); }; scanLoopRef.current = requestAnimationFrame(scan); } catch { setScanError("Camera access denied. Use manual search instead."); setScanMode(false); } }, [doScanCheckIn]); const stopScan = (stream?: MediaStream) => { if (scanLoopRef.current) cancelAnimationFrame(scanLoopRef.current); if (videoRef.current?.srcObject) { (videoRef.current.srcObject as MediaStream).getTracks().forEach((t) => t.stop()); videoRef.current.srcObject = null; } if (stream) stream.getTracks().forEach((t) => t.stop()); setScanMode(false); }; return (
{/* Header */}

Check-In

Scan QR or search by name / paddle

{scanMode ? ( ) : ( )}
{/* Camera viewfinder */} {scanMode && (
); }