Closed
Description
What causes this issue?
My implementation:
"use client"
import React, { useEffect, useRef, useState } from "react"
import Quagga from '@ericblade/quagga2'
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { ScanLine } from "./scan-line"
import { QrCode, Camera, XCircle, CheckCircle2, Loader2, Volume2, VolumeX } from "lucide-react"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { toast } from "sonner"
interface Camera {
id: string;
label: string;
}
interface ScanResult {
code: string;
format: string;
}
interface BarcodeFormat {
value: string;
label: string;
}
interface BarcodeConstraints {
width?: number;
height?: number;
facingMode?: string;
deviceId?: string | { exact: string };
}
interface BarcodeScannerProps {
onDetected?: (result: any) => void;
onError?: (error: any) => void;
onScannerClose?: () => void;
readers?: string[];
fullScreen?: boolean;
constraints?: BarcodeConstraints;
}
// Supported barcode formats
const BARCODE_FORMATS: BarcodeFormat[] = [
{ value: "code_128_reader", label: "Code 128" },
{ value: "ean_reader", label: "EAN-13" },
{ value: "ean_8_reader", label: "EAN-8" },
{ value: "code_39_reader", label: "Code 39" },
{ value: "code_39_vin_reader", label: "Code 39 VIN" },
{ value: "codabar_reader", label: "Codabar" },
{ value: "upc_reader", label: "UPC" },
{ value: "upc_e_reader", label: "UPC-E" },
{ value: "i2of5_reader", label: "I2of5" },
{ value: "2of5_reader", label: "2of5" },
{ value: "code_93_reader", label: "Code 93" },
{ value: "code_32_reader", label: "Code 32" }
]
/**
* Barcode Scanner Component
*
* @param props Component props
* @returns JSX.Element
*/
const BarcodeScanner: React.FC<BarcodeScannerProps> = ({
onDetected,
onError,
onScannerClose,
readers = ["code_128_reader", "ean_reader"],
fullScreen = false,
constraints = {
width: 640,
height: 480,
facingMode: "environment"
}
}) => {
const scannerRef = useRef<HTMLDivElement>(null)
const [scanning, setScanning] = useState<boolean>(false)
const [initialized, setInitialized] = useState<boolean>(false)
const [error, setError] = useState<any>(null)
const [cameras, setCameras] = useState<Camera[]>([])
const [activeCamera, setActiveCamera] = useState<string | null>(null)
const [torchEnabled, setTorchEnabled] = useState<boolean>(false)
const [soundEnabled, setSoundEnabled] = useState<boolean>(true)
const [selectedReaders, setSelectedReaders] = useState<string[]>(readers)
const [lastResult, setLastResult] = useState<ScanResult | null>(null)
const [scanCount, setScanCount] = useState<number>(0)
const beepRef = useRef<HTMLAudioElement>(null)
const [initAttempts, setInitAttempts] = useState<number>(0)
const [errorHandled, setErrorHandled] = useState<boolean>(false)
useEffect(() => {
if (!scanning) return
if (!scanning || initAttempts > 3) return // max retry limit
const initScanner = async () => {
try {
// Get available cameras
const devices = await Quagga.CameraAccess.enumerateVideoDevices()
const formattedCameras = devices.map((device: MediaDeviceInfo) => ({
id: device.deviceId,
label: device.label || `Camera ${devices.indexOf(device) + 1}`
}))
setCameras(formattedCameras)
// If no active camera is set, use the first one
if (!activeCamera && formattedCameras.length > 0) {
setActiveCamera(formattedCameras[0].id)
}
const cameraConstraints: BarcodeConstraints = {
...constraints,
deviceId: activeCamera ? { exact: activeCamera } : undefined
}
setError(null)
setErrorHandled(false)
// Initialize Quagga
await Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: scannerRef.current as HTMLDivElement,
constraints: cameraConstraints,
area: { // Define a smaller area for detection to improve accuracy
top: "15%",
right: "10%",
left: "10%",
bottom: "15%"
}
},
locate: true,
locator: {
patchSize: "medium",
halfSample: true
},
numOfWorkers: navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4,
decoder: {
readers: selectedReaders,
debug: {
drawBoundingBox: true,
showFrequency: false,
drawScanline: true,
showPattern: false
}
},
frequency: 10
}, (err: any) => {
if (err) {
console.error("Quagga initialization failed:", err)
//!! IMPORTANT: Track that we've handled this error already to avoid duplicates
if (!errorHandled) {
setError(err)
setErrorHandled(true)
setInitAttempts(prev => prev + 1)
// Call onError only once per error
if (onError) onError(err)
}
return
}
setInitAttempts(0)
console.log("Quagga initialized successfully")
setInitialized(true)
Quagga.start()
})
Quagga.onDetected((result: any) => {
if (result && result.codeResult) {
const code = result.codeResult.code
const format = result.codeResult.format
// Play beep sound if enabled
if (soundEnabled && beepRef.current) {
beepRef.current.play().catch((err) => console.error("Error playing sound:", err))
}
// Show success animation
setLastResult({ code, format })
setScanCount(prevCount => prevCount + 1)
// Call onDetected callback with result
if (onDetected) onDetected(result)
}
})
// Set up processed handler for debugging
Quagga.onProcessed((result: any) => {
// TODO: visual feedback here...
})
} catch (err) {
console.error("Error initializing scanner:", err)
setError(err)
if (onError) onError(err)
}
}
initScanner()
return () => {
if (initialized) {
Quagga.offDetected()
Quagga.offProcessed()
Quagga.stop()
setInitialized(false)
}
}
}, [scanning, activeCamera, selectedReaders, onError, onDetected, constraints, initialized])
const toggleTorch = async () => {
try {
if (torchEnabled) {
await Quagga.CameraAccess.disableTorch()
setTorchEnabled(false)
toast.success("Torch disabled")
} else {
await Quagga.CameraAccess.enableTorch()
setTorchEnabled(true)
toast.success("Torch enabled")
}
} catch (err) {
console.error("Error toggling torch:", err)
toast.error("Torch functionality not available on this device")
}
}
// Change camera
const changeCamera = (deviceId: string) => {
if (initialized) {
Quagga.stop()
setInitialized(false)
}
setActiveCamera(deviceId)
// Restart scanner with new camera
if (!scanning) {
setScanning(true)
}
}
// Add/remove barcode readers
const toggleReader = (reader: string) => {
if (selectedReaders.includes(reader)) {
setSelectedReaders(selectedReaders.filter(r => r !== reader))
} else {
setSelectedReaders([...selectedReaders, reader])
}
// Restart scanner with new readers
if (initialized) {
Quagga.stop()
setInitialized(false)
}
}
const closeScanner = () => {
if (initialized) {
Quagga.stop()
setInitialized(false)
}
setScanning(false)
setError(null)
setErrorHandled(false)
setInitAttempts(0)
if (onScannerClose) onScannerClose()
}
const startScanning = () => {
setScanning(true)
setError(null)
setErrorHandled(false)
setInitAttempts(0)
}
return (
<div className={`barcode-scanner ${fullScreen ? "fixed inset-0 z-50 bg-black" : "relative"}`}>
{!scanning ? (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<QrCode className="h-5 w-5" />
Barcode Scanner
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground mb-4">
Scan barcodes to quickly find products, check inventory, or add items to your cart.
</p>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="soundEnabled">Sound notification</Label>
<Switch
id="soundEnabled"
checked={soundEnabled}
onCheckedChange={setSoundEnabled}
/>
</div>
<Select
value={selectedReaders[0]}
onValueChange={(value) => setSelectedReaders([value])}
>
<SelectTrigger>
<SelectValue placeholder="Select barcode format" />
</SelectTrigger>
<SelectContent>
{BARCODE_FORMATS.map(format => (
<SelectItem key={format.value} value={format.value}>
{format.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
<CardFooter>
<Button onClick={startScanning} className="w-full">
<Camera className="mr-2 h-4 w-4" />
Start Scanning
</Button>
</CardFooter>
</Card>
) : (
<div className="relative">
{/* Scanner viewport */}
<div
ref={scannerRef}
className={`overflow-hidden relative ${fullScreen ? "w-full h-screen" : "w-full max-w-md mx-auto aspect-[4/3] rounded-md"}`}
>
{/* Loading state */}
{!initialized && !error && (
<div className="absolute inset-0 bg-black/80 flex items-center justify-center z-20">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
<p className="text-white">Initializing camera...</p>
</div>
</div>
)}
{/* Error state */}
{error && (
<div className="absolute inset-0 bg-black/80 flex items-center justify-center z-20">
<div className="text-center p-4">
<XCircle className="h-8 w-8 text-destructive mx-auto mb-2" />
<p className="text-white mb-2">Failed to initialize camera</p>
<p className="text-sm text-white/70 mb-4">
{error.message || "Please ensure you've granted camera permissions"}
</p>
<Button variant="destructive" onClick={closeScanner}>
Close Scanner
</Button>
</div>
</div>
)}
{/* Success overlay */}
{lastResult && (
<div className="absolute inset-0 bg-black/30 flex items-center justify-center z-10 animate-fade-out">
<div className="bg-green-500/20 rounded-full p-8">
<CheckCircle2 className="h-12 w-12 text-green-500" />
</div>
</div>
)}
{/* Scan guides */}
<div className="absolute top-0 left-0 w-full h-full pointer-events-none z-10">
<div className="absolute top-[15%] right-[10%] bottom-[15%] left-[10%] border-2 border-white/40 rounded-lg">
<ScanLine />
</div>
</div>
</div>
<div className={`mt-4 ${fullScreen ? "absolute bottom-0 left-0 right-0 p-4 bg-black/80" : ""}`}>
<div className="flex items-center justify-between gap-2 mb-2">
<Button variant="outline" size="sm" onClick={closeScanner}>
<XCircle className="mr-2 h-4 w-4" />
Cancel
</Button>
<div className="flex gap-2">
{/* Camera selection */}
{cameras.length > 1 && (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<Camera className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-60">
<p className="font-medium text-sm mb-2">Select camera</p>
<div className="space-y-2">
{cameras.map(camera => (
<Button
key={camera.id}
variant={activeCamera === camera.id ? "default" : "outline"}
size="sm"
className="w-full justify-start"
onClick={() => changeCamera(camera.id)}
>
{camera.label}
</Button>
))}
</div>
</PopoverContent>
</Popover>
)}
<Button
variant={torchEnabled ? "default" : "outline"}
size="sm"
onClick={toggleTorch}
>
{torchEnabled ? "Torch On" : "Torch Off"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setSoundEnabled(!soundEnabled)}
>
{soundEnabled ? (
<Volume2 className="h-4 w-4" />
) : (
<VolumeX className="h-4 w-4" />
)}
</Button>
</div>
</div>
{initialized && (
<Alert>
<AlertTitle>Scanner active</AlertTitle>
<AlertDescription>
{scanCount > 0 ? (
<>Last scan: <span className="font-mono">{lastResult?.code}</span> ({lastResult?.format})</>
) : (
"Position barcode within the scanning area"
)}
</AlertDescription>
</Alert>
)}
</div>
<audio ref={beepRef} src="/beep.mp3" preload="auto" />
</div>
)}
</div>
)
}
export default BarcodeScanner
Metadata
Metadata
Assignees
Labels
No labels