'use client' // React Imports import { useEffect, useState } from 'react' // MUI Imports import type { BoxProps } from '@mui/material/Box' import Button from '@mui/material/Button' import IconButton from '@mui/material/IconButton' import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' import Typography from '@mui/material/Typography' import Chip from '@mui/material/Chip' import LinearProgress from '@mui/material/LinearProgress' import { styled } from '@mui/material/styles' // Third-party Imports import { useDropzone } from 'react-dropzone' // Component Imports import Link from '@components/Link' import CustomAvatar from '@core/components/mui/Avatar' // Styled Component Imports import AppReactDropzone from '@/libs/styles/AppReactDropzone' type FileProp = { name: string type: string size: number } type UploadedImage = { id: string url: string name: string size: number } type UploadProgress = { [fileId: string]: number } interface MultipleImageUploadProps { // Required props onUpload: (files: File[]) => Promise | string[] // Returns array of image URLs onSingleUpload?: (file: File) => Promise | string // For individual file upload // Optional customization props title?: string | null currentImages?: UploadedImage[] onImagesChange?: (images: UploadedImage[]) => void onImageRemove?: (imageId: string) => void // Upload state isUploading?: boolean uploadProgress?: UploadProgress // Limits maxFiles?: number maxFileSize?: number // in bytes acceptedFileTypes?: string[] // UI customization showUrlOption?: boolean uploadButtonText?: string browseButtonText?: string dragDropText?: string replaceText?: string maxFilesText?: string // Style customization className?: string disabled?: boolean // Upload modes uploadMode?: 'batch' | 'individual' // batch: upload all at once, individual: upload one by one } // Styled Dropzone Component const Dropzone = styled(AppReactDropzone)(({ theme }) => ({ '& .dropzone': { minHeight: 'unset', padding: theme.spacing(12), [theme.breakpoints.down('sm')]: { paddingInline: theme.spacing(5) }, '&+.MuiList-root .MuiListItem-root .file-name': { fontWeight: theme.typography.body1.fontWeight } } })) const MultipleImageUpload: React.FC = ({ onUpload, onSingleUpload, title = null, currentImages = [], onImagesChange, onImageRemove, isUploading = false, uploadProgress = {}, maxFiles = 10, maxFileSize = 5 * 1024 * 1024, // 5MB default acceptedFileTypes = ['image/*'], showUrlOption = true, uploadButtonText = 'Upload All', browseButtonText = 'Browse Images', dragDropText = 'Drag and Drop Your Images Here.', replaceText = 'Drop Images to Add More', maxFilesText = 'Maximum {max} files allowed', className = '', disabled = false, uploadMode = 'batch' }) => { // States const [files, setFiles] = useState([]) const [error, setError] = useState('') const [individualUploading, setIndividualUploading] = useState>(new Set()) const handleBatchUpload = async () => { if (!files.length) return try { setError('') const imageUrls = await onUpload(files) if (Array.isArray(imageUrls)) { const newImages: UploadedImage[] = files.map((file, index) => ({ id: `${Date.now()}-${index}`, url: imageUrls[index], name: file.name, size: file.size })) const updatedImages = [...currentImages, ...newImages] onImagesChange?.(updatedImages) setFiles([]) // Clear files after successful upload } } catch (err) { setError(err instanceof Error ? err.message : 'Upload failed') } } const handleIndividualUpload = async (file: File, fileIndex: number) => { if (!onSingleUpload) return const fileId = `${file.name}-${fileIndex}` setIndividualUploading(prev => new Set(prev).add(fileId)) try { setError('') const imageUrl = await onSingleUpload(file) if (typeof imageUrl === 'string') { const newImage: UploadedImage = { id: `${Date.now()}-${fileIndex}`, url: imageUrl, name: file.name, size: file.size } const updatedImages = [...currentImages, newImage] onImagesChange?.(updatedImages) // Remove uploaded file from pending files setFiles(prev => prev.filter((_, index) => index !== fileIndex)) } } catch (err) { setError(err instanceof Error ? err.message : 'Upload failed') } finally { setIndividualUploading(prev => { const newSet = new Set(prev) newSet.delete(fileId) return newSet }) } } // Hooks const { getRootProps, getInputProps } = useDropzone({ onDrop: (acceptedFiles: File[]) => { setError('') if (acceptedFiles.length === 0) return const totalFiles = currentImages.length + files.length + acceptedFiles.length if (totalFiles > maxFiles) { setError(`Cannot upload more than ${maxFiles} files. Current: ${currentImages.length + files.length}`) return } // Validate file sizes const invalidFiles = acceptedFiles.filter(file => file.size > maxFileSize) if (invalidFiles.length > 0) { setError(`Some files exceed ${formatFileSize(maxFileSize)} limit`) return } // Add to existing files setFiles(prev => [...prev, ...acceptedFiles]) }, accept: acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}), disabled: disabled || isUploading, multiple: true }) const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } const renderFilePreview = (file: FileProp) => { if (file.type.startsWith('image')) { return ( {file.name} ) } else { return } } const handleRemoveFile = (fileIndex: number) => { setFiles(prev => prev.filter((_, index) => index !== fileIndex)) setError('') } const handleRemoveCurrentImage = (imageId: string) => { onImageRemove?.(imageId) } const handleRemoveAllFiles = () => { setFiles([]) setError('') } const isIndividualUploading = (file: File, index: number) => { const fileId = `${file.name}-${index}` return individualUploading.has(fileId) } const fileList = files.map((file: File, index: number) => { const isFileUploading = isIndividualUploading(file, index) const progress = uploadProgress[`${file.name}-${index}`] || 0 return (
{renderFilePreview(file)}
{file.name} {formatFileSize(file.size)} {isFileUploading && progress > 0 && ( )}
{uploadMode === 'individual' && onSingleUpload && ( )} handleRemoveFile(index)} disabled={isUploading || isFileUploading}>
) }) const currentImagesList = currentImages.map(image => (
{image.name}
{image.name} {formatFileSize(image.size)}
{onImageRemove && ( handleRemoveCurrentImage(image.id)} color='error' disabled={isUploading}> )}
)) return ( {/* Conditional title and URL option header */} {title && (
{title} {showUrlOption && ( Add media from URL )}
)} {/* File limits info */}
{maxFilesText.replace('{max}', maxFiles.toString())} {currentImages.length + files.length} / {maxFiles} files
{currentImages.length > 0 || files.length > 0 ? replaceText : dragDropText} or
{/* Error Message */} {error && ( {error} )} {/* Current uploaded images */} {currentImages.length > 0 && (
Uploaded Images ({currentImages.length}): {currentImagesList}
)} {/* Pending files list and upload buttons */} {files.length > 0 && (
Pending Files ({files.length}): {fileList}
{uploadMode === 'batch' && ( )}
)}
) } export default MultipleImageUpload // ===== USAGE EXAMPLES ===== // 1. Batch upload mode (upload all files at once) // const [images, setImages] = useState([]) // // setImages(prev => prev.filter(img => img.id !== id))} // maxFiles={5} // uploadMode="batch" // /> // 2. Individual upload mode (upload files one by one) // setImages(prev => prev.filter(img => img.id !== id))} // maxFiles={10} // uploadMode="individual" // uploadProgress={uploadProgress} // /> // 3. Without title, custom limits // // 4. Example upload handlers // const handleBatchUpload = async (files: File[]): Promise => { // const formData = new FormData() // files.forEach(file => formData.append('images', file)) // // const response = await fetch('/api/upload-multiple', { // method: 'POST', // body: formData // }) // // const result = await response.json() // return result.urls // Array of uploaded image URLs // } // // const handleSingleUpload = async (file: File): Promise => { // const formData = new FormData() // formData.append('image', file) // // const response = await fetch('/api/upload-single', { // method: 'POST', // body: formData // }) // // const result = await response.json() // return result.url // Single uploaded image URL // }