/** * Image Uploader Component * Agent 3: File Upload & Storage System * * Drag & drop image upload with preview and progress */ import React, { useState, useCallback, useRef } from 'react'; import type { FileCategory } from '../../lib/storage/types'; interface UploadedFile { id: string; url: string; thumbnailUrl?: string; width?: number; height?: number; size: number; } interface ImageUploaderProps { category?: FileCategory; plantId?: string; farmId?: string; userId?: string; onUpload?: (file: UploadedFile) => void; onError?: (error: string) => void; maxFiles?: number; accept?: string; className?: string; } interface UploadState { isUploading: boolean; progress: number; error?: string; preview?: string; } export function ImageUploader({ category = 'plant-photo', plantId, farmId, userId, onUpload, onError, maxFiles = 1, accept = 'image/*', className = '', }: ImageUploaderProps) { const [uploadState, setUploadState] = useState({ isUploading: false, progress: 0, }); const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); 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 uploadFile = async (file: File) => { // Create preview const reader = new FileReader(); reader.onload = (e) => { setUploadState((prev) => ({ ...prev, preview: e.target?.result as string, })); }; reader.readAsDataURL(file); // Start upload setUploadState((prev) => ({ ...prev, isUploading: true, progress: 0, error: undefined, })); const formData = new FormData(); formData.append('file', file); formData.append('category', category); if (plantId) formData.append('plantId', plantId); if (farmId) formData.append('farmId', farmId); if (userId) formData.append('userId', userId); try { const response = await fetch('/api/upload/image', { method: 'POST', body: formData, }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Upload failed'); } setUploadState({ isUploading: false, progress: 100, preview: data.file.thumbnailUrl || data.file.url, }); onUpload?.(data.file); } catch (error) { const message = error instanceof Error ? error.message : 'Upload failed'; setUploadState((prev) => ({ ...prev, isUploading: false, error: message, })); onError?.(message); } }; const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = Array.from(e.dataTransfer.files).slice(0, maxFiles); if (files.length > 0) { await uploadFile(files[0]); } }, [maxFiles] ); const handleFileChange = useCallback( async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []).slice(0, maxFiles); if (files.length > 0) { await uploadFile(files[0]); } }, [maxFiles] ); const handleClick = () => { fileInputRef.current?.click(); }; const handleRemove = () => { setUploadState({ isUploading: false, progress: 0, preview: undefined, error: undefined, }); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; return (
{uploadState.preview ? (
Uploaded preview
) : (

Click to upload {' '}or drag and drop

PNG, JPG, GIF, WEBP up to 5MB

)} {uploadState.isUploading && (

Uploading...

)} {uploadState.error && (

{uploadState.error}

)}
); } export default ImageUploader;