Files
tfm_ainventory/frontend/components/ItemPhotoUpload.tsx
2026-04-25 12:53:41 +03:00

156 lines
4.4 KiB
TypeScript

import React, { useRef, useState } from 'react';
import { Camera, Upload, Loader2 } from 'lucide-react';
import { usePhotoUpload } from '@/hooks/usePhotoUpload';
import { toast } from 'react-hot-toast';
interface ItemPhotoUploadProps {
itemId: number;
onUploadSuccess: (photo: { thumbnail_url: string; full_url: string; uploaded_at: string }) => void;
onError: (errorMessage: string) => void;
}
export default function ItemPhotoUpload({
itemId,
onUploadSuccess,
onError,
}: ItemPhotoUploadProps) {
const { upload, isLoading, error } = usePhotoUpload();
const fileInputRef = useRef<HTMLInputElement>(null);
const cameraInputRef = useRef<HTMLInputElement>(null);
const [localError, setLocalError] = useState<string | null>(null);
const toastIdRef = useRef<string | null>(null);
// Sync hook error to local state for display
React.useEffect(() => {
if (error) {
setLocalError(error);
}
}, [error]);
// Cleanup: dismiss pending toasts on unmount
React.useEffect(() => {
return () => {
if (toastIdRef.current) {
toast.dismiss(toastIdRef.current);
}
};
}, []);
const handleFileSelect = async (file: File) => {
setLocalError(null);
if (!file) return;
toastIdRef.current = toast.loading('Uploading...');
try {
const photo = await upload(file, itemId);
toast.success('Photo uploaded successfully', { id: toastIdRef.current });
toastIdRef.current = null;
onUploadSuccess(photo);
// Reset file inputs
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
if (cameraInputRef.current) {
cameraInputRef.current.value = '';
}
} catch (err: any) {
const errorMsg = err.message || 'Upload failed';
setLocalError(errorMsg);
toast.error(errorMsg, { id: toastIdRef.current || undefined });
toastIdRef.current = null;
onError(errorMsg);
}
};
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
handleFileSelect(files[0]);
}
};
const handleCameraCapture = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
handleFileSelect(files[0]);
}
};
const triggerFileInput = () => {
fileInputRef.current?.click();
};
const triggerCameraInput = () => {
cameraInputRef.current?.click();
};
return (
<div className="flex flex-col gap-3">
{/* Hidden file inputs */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileInputChange}
className="sr-only"
aria-label="Upload photo from device"
/>
<input
ref={cameraInputRef}
type="file"
accept="image/*"
capture="environment"
onChange={handleCameraCapture}
className="sr-only"
aria-label="Capture photo with camera"
/>
{/* Button group */}
<div className="flex gap-2">
{/* File upload button */}
<button
onClick={triggerFileInput}
disabled={isLoading}
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-primary text-white rounded-none font-normal text-base transition-colors hover:bg-primary/90 disabled:opacity-60 disabled:cursor-not-allowed"
aria-label="Upload photo"
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Upload className="w-5 h-5" />
)}
<span>Upload</span>
</button>
{/* Camera button (mobile) */}
<button
onClick={triggerCameraInput}
disabled={isLoading}
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-[#222222] text-white rounded-none font-normal text-base transition-colors hover:bg-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
aria-label="Capture photo with camera"
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Camera className="w-5 h-5" />
)}
<span>Camera</span>
</button>
</div>
{/* Status messages */}
{isLoading && (
<div className="text-sm text-secondary">Uploading...</div>
)}
{localError && (
<div className="text-sm text-rose-500">{localError}</div>
)}
</div>
);
}