feat: implement QR code check-in system for event attendees
- Add UserQRCode component to display unique QR code on user profile - Add QRCodeScanner component for mobile camera-based scanning - Create EventCheckInPage for organizers to scan attendee QR codes - Add backend endpoint POST /api/events/:id/checkin for check-in - Add check-in button to EventDetailModal for organizers - Install qrcode.react and html5-qrcode dependencies - Implement real-time attendee list updates - Add organizer permission validation - Prevent duplicate check-ins - Mobile-optimized with back camera support
This commit is contained in:
117
src/components/event/QRCodeScanner.tsx
Normal file
117
src/components/event/QRCodeScanner.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
|
||||
interface QRCodeScannerProps {
|
||||
onScan: (userId: string) => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export function QRCodeScanner({ onScan, onError }: QRCodeScannerProps) {
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const scannerRef = useRef<Html5Qrcode | null>(null);
|
||||
const scannerIdRef = useRef('qr-reader');
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Cleanup on unmount
|
||||
if (scannerRef.current) {
|
||||
scannerRef.current.stop().catch(console.error);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const startScanning = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
const scanner = new Html5Qrcode(scannerIdRef.current);
|
||||
scannerRef.current = scanner;
|
||||
|
||||
await scanner.start(
|
||||
{ facingMode: 'environment' }, // Use back camera
|
||||
{
|
||||
fps: 10,
|
||||
qrbox: { width: 250, height: 250 },
|
||||
},
|
||||
(decodedText) => {
|
||||
try {
|
||||
const data = JSON.parse(decodedText);
|
||||
if (data.type === 'attendee-checkin' && data.userId) {
|
||||
onScan(data.userId);
|
||||
} else {
|
||||
const errorMsg = 'Invalid QR code format';
|
||||
setError(errorMsg);
|
||||
onError?.(errorMsg);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = 'Failed to parse QR code';
|
||||
setError(errorMsg);
|
||||
onError?.(errorMsg);
|
||||
}
|
||||
},
|
||||
(errorMessage) => {
|
||||
// Ignore scanning errors (happens continuously while scanning)
|
||||
}
|
||||
);
|
||||
|
||||
setIsScanning(true);
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.message || 'Failed to start camera';
|
||||
setError(errorMsg);
|
||||
onError?.(errorMsg);
|
||||
setIsScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopScanning = async () => {
|
||||
if (scannerRef.current) {
|
||||
try {
|
||||
await scannerRef.current.stop();
|
||||
scannerRef.current = null;
|
||||
setIsScanning(false);
|
||||
} catch (err) {
|
||||
console.error('Error stopping scanner:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
id={scannerIdRef.current}
|
||||
className="w-full rounded-lg overflow-hidden bg-black"
|
||||
style={{ minHeight: isScanning ? '300px' : '0' }}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
{!isScanning ? (
|
||||
<button
|
||||
onClick={startScanning}
|
||||
className="flex-1 bg-blue-600 text-white px-4 py-3 rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
>
|
||||
Start Scanning
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={stopScanning}
|
||||
className="flex-1 bg-red-600 text-white px-4 py-3 rounded-lg hover:bg-red-700 transition-colors font-medium"
|
||||
>
|
||||
Stop Scanning
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isScanning && (
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
Position the QR code within the frame to scan
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user