"use client"; import React, { useState, useRef, useCallback } from "react"; import { cn } from "@/lib/utils"; import { ImageIcon, X, Upload } from "lucide-react"; import type { ImageAttachment } from "@/store/app-store"; interface ImageDropZoneProps { onImagesSelected: (images: ImageAttachment[]) => void; maxFiles?: number; maxFileSize?: number; // in bytes, default 10MB className?: string; children?: React.ReactNode; disabled?: boolean; images?: ImageAttachment[]; // Optional controlled images prop } const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB export function ImageDropZone({ onImagesSelected, maxFiles = 5, maxFileSize = DEFAULT_MAX_FILE_SIZE, className, children, disabled = false, images, }: ImageDropZoneProps) { const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [internalImages, setInternalImages] = useState([]); const fileInputRef = useRef(null); // Use controlled images if provided, otherwise use internal state const selectedImages = images ?? internalImages; // Update images - for controlled mode, just call the callback; for uncontrolled, also update internal state const updateImages = useCallback((newImages: ImageAttachment[]) => { if (images === undefined) { setInternalImages(newImages); } onImagesSelected(newImages); }, [images, onImagesSelected]); const processFiles = useCallback(async (files: FileList) => { if (disabled || isProcessing) return; setIsProcessing(true); const newImages: ImageAttachment[] = []; const errors: string[] = []; for (const file of Array.from(files)) { // Validate file type if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { errors.push(`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`); continue; } // Validate file size if (file.size > maxFileSize) { const maxSizeMB = maxFileSize / (1024 * 1024); errors.push(`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`); continue; } // Check if we've reached max files if (newImages.length + selectedImages.length >= maxFiles) { errors.push(`Maximum ${maxFiles} images allowed.`); break; } try { const base64 = await fileToBase64(file); const imageAttachment: ImageAttachment = { id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, data: base64, mimeType: file.type, filename: file.name, size: file.size, }; newImages.push(imageAttachment); } catch (error) { errors.push(`${file.name}: Failed to process image.`); } } if (errors.length > 0) { console.warn('Image upload errors:', errors); // You could show these errors to the user via a toast or notification } if (newImages.length > 0) { const allImages = [...selectedImages, ...newImages]; updateImages(allImages); } setIsProcessing(false); }, [disabled, isProcessing, maxFiles, maxFileSize, selectedImages, updateImages]); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (disabled) return; const files = e.dataTransfer.files; if (files.length > 0) { processFiles(files); } }, [disabled, processFiles]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!disabled) { setIsDragOver(true); } }, [disabled]); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const handleFileSelect = useCallback((e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { processFiles(files); } // Reset the input so the same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = ''; } }, [processFiles]); const handleBrowseClick = useCallback(() => { if (!disabled && fileInputRef.current) { fileInputRef.current.click(); } }, [disabled]); const removeImage = useCallback((imageId: string) => { const updated = selectedImages.filter(img => img.id !== imageId); updateImages(updated); }, [selectedImages, updateImages]); const clearAllImages = useCallback(() => { updateImages([]); }, [updateImages]); return (
{/* Hidden file input */} {/* Drop zone */}
{children || (
{isProcessing ? ( ) : ( )}

{isDragOver && !disabled ? "Drop your images here" : "Drag images here or click to browse"}

{maxFiles > 1 ? `Up to ${maxFiles} images` : "1 image"}, max {Math.round(maxFileSize / (1024 * 1024))}MB each

{!disabled && ( )}
)}
{/* Image previews */} {selectedImages.length > 0 && (

{selectedImages.length} image{selectedImages.length > 1 ? 's' : ''} selected

{selectedImages.map((image) => (
{/* Image thumbnail */}
{image.filename}
{/* Image info */}

{image.filename}

{formatFileSize(image.size)}

{/* Remove button */} {!disabled && ( )}
))}
)}
); } function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === 'string') { resolve(reader.result); } else { reject(new Error('Failed to read file as base64')); } }; reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsDataURL(file); }); } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }