156 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|