151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useCallback, useRef } from 'react';
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
import { uploadOCRScan } from '@/lib/ocr';
|
||
|
|
|
||
|
|
interface OCRUploadProps {
|
||
|
|
vehicleId?: string;
|
||
|
|
onUploadComplete?: (resultId: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function OCRUpload({ vehicleId, onUploadComplete }: OCRUploadProps) {
|
||
|
|
const [isDragging, setIsDragging] = useState(false);
|
||
|
|
const [uploading, setUploading] = useState(false);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
const [success, setSuccess] = useState<string | null>(null);
|
||
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
|
|
||
|
|
const handleFile = useCallback(
|
||
|
|
async (file: File) => {
|
||
|
|
setError(null);
|
||
|
|
setSuccess(null);
|
||
|
|
|
||
|
|
if (!file.type.startsWith('image/')) {
|
||
|
|
setError('Only image files are allowed');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setUploading(true);
|
||
|
|
try {
|
||
|
|
const result = await uploadOCRScan(file, vehicleId);
|
||
|
|
setSuccess(`Upload queued. Result ID: ${result.ocr_result_id}`);
|
||
|
|
if (onUploadComplete) {
|
||
|
|
onUploadComplete(result.ocr_result_id);
|
||
|
|
}
|
||
|
|
} catch (err: unknown) {
|
||
|
|
const apiErr = err as { error?: { message?: string } };
|
||
|
|
setError(apiErr?.error?.message || 'Upload failed');
|
||
|
|
} finally {
|
||
|
|
setUploading(false);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[vehicleId, onUploadComplete]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
setIsDragging(true);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
setIsDragging(false);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleDrop = useCallback(
|
||
|
|
(e: React.DragEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
setIsDragging(false);
|
||
|
|
|
||
|
|
const files = e.dataTransfer.files;
|
||
|
|
if (files && files.length > 0) {
|
||
|
|
handleFile(files[0]);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[handleFile]
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleFileSelect = useCallback(
|
||
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
const files = e.target.files;
|
||
|
|
if (files && files.length > 0) {
|
||
|
|
handleFile(files[0]);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[handleFile]
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div data-testid="ocr-upload" className="space-y-4">
|
||
|
|
<div
|
||
|
|
data-testid="ocr-dropzone"
|
||
|
|
onDragEnter={handleDragEnter}
|
||
|
|
onDragLeave={handleDragLeave}
|
||
|
|
onDragOver={handleDragOver}
|
||
|
|
onDrop={handleDrop}
|
||
|
|
onClick={() => fileInputRef.current?.click()}
|
||
|
|
className={`
|
||
|
|
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors
|
||
|
|
${isDragging ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'}
|
||
|
|
`}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
ref={fileInputRef}
|
||
|
|
type="file"
|
||
|
|
accept="image/*"
|
||
|
|
onChange={handleFileSelect}
|
||
|
|
className="hidden"
|
||
|
|
data-testid="ocr-file-input"
|
||
|
|
/>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<svg
|
||
|
|
className="mx-auto h-12 w-12 text-text-muted"
|
||
|
|
fill="none"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
stroke="currentColor"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<p className="text-text font-medium">
|
||
|
|
{isDragging ? 'Drop image here' : 'Drag and drop scan image here'}
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-text-muted">or click to browse</p>
|
||
|
|
<p className="text-xs text-text-muted">PNG, JPEG, WebP (max 50 MB)</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{uploading && (
|
||
|
|
<div data-testid="ocr-uploading" className="text-center text-text-muted">
|
||
|
|
Uploading...
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<div data-testid="ocr-upload-error" className="p-4 bg-error/10 text-error rounded-lg">
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{success && (
|
||
|
|
<div data-testid="ocr-upload-success" className="p-4 bg-green-50 text-green-700 rounded-lg">
|
||
|
|
{success}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|