index.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { X } from 'lucide-react'
3 import QrScanner from 'qr-scanner'
4 import { useCallback, useEffect, useRef, useState } from 'react'
5 import { useTranslation } from 'react-i18next'
6
7 export default function QrScannerModal({
8 onScan,
9 onClose
10 }: {
11 onScan: (result: string) => void
12 onClose: () => void
13 }) {
14 const { t } = useTranslation()
15 const videoRef = useRef<HTMLVideoElement>(null)
16 const scannerRef = useRef<QrScanner | null>(null)
17 const [error, setError] = useState<string | null>(null)
18
19 const handleScan = useCallback(
20 (result: QrScanner.ScanResult) => {
21 onScan(result.data)
22 onClose()
23 },
24 [onScan, onClose]
25 )
26
27 useEffect(() => {
28 if (!videoRef.current) return
29
30 const scanner = new QrScanner(videoRef.current, handleScan, {
31 preferredCamera: 'environment',
32 highlightScanRegion: true,
33 highlightCodeOutline: true
34 })
35
36 scannerRef.current = scanner
37
38 scanner.start().catch(() => {
39 setError(t('Failed to access camera'))
40 })
41
42 return () => {
43 scanner.destroy()
44 }
45 }, [handleScan, t])
46
47 return (
48 <div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center">
49 <div className="relative w-full max-w-sm mx-4">
50 <Button
51 variant="ghost"
52 size="icon"
53 className="absolute -top-12 right-0 text-white hover:bg-white/20"
54 onClick={onClose}
55 >
56 <X className="h-6 w-6" />
57 </Button>
58 <div className="rounded-lg overflow-hidden bg-black">
59 {error ? (
60 <div className="p-8 text-center text-destructive">{error}</div>
61 ) : (
62 <video ref={videoRef} className="w-full" />
63 )}
64 </div>
65 <p className="text-center text-white/70 text-sm mt-4">
66 {t('Point camera at QR code')}
67 </p>
68 </div>
69 </div>
70 )
71 }
72