// checkin.jsx — Gamification check-in: QR scanner, celebrate, leaderboard, badges. Global. function parseCheckinToken(text) { if (!text) return null; const m = text.match(/checkin\/([0-9a-fA-F-]{36})/) ; if (m) return m[1]; const u = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); return u ? u[0] : null; } function getGeo(timeout = 6000) { return new Promise((resolve) => { if (!navigator.geolocation) return resolve(null); navigator.geolocation.getCurrentPosition( p => resolve({ lat: p.coords.latitude, lng: p.coords.longitude }), () => resolve(null), { enableHighAccuracy: true, timeout: timeout, maximumAge: 30000 } ); }); } function loadJsQR() { return new Promise((resolve) => { if (window.jsQR) return resolve(window.jsQR); const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js'; s.onload = () => resolve(window.jsQR || null); s.onerror = () => resolve(null); document.body.appendChild(s); }); } // ---------- QR scanner (camera) ---------- function CheckinScanner({ onClose, onToken }) { const videoRef = React.useRef(null); const canvasRef = React.useRef(null); const streamRef = React.useRef(null); const rafRef = React.useRef(0); const doneRef = React.useRef(false); const [err, setErr] = React.useState(''); React.useEffect(() => { let detector = null; let jsqr = null; let cancelled = false; async function start() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } }, audio: false }); if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; } streamRef.current = stream; const v = videoRef.current; if (!v) return; v.srcObject = stream; v.setAttribute('playsinline', 'true'); v.muted = true; await v.play().catch(() => {}); if ('BarcodeDetector' in window) { try { detector = new window.BarcodeDetector({ formats: ['qr_code'] }); } catch (e) { detector = null; } } if (!detector) jsqr = await loadJsQR(); scan(); } catch (e) { setErr('Tidak bisa akses kamera. Izinkan kamera atau tempel link QR di aplikasi kamera HP.'); } } async function scan() { if (cancelled || doneRef.current) return; const v = videoRef.current; if (v && v.readyState === 4) { try { if (detector) { const codes = await detector.detect(v); if (codes && codes.length) return hit(codes[0].rawValue); } else if (jsqr) { const cv = canvasRef.current; cv.width = v.videoWidth; cv.height = v.videoHeight; const ctx = cv.getContext('2d'); ctx.drawImage(v, 0, 0, cv.width, cv.height); const img = ctx.getImageData(0, 0, cv.width, cv.height); const res = jsqr(img.data, cv.width, cv.height, { inversionAttempts: 'dontInvert' }); if (res && res.data) return hit(res.data); } } catch (e) {} } rafRef.current = requestAnimationFrame(scan); } function hit(raw) { const tok = parseCheckinToken(raw); if (!tok) { rafRef.current = requestAnimationFrame(scan); return; } doneRef.current = true; stop(); onToken(tok); } function stop() { cancelAnimationFrame(rafRef.current); if (streamRef.current) { streamRef.current.getTracks().forEach(t => t.stop()); streamRef.current = null; } } start(); return () => { cancelled = true; stop(); }; }, []); return (