Add complete NexaPantry application structure including: - Docker Compose configuration with PostgreSQL, Redis, FastAPI backend, worker, frontend and Caddy - Environment configuration template with database, auth, and service settings - GitHub Actions CI workflow for backend/frontend linting, testing, auditing and Docker builds - AGPL-3.0 license and comprehensive README with setup, development, and security documentation - Backend
43 lines
1.8 KiB
TypeScript
43 lines
1.8 KiB
TypeScript
import { BrowserMultiFormatReader } from '@zxing/browser';
|
|
import { ScanLine } from 'lucide-react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { api } from '../api/client';
|
|
import { Panel } from '../components/Forms';
|
|
import { ProductForm } from '../components/ProductForm';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { useI18n } from '../contexts/I18nContext';
|
|
import type { Product } from '../types';
|
|
|
|
export function ScannerPage() {
|
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
const { activeHome } = useAuth();
|
|
const { t } = useI18n();
|
|
const [barcode, setBarcode] = useState('');
|
|
const [prefill, setPrefill] = useState<Partial<Product>>({});
|
|
|
|
useEffect(() => {
|
|
if (!activeHome || !videoRef.current) return;
|
|
const reader = new BrowserMultiFormatReader();
|
|
let controls: { stop: () => void } | undefined;
|
|
let stop = false;
|
|
reader.decodeFromVideoDevice(undefined, videoRef.current, async (result) => {
|
|
if (result && !stop) {
|
|
stop = true;
|
|
const code = result.getText();
|
|
setBarcode(code);
|
|
const lookup = await api<{ found: boolean; product?: Partial<Product> }>(`/homes/${activeHome.id}/products/lookup/${code}`);
|
|
setPrefill(lookup.product ?? { barcode: code });
|
|
}
|
|
}).then((scannerControls) => { controls = scannerControls; }).catch(() => undefined);
|
|
return () => { stop = true; controls?.stop(); };
|
|
}, [activeHome]);
|
|
|
|
return (
|
|
<div className="grid gap-4">
|
|
<h1 className="flex items-center gap-2 text-3xl font-bold"><ScanLine /> {t('scanBarcode')}</h1>
|
|
<video ref={videoRef} className="aspect-video w-full rounded-lg bg-gray-950 object-cover" muted playsInline />
|
|
{barcode ? <Panel title={`${t('barcode')}: ${barcode}`}><ProductForm initial={prefill} onSaved={() => setPrefill({})} /></Panel> : null}
|
|
</div>
|
|
);
|
|
}
|