sozsoft-platform/ui/src/components/shared/BarcodeScanner/BarcodeScannerDialog.tsx

112 lines
3.5 KiB
TypeScript
Raw Normal View History

2026-09-21 15:01:40 +00:00
import { useState } from 'react'
import { Button, Dialog } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useBarcodeReader, type BarcodeReaderStatus } from './useBarcodeReader'
export type BarcodeScannerDialogProps = {
open: boolean
onDialogClose: () => void
/**
* Okunan kod. `false` dönerse kod reddedilmiş sayılır ve listeye yazılmaz;
* sürekli modda yazma hatasını kullanıcıya göstermek için kullanılır.
*/
onDetect: (code: string) => boolean | void | Promise<boolean | void>
/** Açıkken diyalog okumadan sonra kapanmaz, okunanları listeler. */
continuous?: boolean
title?: string
}
const statusMessageKeys: Record<BarcodeReaderStatus, string> = {
starting: '::App.BarcodeScanner.Starting',
scanning: '::App.BarcodeScanner.Aim',
denied: '::App.BarcodeScanner.PermissionDenied',
insecure: '::App.BarcodeScanner.InsecureContext',
standaloneIos: '::App.BarcodeScanner.StandaloneIos',
error: '::App.BarcodeScanner.CameraError',
}
/**
* Kamerayla barkod/QR okuma diyaloğu. Tek okuma ve sürekli tarama aynı
* bileşendir; fark yalnızca okumadan sonra kapanıp kapanmadığıdır.
*/
const BarcodeScannerDialog = ({
open,
onDialogClose,
onDetect,
continuous = false,
title,
}: BarcodeScannerDialogProps) => {
const { translate } = useLocalization()
const [scanned, setScanned] = useState<string[]>([])
const [failed, setFailed] = useState<string>()
const handleDetect = async (code: string) => {
const accepted = await onDetect(code)
if (accepted === false) {
setFailed(code)
return
}
if (!continuous) {
onDialogClose()
return
}
setFailed(undefined)
setScanned((previous) => [code, ...previous])
}
const { videoRef, status } = useBarcodeReader({ active: open, onDetect: handleDetect })
return (
<Dialog isOpen={open} width={520} onClose={onDialogClose} onRequestClose={onDialogClose}>
<h5 className="mb-4">{title ?? translate('::App.BarcodeScanner.Title')}</h5>
<div className="relative overflow-hidden rounded-lg bg-black">
<video
ref={videoRef}
autoPlay
muted
playsInline
className="h-64 w-full object-cover sm:h-80"
/>
{status === 'scanning' && (
// Nişangah: kullanıcıya kodu nereye tutacağını gösterir.
<div className="pointer-events-none absolute inset-x-10 inset-y-16 rounded-lg border-2 border-white/80" />
)}
</div>
<p className="mt-3 text-sm opacity-80">{translate(statusMessageKeys[status])}</p>
{failed && (
<p className="mt-1 text-sm text-red-500">
{translate('::App.BarcodeScanner.SaveFailed')} ({failed})
</p>
)}
{continuous && scanned.length > 0 && (
<div className="mt-3">
<p className="mb-1 text-sm font-semibold">
{translate('::App.BarcodeScanner.ScannedCount')}: {scanned.length}
</p>
<ul className="max-h-32 overflow-auto rounded border text-sm">
{scanned.map((code, index) => (
<li key={`${code}-${index}`} className="border-b px-2 py-1 last:border-b-0">
{code}
</li>
))}
</ul>
</div>
)}
<div className="mt-5 text-right">
<Button variant="solid" onClick={onDialogClose}>
{translate(continuous ? '::App.Platform.Close' : '::App.Platform.Cancel')}
</Button>
</div>
</Dialog>
)
}
export default BarcodeScannerDialog