Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { motion } from "framer-motion"; import { Camera, CameraOff, Copy, ExternalLink, Flashlight, RefreshCcw, ScanLine, ShieldCheck, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; const BARCODE_FORMATS = [ "qr_code", "ean_13", "ean_8", "upc_a", "upc_e", "code_128", "code_39", "code_93", "codabar", "itf", "data_matrix", "aztec", "pdf417", ]; function isLikelyUrl(value) { try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } } function normalizeError(error) { if (!error) return "Something went wrong."; if (error.name === "NotAllowedError") return "Camera permission was denied. Allow camera access and try again."; if (error.name === "NotFoundError") return "No camera was found on this device."; if (error.name === "NotReadableError") return "The camera is already being used by another app or browser tab."; if (error.name === "OverconstrainedError") return "This camera does not support the requested settings."; return error.message || String(error); } export default function LocalPhoneBarcodeScanner() { const videoRef = useRef(null); const streamRef = useRef(null); const detectorRef = useRef(null); const animationFrameRef = useRef(null); const scanLockRef = useRef(false); const [isSupported, setIsSupported] = useState(true); const [isScanning, setIsScanning] = useState(false); const [status, setStatus] = useState("Ready to scan."); const [error, setError] = useState(""); const [result, setResult] = useState(null); const [history, setHistory] = useState([]); const [facingMode, setFacingMode] = useState("environment"); const [torchOn, setTorchOn] = useState(false); const [torchAvailable, setTorchAvailable] = useState(false); const resultIsUrl = useMemo(() => result?.rawValue && isLikelyUrl(result.rawValue), [result]); const stopCamera = useCallback(() => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); animationFrameRef.current = null; } const stream = streamRef.current; if (stream) { stream.getTracks().forEach((track) => track.stop()); streamRef.current = null; } if (videoRef.current) { videoRef.current.srcObject = null; } scanLockRef.current = false; setIsScanning(false); setTorchOn(false); setTorchAvailable(false); setStatus("Scanner stopped."); }, []); const saveResult = useCallback((barcode) => { const next = { rawValue: barcode.rawValue || "", format: barcode.format || "unknown", scannedAt: new Date().toLocaleString(), }; setResult(next); setHistory((items) => { const withoutDuplicate = items.filter((item) => item.rawValue !== next.rawValue); return [next, ...withoutDuplicate].slice(0, 8); }); }, []); const scanLoop = useCallback(async () => { const video = videoRef.current; const detector = detectorRef.current; if (!video || !detector || video.readyState < 2) { animationFrameRef.current = requestAnimationFrame(scanLoop); return; } if (!scanLockRef.current) { scanLockRef.current = true; try { const barcodes = await detector.detect(video); if (barcodes.length > 0) { saveResult(barcodes[0]); setStatus("Barcode found. Keep scanning or copy the result."); if (navigator.vibrate) navigator.vibrate(80); } } catch (err) { setError(normalizeError(err)); } finally { setTimeout(() => { scanLockRef.current = false; }, 350); } } animationFrameRef.current = requestAnimationFrame(scanLoop); }, [saveResult]); const startCamera = useCallback(async () => { setError(""); setResult(null); setStatus("Starting camera…"); if (!("BarcodeDetector" in window)) { setIsSupported(false); setError("This browser does not expose the built-in BarcodeDetector API. Try Chrome or Edge on Android, or add a JavaScript decoder fallback such as ZXing for broader browser support."); setStatus("Barcode detection is unavailable in this browser."); return; } try { stopCamera(); detectorRef.current = new window.BarcodeDetector({ formats: BARCODE_FORMATS }); const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: { ideal: facingMode }, width: { ideal: 1280 }, height: { ideal: 720 }, }, }); streamRef.current = stream; const [track] = stream.getVideoTracks(); const capabilities = track?.getCapabilities?.() || {}; setTorchAvailable(Boolean(capabilities.torch)); const video = videoRef.current; if (!video) return; video.srcObject = stream; video.setAttribute("playsinline", "true"); video.muted = true; await video.play(); setIsSupported(true); setIsScanning(true); setStatus("Point the camera at a barcode or QR code."); animationFrameRef.current = requestAnimationFrame(scanLoop); } catch (err) { setError(normalizeError(err)); setStatus("Could not start camera."); stopCamera(); } }, [facingMode, scanLoop, stopCamera]); const switchCamera = useCallback(async () => { setFacingMode((mode) => (mode === "environment" ? "user" : "environment")); }, []); const toggleTorch = useCallback(async () => { const stream = streamRef.current; const track = stream?.getVideoTracks?.()[0]; if (!track) return; try { await track.applyConstraints({ advanced: [{ torch: !torchOn }] }); setTorchOn((value) => !value); } catch (err) { setError("Torch control is not available on this camera/browser."); setTorchAvailable(false); } }, [torchOn]); const copyResult = useCallback(async (value) => { try { await navigator.clipboard.writeText(value); setStatus("Copied to clipboard."); } catch { setStatus("Copy failed. Select the text manually."); } }, []); useEffect(() => { if (isScanning) startCamera(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [facingMode]); useEffect(() => { return () => stopCamera(); }, [stopCamera]); return ( <main className="min-h-screen bg-slate-950 px-4 py-5 text-white sm:px-6 lg:px-8"> <div className="mx-auto flex w-full max-w-3xl flex-col gap-4"> <motion.header initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="flex flex-col gap-3 rounded-3xl border border-white/10 bg-white/5 p-5 shadow-2xl shadow-black/30 backdrop-blur" > <div className="flex items-center justify-between gap-3"> <div> <p className="mb-1 flex items-center gap-2 text-sm font-medium text-emerald-300"> <ShieldCheck className="h-4 w-4" /> Local-only camera scanner </p> <h1 className="text-2xl font-bold tracking-tight sm:text-3xl">Phone Barcode Scanner</h1> </div> <div className="rounded-2xl bg-emerald-400/10 p-3 text-emerald-300"> <ScanLine className="h-7 w-7" /> </div> </div> <p className="text-sm leading-6 text-slate-300"> Runs in the browser using the device camera. No images or scan data are uploaded. For local phone testing, serve this app over HTTPS or from localhost; most mobile browsers block camera access on plain HTTP. </p> </motion.header> <Card className="overflow-hidden rounded-3xl border-white/10 bg-white/5 shadow-2xl shadow-black/30"> <CardContent className="p-0"> <div className="relative aspect-[3/4] w-full overflow-hidden bg-black sm:aspect-video"> <video ref={videoRef} className="h-full w-full object-cover" playsInline muted /> {!isScanning && ( <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/70 p-6 text-center"> <Camera className="h-12 w-12 text-slate-300" /> <p className="max-w-sm text-sm text-slate-300"> Tap Start Scanner and allow camera access. Use the rear camera for best results on a phone. </p> </div> )} {isScanning && ( <div className="pointer-events-none absolute inset-0 flex items-center justify-center p-8"> <div className="relative h-52 w-full max-w-md rounded-3xl border-2 border-emerald-300/80 shadow-[0_0_0_999px_rgba(0,0,0,0.35)]"> <motion.div className="absolute left-4 right-4 top-6 h-0.5 rounded-full bg-emerald-300 shadow-lg shadow-emerald-300/70" animate={{ y: [0, 150, 0] }} transition={{ duration: 2.2, repeat: Infinity, ease: "easeInOut" }} /> <div className="absolute -left-1 -top-1 h-8 w-8 rounded-tl-3xl border-l-4 border-t-4 border-white" /> <div className="absolute -right-1 -top-1 h-8 w-8 rounded-tr-3xl border-r-4 border-t-4 border-white" /> <div className="absolute -bottom-1 -left-1 h-8 w-8 rounded-bl-3xl border-b-4 border-l-4 border-white" /> <div className="absolute -bottom-1 -right-1 h-8 w-8 rounded-br-3xl border-b-4 border-r-4 border-white" /> </div> </div> )} </div> <div className="flex flex-col gap-3 border-t border-white/10 p-4"> <div className="flex flex-wrap gap-2"> {!isScanning ? ( <Button onClick={startCamera} className="flex-1 rounded-2xl bg-emerald-400 px-4 py-6 text-base font-semibold text-slate-950 hover:bg-emerald-300"> <Camera className="mr-2 h-5 w-5" /> Start Scanner </Button> ) : ( <Button onClick={stopCamera} variant="secondary" className="flex-1 rounded-2xl px-4 py-6 text-base font-semibold"> <CameraOff className="mr-2 h-5 w-5" /> Stop </Button> )} <Button onClick={switchCamera} variant="secondary" className="rounded-2xl px-4 py-6" title="Switch camera"> <RefreshCcw className="h-5 w-5" /> </Button> <Button onClick={toggleTorch} disabled={!torchAvailable || !isScanning} variant="secondary" className="rounded-2xl px-4 py-6" title="Toggle flashlight" > <Flashlight className="h-5 w-5" /> </Button> </div> <div className="rounded-2xl border border-white/10 bg-slate-900/70 p-3 text-sm text-slate-300"> <span className="font-medium text-white">Status:</span> {status} </div> {!isSupported && ( <div className="rounded-2xl border border-amber-300/30 bg-amber-300/10 p-3 text-sm text-amber-100"> Built-in barcode detection is not available in this browser. The camera UI is ready, but you will need a fallback decoder library for full cross-browser support. </div> )} {error && ( <div className="flex items-start gap-2 rounded-2xl border border-red-400/30 bg-red-500/10 p-3 text-sm text-red-100"> <X className="mt-0.5 h-4 w-4 shrink-0" /> <span>{error}</span> </div> )} </div> </CardContent> </Card> {result && ( <motion.section initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}> <Card className="rounded-3xl border-emerald-300/30 bg-emerald-300/10 shadow-xl shadow-black/20"> <CardContent className="flex flex-col gap-3 p-4"> <div className="flex items-center justify-between gap-3"> <div> <p className="text-sm font-medium text-emerald-200">Latest scan</p> <p className="text-xs uppercase tracking-wide text-slate-400">{result.format}</p> </div> <Button onClick={() => copyResult(result.rawValue)} size="sm" className="rounded-xl bg-white text-slate-950 hover:bg-slate-200"> <Copy className="mr-2 h-4 w-4" /> Copy </Button> </div> <div className="break-all rounded-2xl bg-black/35 p-3 font-mono text-sm text-white">{result.rawValue}</div> {resultIsUrl && ( <a href={result.rawValue} target="_blank" rel="noreferrer" className="inline-flex items-center justify-center rounded-2xl bg-emerald-400 px-4 py-3 text-sm font-semibold text-slate-950 hover:bg-emerald-300" > Open link <ExternalLink className="ml-2 h-4 w-4" /> </a> )} </CardContent> </Card> </motion.section> )} {history.length > 0 && ( <section className="rounded-3xl border border-white/10 bg-white/5 p-4"> <h2 className="mb-3 text-lg font-semibold">Recent scans</h2> <div className="flex flex-col gap-2"> {history.map((item) => ( <button key={`${item.rawValue}-${item.scannedAt}`} onClick={() => setResult(item)} className="rounded-2xl border border-white/10 bg-slate-900/70 p-3 text-left transition hover:bg-slate-800" > <div className="mb-1 flex items-center justify-between gap-2 text-xs text-slate-400"> <span className="uppercase tracking-wide">{item.format}</span> <span>{item.scannedAt}</span> </div> <div className="line-clamp-2 break-all font-mono text-sm text-slate-100">{item.rawValue}</div> </button> ))} </div> </section> )} <footer className="pb-8 text-center text-xs leading-5 text-slate-500"> Tip: barcodes scan best with bright lighting, a steady hand, and the code centered inside the frame. </footer> </div> </main> ); }
No revisions found. Save the file to create a backup.
Delete
Update App