# File Upload Dropzone Implementation ## Table of Contents - [Basic Dropzone](#basic-dropzone) - [Visual Feedback States](#visual-feedback-states) - [File Type Validation](#file-type-validation) - [Multi-File Handling](#multi-file-handling) - [Progress Indicators](#progress-indicators) - [Advanced Features](#advanced-features) ## Basic Dropzone ### Core Implementation ```tsx import { useDropzone } from 'react-dropzone'; function BasicDropzone({ onFilesAdded }) { const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject, acceptedFiles, rejectedFiles } = useDropzone({ accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif'], 'application/pdf': ['.pdf'] }, maxFiles: 10, maxSize: 5 * 1024 * 1024, // 5MB onDrop: (acceptedFiles, rejectedFiles) => { onFilesAdded(acceptedFiles); if (rejectedFiles.length > 0) { handleRejectedFiles(rejectedFiles); } } }); return (
{isDragActive ? (

Drop the files here...

) : (

Drag 'n' drop files here, or click to select

)}
Accepted: Images, PDFs Max size: 5MB Max files: 10
); } ``` ### Native HTML5 Implementation ```tsx function NativeDropzone({ onFilesAdded }) { const [dragActive, setDragActive] = useState(false); const [dragCounter, setDragCounter] = useState(0); const dropRef = useRef(null); const handleDrag = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDragIn = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragCounter(prev => prev + 1); if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) { setDragActive(true); } }; const handleDragOut = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragCounter(prev => prev - 1); if (dragCounter === 1) { setDragActive(false); } }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); setDragCounter(0); if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) { const files = Array.from(e.dataTransfer.files); onFilesAdded(files); e.dataTransfer.clearData(); } }; useEffect(() => { const div = dropRef.current; if (!div) return; div.addEventListener('dragenter', handleDragIn); div.addEventListener('dragleave', handleDragOut); div.addEventListener('dragover', handleDrag); div.addEventListener('drop', handleDrop); return () => { div.removeEventListener('dragenter', handleDragIn); div.removeEventListener('dragleave', handleDragOut); div.removeEventListener('dragover', handleDrag); div.removeEventListener('drop', handleDrop); }; }, []); return (
{ if (e.target.files) { onFilesAdded(Array.from(e.target.files)); } }} style={{ display: 'none' }} />
); } ``` ## Visual Feedback States ### State-Based Styling ```css /* Base dropzone styles */ .dropzone { border: 2px dashed var(--drop-zone-border); border-radius: var(--drop-zone-border-radius); padding: var(--drop-zone-padding); background: var(--drop-zone-bg); cursor: pointer; transition: all 0.2s ease; text-align: center; min-height: 200px; display: flex; flex-direction: column; justify-content: center; align-items: center; } /* Hovering with files */ .dropzone--active { border-color: var(--drop-zone-border-active); background: var(--drop-zone-bg-active); transform: scale(1.02); box-shadow: var(--shadow-lg); } /* Valid files being dragged */ .dropzone--accept { border-color: var(--color-success); background: var(--color-success-50); } /* Invalid files being dragged */ .dropzone--reject { border-color: var(--color-danger); background: var(--color-danger-50); animation: shake 0.5s; } /* Shake animation for rejection */ @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } /* File type indicators */ .file-type-indicator { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-size: 0.875rem; } .file-type-indicator--image { background: var(--color-blue-100); color: var(--color-blue-700); } .file-type-indicator--pdf { background: var(--color-red-100); color: var(--color-red-700); } .file-type-indicator--video { background: var(--color-purple-100); color: var(--color-purple-700); } ``` ### Enhanced Visual Feedback Component ```tsx function EnhancedDropzone({ onFilesAdded, accept, maxSize }) { const [isDragging, setIsDragging] = useState(false); const [isValidating, setIsValidating] = useState(false); const [validationStatus, setValidationStatus] = useState<'valid' | 'invalid' | null>(null); const validateDraggedItems = (e: DragEvent) => { if (!e.dataTransfer?.items) return null; setIsValidating(true); const items = Array.from(e.dataTransfer.items); const hasValidFiles = items.some(item => { if (item.kind !== 'file') return false; const type = item.type; if (accept) { return Object.keys(accept).some(acceptType => { if (acceptType === '*/*') return true; return type.match(new RegExp(acceptType.replace('*', '.*'))); }); } return true; }); setValidationStatus(hasValidFiles ? 'valid' : 'invalid'); setIsValidating(false); return hasValidFiles; }; return (
{ e.preventDefault(); setIsDragging(true); validateDraggedItems(e); }} onDragLeave={(e) => { e.preventDefault(); if (e.currentTarget === e.target) { setIsDragging(false); setValidationStatus(null); } }} onDragOver={(e) => { e.preventDefault(); }} onDrop={(e) => { e.preventDefault(); setIsDragging(false); setValidationStatus(null); const files = Array.from(e.dataTransfer.files); onFilesAdded(files); }} > {/* Visual indicators */}
{isDragging && (
{isValidating && } {validationStatus === 'valid' && } {validationStatus === 'invalid' && }
)}
{/* Content */}
{isDragging ? ( <> {validationStatus === 'valid' &&

Drop files to upload

} {validationStatus === 'invalid' &&

Invalid file type

} {isValidating &&

Checking files...

} ) : (

Drag and drop files here

)}
); } ``` ## File Type Validation ### Comprehensive Validation ```tsx interface FileValidationRules { accept?: Record; maxSize?: number; minSize?: number; maxFiles?: number; validator?: (file: File) => boolean | Promise; } function useFileValidation(rules: FileValidationRules) { const validateFile = async (file: File): Promise<{ valid: boolean; errors: string[]; }> => { const errors: string[] = []; // Type validation if (rules.accept) { const isValidType = Object.entries(rules.accept).some(([type, extensions]) => { if (type === '*/*') return true; // Check MIME type if (file.type.match(new RegExp(type.replace('*', '.*')))) { return true; } // Check extension const fileName = file.name.toLowerCase(); return extensions.some(ext => fileName.endsWith(ext)); }); if (!isValidType) { errors.push(`File type not accepted: ${file.type || 'unknown'}`); } } // Size validation if (rules.maxSize && file.size > rules.maxSize) { errors.push(`File too large: ${formatFileSize(file.size)} (max: ${formatFileSize(rules.maxSize)})`); } if (rules.minSize && file.size < rules.minSize) { errors.push(`File too small: ${formatFileSize(file.size)} (min: ${formatFileSize(rules.minSize)})`); } // Custom validation if (rules.validator) { try { const isValid = await rules.validator(file); if (!isValid) { errors.push('File failed custom validation'); } } catch (error) { errors.push(`Validation error: ${error.message}`); } } return { valid: errors.length === 0, errors }; }; const validateFiles = async (files: File[]): Promise<{ accepted: File[]; rejected: Array<{ file: File; errors: string[] }>; }> => { // Check max files if (rules.maxFiles && files.length > rules.maxFiles) { return { accepted: [], rejected: files.map(file => ({ file, errors: [`Too many files. Maximum allowed: ${rules.maxFiles}`] })) }; } const results = await Promise.all( files.map(async (file) => { const validation = await validateFile(file); return { file, ...validation }; }) ); return { accepted: results.filter(r => r.valid).map(r => r.file), rejected: results.filter(r => !r.valid).map(r => ({ file: r.file, errors: r.errors })) }; }; return { validateFile, validateFiles }; } ``` ### Image-Specific Validation ```tsx async function validateImage(file: File): Promise { return new Promise((resolve) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); // Check dimensions const maxWidth = 4000; const maxHeight = 4000; const minWidth = 100; const minHeight = 100; if (img.width > maxWidth || img.height > maxHeight) { console.error(`Image too large: ${img.width}x${img.height}`); resolve(false); } else if (img.width < minWidth || img.height < minHeight) { console.error(`Image too small: ${img.width}x${img.height}`); resolve(false); } else { resolve(true); } }; img.onerror = () => { URL.revokeObjectURL(url); resolve(false); }; img.src = url; }); } ``` ## Multi-File Handling ### File List Management ```tsx interface FileWithMeta extends File { id: string; status: 'pending' | 'uploading' | 'success' | 'error'; progress: number; error?: string; preview?: string; } function MultiFileDropzone() { const [files, setFiles] = useState([]); const addFiles = (newFiles: File[]) => { const filesWithMeta: FileWithMeta[] = newFiles.map(file => ({ ...file, id: generateId(), status: 'pending', progress: 0, preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined })); setFiles(prev => [...prev, ...filesWithMeta]); }; const removeFile = (id: string) => { setFiles(prev => { const file = prev.find(f => f.id === id); if (file?.preview) { URL.revokeObjectURL(file.preview); } return prev.filter(f => f.id !== id); }); }; const updateFileStatus = (id: string, updates: Partial) => { setFiles(prev => prev.map(f => f.id === id ? { ...f, ...updates } : f) ); }; return (
{files.map(file => ( removeFile(file.id)} onRetry={() => uploadFile(file)} /> ))}
{files.length > 0 && (
)}
); } ``` ### File Item Component ```tsx function FileItem({ file, onRemove, onRetry }) { return (
{/* Preview */} {file.preview && (
{file.name}
)} {/* File icon for non-images */} {!file.preview && (
{getFileIcon(file.type)}
)} {/* File info */}
{file.name}
{formatFileSize(file.size)}
{/* Status indicator */}
{file.status === 'uploading' && (
)} {file.status === 'success' && ✓ Uploaded} {file.status === 'error' && ( ✗ {file.error} )}
{/* Actions */}
{file.status === 'error' && ( )}
); } ``` ## Progress Indicators ### Upload Progress Tracking ```tsx function useFileUpload() { const uploadFile = async ( file: FileWithMeta, onProgress: (progress: number) => void ): Promise => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('file', file); // Track upload progress xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const progress = (e.loaded / e.total) * 100; onProgress(progress); } }); // Handle completion xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error(`Upload failed: ${xhr.status}`)); } }); // Handle errors xhr.addEventListener('error', () => { reject(new Error('Upload failed')); }); // Send request xhr.open('POST', '/api/upload'); xhr.send(formData); }); }; const uploadWithProgress = async ( file: FileWithMeta, updateProgress: (id: string, progress: number) => void ) => { try { await uploadFile(file, (progress) => { updateProgress(file.id, progress); }); return { success: true }; } catch (error) { return { success: false, error: error.message }; } }; return { uploadFile, uploadWithProgress }; } ``` ### Progress UI Components ```tsx function CircularProgress({ progress, size = 60 }) { const circumference = 2 * Math.PI * 20; const offset = circumference - (progress / 100) * circumference; return ( {Math.round(progress)}% ); } ``` ## Advanced Features ### Paste from Clipboard ```tsx function PasteableDropzone({ onFilesAdded }) { useEffect(() => { const handlePaste = async (e: ClipboardEvent) => { const items = Array.from(e.clipboardData?.items || []); const files: File[] = []; for (const item of items) { if (item.kind === 'file') { const file = item.getAsFile(); if (file) files.push(file); } else if (item.type === 'text/html') { // Extract images from HTML const html = await new Promise(resolve => { item.getAsString(resolve); }); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const images = doc.querySelectorAll('img'); for (const img of images) { const response = await fetch(img.src); const blob = await response.blob(); const file = new File([blob], 'pasted-image.png', { type: blob.type }); files.push(file); } } } if (files.length > 0) { onFilesAdded(files); } }; document.addEventListener('paste', handlePaste); return () => document.removeEventListener('paste', handlePaste); }, [onFilesAdded]); return (

Drag files here or paste from clipboard (Ctrl/Cmd+V)

); } ``` ### Camera Capture ```tsx function CameraDropzone({ onFilesAdded }) { const [showCamera, setShowCamera] = useState(false); const videoRef = useRef(null); const canvasRef = useRef(null); const startCamera = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); if (videoRef.current) { videoRef.current.srcObject = stream; setShowCamera(true); } } catch (error) { console.error('Camera access denied:', error); } }; const capturePhoto = () => { if (!videoRef.current || !canvasRef.current) return; const video = videoRef.current; const canvas = canvasRef.current; const context = canvas.getContext('2d'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; context?.drawImage(video, 0, 0); canvas.toBlob((blob) => { if (blob) { const file = new File([blob], `photo-${Date.now()}.jpg`, { type: 'image/jpeg' }); onFilesAdded([file]); stopCamera(); } }, 'image/jpeg', 0.9); }; const stopCamera = () => { const stream = videoRef.current?.srcObject as MediaStream; stream?.getTracks().forEach(track => track.stop()); setShowCamera(false); }; return (
{showCamera && (
)}
); } ```