'use client'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { Html5Qrcode } from 'html5-qrcode'; import { X, Camera, AlertCircle, ZoomIn, ZoomOut } from 'lucide-react'; interface BarcodeScannerProps { onScan: (value: string) => void; onClose: () => void; } export default function BarcodeScanner({ onScan, onClose }: BarcodeScannerProps) { const containerRef = useRef(null); const [error, setError] = useState(''); const [zoom, setZoom] = useState(1); const [zoomMax, setZoomMax] = useState(3); const [supportsZoom, setSupportsZoom] = useState(false); const initializedRef = useRef(false); const scannerRef = useRef(null); const trackRef = useRef(null); const applyZoom = useCallback(async (level: number, video?: HTMLVideoElement | null) => { const v = video || (document.querySelector('#sr-barcode-scanner video') as HTMLVideoElement | null); if (v && v.srcObject) { if (trackRef.current && supportsZoom) { try { await trackRef.current.applyConstraints({ advanced: [{ zoom: level }] as any }); } catch { } } (v as HTMLElement).style.transform = `scale(${level})`; } }, [supportsZoom]); useEffect(() => { // StrictMode guard: skip second effect run if (initializedRef.current) return; initializedRef.current = true; const el = containerRef.current; if (!el) return; const id = 'sr-barcode-scanner'; el.id = id; el.innerHTML = ''; let cancelled = false; const s = new Html5Qrcode(id); scannerRef.current = s; const startWithFacing = async (facingMode: 'environment' | 'user') => { await s.start( { facingMode }, { fps: 30 }, (decodedText) => { if (cancelled) return; try { s.stop(); } catch { } onScan(decodedText); }, () => {} ); // Set up zoom after camera starts setTimeout(() => { if (cancelled) return; const video = el.querySelector('video'); if (video && video.srcObject) { (video as HTMLElement).style.transition = 'transform 0.2s'; (video as HTMLElement).style.transformOrigin = 'center center'; const track = (video.srcObject as MediaStream).getVideoTracks()[0]; if (track) { trackRef.current = track; const caps = track.getCapabilities() as any; if (caps?.zoom) { setSupportsZoom(true); setZoomMax(caps.zoom.max || 3); } } } }, 300); }; startWithFacing('environment').catch(() => { return startWithFacing('user'); }).catch((err: unknown) => { if (cancelled) return; const msg = err instanceof Error ? err.message : String(err); const isIOS = navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad'); if (msg.includes('NotAllowed') || msg.includes('Permission')) { setError(isIOS ? 'Camera denied. Settings → Safari → Camera → Allow.' : 'Camera permission denied.'); } else if (msg.includes('NotFound')) { setError('No camera found on this device.'); } else { setError(msg); } }); return () => { cancelled = true; try { s.stop(); } catch { } }; }, []); const handleZoomIn = () => { const newZoom = Math.min(zoom + 0.5, zoomMax); setZoom(newZoom); applyZoom(newZoom); }; const handleZoomOut = () => { const newZoom = Math.max(zoom - 0.5, 0.5); setZoom(newZoom); applyZoom(newZoom); }; return (
e.stopPropagation()}>
Scan Barcode
{error ? (

{error}

) : ( <>
{zoom.toFixed(1)}x

Position the barcode or QR code within the scanning area

)}
); }