import { useState, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { HiUpload, HiX, HiPhotograph, HiDocumentText, HiCheckCircle, HiExclamationCircle } from 'react-icons/hi'; import { HiArrowPath } from 'react-icons/hi2'; import api from '../../services/api'; import './FileUpload.css'; /** * Reusable drag-and-drop file upload component. * * Props: * category {string} — storage category: 'profiles' | 'campaigns' | 'gallery' | 'stories' | 'blog' | 'help-requests' * entityId {string} — optional related entity ID * accept {string} — MIME types for the file input (e.g. "image/*" or "image/*,application/pdf") * label {string} — label shown in the drop zone * maxMB {number} — client-side file size hint (just for UI; server validates too) * currentUrl {string} — existing file URL to show as preview * onUpload {function} — callback(url, key) called on successful upload * onDelete {function} — callback(key) called when user removes the file * multiple {boolean} — allow multiple file selection * className {string} — extra class for the root element */ export default function FileUpload({ category, entityId = '', accept = 'image/*', label = 'Upload a file', maxMB = 5, currentUrl = '', onUpload, onDelete, multiple = false, className = '', }) { const [isDragging, setIsDragging] = useState(false); const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); const [error, setError] = useState(''); const [preview, setPreview] = useState(currentUrl); const [uploadedKey, setUploadedKey] = useState(''); const isImage = (file) => file?.type?.startsWith('image/'); const handleFiles = useCallback(async (files) => { if (!files || files.length === 0) return; const file = files[0]; // handle first file (multiple handled separately) setError(''); setProgress(0); // Client-side size check const maxBytes = maxMB * 1024 * 1024; if (file.size > maxBytes) { setError(`File too large. Maximum size is ${maxMB}MB.`); return; } // Show local preview for images immediately if (isImage(file)) { const reader = new FileReader(); reader.onload = (e) => setPreview(e.target.result); reader.readAsDataURL(file); } setUploading(true); try { const formData = new FormData(); formData.append('file', file); formData.append('category', category); if (entityId) formData.append('entityId', entityId); // Use raw axios call for multipart + progress tracking // (api interceptor handles auth token) const result = await api.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { const pct = Math.round((e.loaded * 100) / (e.total || 1)); setProgress(pct); }, }); const url = result.url || result.previewUrl || ''; const key = result.key || ''; setUploadedKey(key); if (url) setPreview(url); setProgress(100); onUpload?.(url, key, result); } catch (err) { setError(err.message || 'Upload failed. Please try again.'); // Revert preview on failure setPreview(currentUrl); } finally { setUploading(false); } }, [category, entityId, maxMB, currentUrl, onUpload]); const handleRemove = useCallback(() => { const key = uploadedKey; setPreview(''); setUploadedKey(''); setError(''); onDelete?.(key); }, [uploadedKey, onDelete]); // Drag events const onDragOver = (e) => { e.preventDefault(); setIsDragging(true); }; const onDragLeave = (e) => { e.preventDefault(); setIsDragging(false); }; const onDrop = (e) => { e.preventDefault(); setIsDragging(false); handleFiles(e.dataTransfer.files); }; const fileIcon = accept.includes('pdf') ? : ; return (
{preview ? ( /* ── Preview State ── */ {isImage({ type: accept.startsWith('image') ? 'image/jpeg' : '' }) || preview.match(/\.(jpg|jpeg|png|webp|gif)(\?|$)/i) ? ( Upload preview ) : (
File uploaded
)}
Uploaded
) : ( /* ── Drop Zone ── */ handleFiles(e.target.files)} disabled={uploading} />
{uploading ? ( ) : isDragging ? ( ) : ( fileIcon )}

{uploading ? 'Uploading…' : isDragging ? 'Drop to upload' : label}

{uploading ? '' : `Drag & drop or click to browse · Max ${maxMB}MB`}

{/* Progress bar */} {uploading && (
{progress}%
)}
)}
{/* Error message */} {error && ( {error} )}
); }