SocialShare / frontend /src /components /common /FileUpload.jsx
NitinBot002's picture
Initial commit with all project files
f4854a1
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') ? <HiDocumentText /> : <HiPhotograph />;
return (
<div className={`fu-root ${className}`}>
<AnimatePresence mode="wait">
{preview ? (
/* ── Preview State ── */
<motion.div
key="preview"
className="fu-preview"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
>
{isImage({ type: accept.startsWith('image') ? 'image/jpeg' : '' }) || preview.match(/\.(jpg|jpeg|png|webp|gif)(\?|$)/i) ? (
<img src={preview} alt="Upload preview" className="fu-preview__img" />
) : (
<div className="fu-preview__doc">
<HiDocumentText className="fu-preview__doc-icon" />
<span>File uploaded</span>
</div>
)}
<button
type="button"
className="fu-preview__remove"
onClick={handleRemove}
title="Remove file"
>
<HiX />
</button>
<div className="fu-preview__success">
<HiCheckCircle /> Uploaded
</div>
</motion.div>
) : (
/* ── Drop Zone ── */
<motion.label
key="dropzone"
className={`fu-zone ${isDragging ? 'fu-zone--dragging' : ''} ${uploading ? 'fu-zone--uploading' : ''}`}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
whileHover={{ scale: 1.01 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<input
type="file"
accept={accept}
multiple={multiple}
className="fu-input"
onChange={(e) => handleFiles(e.target.files)}
disabled={uploading}
/>
<div className="fu-zone__icon">
{uploading ? (
<HiArrowPath className="spin-icon" />
) : isDragging ? (
<HiUpload />
) : (
fileIcon
)}
</div>
<p className="fu-zone__label">
{uploading ? 'Uploading…' : isDragging ? 'Drop to upload' : label}
</p>
<p className="fu-zone__hint">
{uploading ? '' : `Drag & drop or click to browse · Max ${maxMB}MB`}
</p>
{/* Progress bar */}
{uploading && (
<div className="fu-progress">
<motion.div
className="fu-progress__bar"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ ease: 'easeOut' }}
/>
<span className="fu-progress__label">{progress}%</span>
</div>
)}
</motion.label>
)}
</AnimatePresence>
{/* Error message */}
<AnimatePresence>
{error && (
<motion.p
className="fu-error"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
>
<HiExclamationCircle /> {error}
</motion.p>
)}
</AnimatePresence>
</div>
);
}