2025-10-23 14:12:18 -06:00
|
|
|
"use client";
|
|
|
|
|
|
2025-11-12 17:05:47 -06:00
|
|
|
import { useEffect, useRef } from "react";
|
|
|
|
|
import toast from "react-hot-toast";
|
|
|
|
|
|
|
|
|
|
// 👇 Declaraciones globales para BarcodeDetector
|
|
|
|
|
declare global {
|
|
|
|
|
interface BarcodeDetectorOptions {
|
|
|
|
|
formats?: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DetectedBarcode {
|
|
|
|
|
rawValue: string;
|
|
|
|
|
format: string;
|
|
|
|
|
cornerPoints?: DOMPoint[];
|
|
|
|
|
boundingBox?: DOMRectReadOnly;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class BarcodeDetector {
|
|
|
|
|
constructor(options?: BarcodeDetectorOptions);
|
|
|
|
|
detect(source: ImageBitmapSource): Promise<DetectedBarcode[]>;
|
|
|
|
|
static getSupportedFormats(): Promise<string[]>;
|
|
|
|
|
}
|
2025-10-27 17:41:24 -06:00
|
|
|
}
|
2025-11-12 17:05:47 -06:00
|
|
|
export {};
|
2025-10-27 17:41:24 -06:00
|
|
|
|
|
|
|
|
interface BarcodeScannerProps {
|
|
|
|
|
onScan: (code: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
|
2025-11-12 17:05:47 -06:00
|
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
|
const lastScan = useRef<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// 🔹 Verifica soporte antes de montar el componente
|
|
|
|
|
if (typeof window !== "undefined" && !("BarcodeDetector" in window)) {
|
|
|
|
|
toast.error("BarcodeDetector API no soportada en este navegador");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-10-23 14:12:18 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-11-12 17:05:47 -06:00
|
|
|
let stream: MediaStream | null = null;
|
|
|
|
|
let detector: BarcodeDetector | null = null;
|
|
|
|
|
let animationFrame: number;
|
2025-10-27 17:41:24 -06:00
|
|
|
|
2025-11-12 17:05:47 -06:00
|
|
|
const startCamera = async () => {
|
|
|
|
|
try {
|
|
|
|
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
|
|
|
video: { facingMode: "environment" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!videoRef.current) return;
|
2025-10-23 14:12:18 -06:00
|
|
|
|
2025-11-12 17:05:47 -06:00
|
|
|
videoRef.current.srcObject = stream;
|
|
|
|
|
|
|
|
|
|
// Esperar a que el video esté listo antes de reproducirlo
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
videoRef.current!.onloadedmetadata = () => resolve(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await videoRef.current.play().catch((err) => {
|
|
|
|
|
console.warn("No se pudo reproducir automáticamente:", err);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!("BarcodeDetector" in window)) {
|
|
|
|
|
console.error("BarcodeDetector API no soportada en este navegador");
|
|
|
|
|
return;
|
2025-10-27 17:41:24 -06:00
|
|
|
}
|
2025-11-12 17:05:47 -06:00
|
|
|
|
|
|
|
|
detector = new BarcodeDetector({ formats: ["code_128", "ean_13"] });
|
|
|
|
|
|
|
|
|
|
const detect = async () => {
|
|
|
|
|
if (!videoRef.current || !detector) return;
|
|
|
|
|
try {
|
|
|
|
|
const barcodes = await detector.detect(videoRef.current);
|
|
|
|
|
if (barcodes.length > 0) {
|
|
|
|
|
const rawValue = barcodes[0].rawValue.trim();
|
|
|
|
|
if (rawValue && rawValue !== lastScan.current) {
|
|
|
|
|
lastScan.current = rawValue;
|
|
|
|
|
const processed = rawValue.startsWith("0")
|
|
|
|
|
? rawValue.slice(1)
|
|
|
|
|
: rawValue;
|
|
|
|
|
onScan(processed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
animationFrame = requestAnimationFrame(detect);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
detect();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Error al iniciar cámara:", err);
|
|
|
|
|
toast.error("No se pudo acceder a la cámara");
|
2025-10-27 17:41:24 -06:00
|
|
|
}
|
|
|
|
|
};
|
2025-10-23 14:12:18 -06:00
|
|
|
|
2025-11-12 17:05:47 -06:00
|
|
|
startCamera();
|
2025-10-27 17:41:24 -06:00
|
|
|
|
|
|
|
|
return () => {
|
2025-11-12 17:05:47 -06:00
|
|
|
if (animationFrame) cancelAnimationFrame(animationFrame);
|
|
|
|
|
if (stream) stream.getTracks().forEach((t) => t.stop());
|
2025-10-27 17:41:24 -06:00
|
|
|
};
|
|
|
|
|
}, [onScan]);
|
2025-10-23 14:12:18 -06:00
|
|
|
|
|
|
|
|
return (
|
2025-11-12 17:05:47 -06:00
|
|
|
<video
|
|
|
|
|
ref={videoRef}
|
2025-10-23 14:12:18 -06:00
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
2025-11-12 17:05:47 -06:00
|
|
|
maxWidth: "480px",
|
2025-10-27 17:41:24 -06:00
|
|
|
height: "300px",
|
|
|
|
|
border: "2px solid #0056b3",
|
2025-11-12 17:05:47 -06:00
|
|
|
borderRadius: "12px",
|
|
|
|
|
objectFit: "cover",
|
2025-10-23 14:12:18 -06:00
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-10-27 18:17:53 -06:00
|
|
|
}
|