import { useState, useCallback, useRef, useImperativeHandle, useEffect, forwardRef, } from 'react'; import { X, Image as ImageIcon, Upload, ChevronRight, AlertCircle, } from 'lucide-react'; import { Button } from './button'; import { Alert, AlertDescription } from './alert'; import { cn, formatFileSize } from '@/lib/utils'; import { imagesApi } from '@/lib/api'; import type { ImageResponse } from 'shared/types'; interface ImageUploadSectionProps { images: ImageResponse[]; onImagesChange: (images: ImageResponse[]) => void; onUpload: (file: File) => Promise; onDelete?: (imageId: string) => Promise; onImageUploaded?: (image: ImageResponse) => void; // Custom callback for upload success isUploading?: boolean; disabled?: boolean; readOnly?: boolean; collapsible?: boolean; defaultExpanded?: boolean; hideDropZone?: boolean; // Hide the drag and drop area className?: string; } export interface ImageUploadSectionHandle { addFiles: (files: FileList | File[] | null) => Promise; } const MAX_SIZE_BYTES = 20 * 1024 * 1024; // 20MB const VALID_TYPES = [ 'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp', 'image/bmp', 'image/svg+xml', ]; export const ImageUploadSection = forwardRef< ImageUploadSectionHandle, ImageUploadSectionProps >( ( { images, onImagesChange, onUpload, onDelete, onImageUploaded, isUploading = false, disabled = false, readOnly = false, collapsible = true, defaultExpanded = false, hideDropZone = false, className, }, ref ) => { const [isExpanded, setIsExpanded] = useState( defaultExpanded || images.length > 0 ); const [isDragging, setIsDragging] = useState(false); const [uploadingFiles, setUploadingFiles] = useState>( new Set() ); const [errorMessage, setErrorMessage] = useState(null); const fileInputRef = useRef(null); const latestImagesRef = useRef(images); useEffect(() => { latestImagesRef.current = images; }, [images]); useEffect(() => { if (collapsible && images.length > 0 && !isExpanded) { setIsExpanded(true); } }, [collapsible, images.length, isExpanded]); const handleFiles = useCallback( async (filesInput: FileList | File[] | null) => { if (!filesInput || disabled || readOnly) return; const files = Array.isArray(filesInput) ? filesInput : Array.from(filesInput); setErrorMessage(null); const invalidFiles: string[] = []; const oversizedFiles: string[] = []; const validFiles: File[] = []; files.forEach((file) => { if (!VALID_TYPES.includes(file.type.toLowerCase())) { invalidFiles.push(file.name); return; } if (file.size > MAX_SIZE_BYTES) { oversizedFiles.push( `${file.name} (${(file.size / 1048576).toFixed(1)} MB)` ); return; } validFiles.push(file); }); if (invalidFiles.length > 0 || oversizedFiles.length > 0) { const errors: string[] = []; if (invalidFiles.length > 0) { errors.push(`Unsupported file type: ${invalidFiles.join(', ')}`); } if (oversizedFiles.length > 0) { errors.push( `Files too large (max 20 MB): ${oversizedFiles.join(', ')}` ); } setErrorMessage(errors.join('. ')); } for (const file of validFiles) { const tempId = `uploading-${Date.now()}-${file.name}`; setUploadingFiles((prev) => new Set(prev).add(tempId)); try { const uploadedImage = await onUpload(file); // Call custom upload callback if provided, otherwise use default behavior if (onImageUploaded) { onImageUploaded(uploadedImage); } else { const nextImages = [...latestImagesRef.current, uploadedImage]; latestImagesRef.current = nextImages; onImagesChange(nextImages); } setErrorMessage(null); } catch (error: unknown) { console.error('Failed to upload image:', error); const message = error instanceof Error ? error.message : 'Failed to upload image. Please try again.'; setErrorMessage(message); } finally { setUploadingFiles((prev) => { const next = new Set(prev); next.delete(tempId); return next; }); } } }, [disabled, readOnly, onUpload, onImageUploaded, onImagesChange] ); useImperativeHandle( ref, () => ({ addFiles: async (files: FileList | File[] | null) => { await handleFiles(files); }, }), [handleFiles] ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); void handleFiles(e.dataTransfer.files); }, [handleFiles] ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleRemoveImage = useCallback( async (imageId: string) => { if (onDelete) { try { await onDelete(imageId); } catch (error) { console.error('Failed to delete image:', error); } } onImagesChange(images.filter((img) => img.id !== imageId)); }, [images, onImagesChange, onDelete] ); const content = (
{/* Error message */} {errorMessage && ( {errorMessage} )} {/* Read-only message */} {readOnly && images.length === 0 && (

No images attached

)} {/* Drop zone - only show when not read-only and not hidden */} {!readOnly && !hideDropZone && (

Drag and drop images here, or click to select

{ void handleFiles(e.target.files); }} disabled={disabled} />
)} {/* Image previews */} {images.length > 0 && (
{images.map((image) => (
{image.original_name}

{image.original_name}

{formatFileSize(image.size_bytes)}

{!disabled && !readOnly && ( )}
))}
)} {/* Uploading indicators */} {uploadingFiles.size > 0 && (
{Array.from(uploadingFiles).map((tempId) => (
Uploading...
))}
)}
); if (!collapsible) { return content; } return (
{isExpanded && content}
); } ); ImageUploadSection.displayName = 'ImageUploadSection';