![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/public_html/src/components/ui/ |
import React, { useState, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Paperclip, X, FileText, Image } from 'lucide-react';
interface FileUploadProps {
onFileSelect: (file: File) => void;
onRemoveFile: () => void;
selectedFile: File | null;
uploading: boolean;
acceptedTypes?: string;
maxSizeMB?: number;
className?: string;
}
const FileUpload: React.FC<FileUploadProps> = ({
onFileSelect,
onRemoveFile,
selectedFile,
uploading,
acceptedTypes = "image/jpeg,image/jpg,image/png,image/gif,image/webp,image/bmp,image/svg+xml,application/pdf,.doc,.docx,.txt",
maxSizeMB = 10,
className = ''
}) => {
const [dragActive, setDragActive] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const validateFile = (file: File): string | null => {
// Check file size
const maxSizeBytes = maxSizeMB * 1024 * 1024;
if (file.size > maxSizeBytes) {
return `File size must be less than ${maxSizeMB}MB`;
}
// Define supported image formats
const imageTypes = [
'image/jpeg', 'image/jpg', 'image/png', 'image/gif',
'image/webp', 'image/bmp', 'image/svg+xml'
];
const documentTypes = [
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain'
];
const isImage = imageTypes.includes(file.type) ||
/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name);
const isDocument = documentTypes.includes(file.type) ||
/\.(pdf|doc|docx|txt)$/i.test(file.name);
if (!isImage && !isDocument) {
return `File type not supported. Please upload images (JPG, PNG, GIF, WebP, BMP, SVG) or documents (PDF, DOC, DOCX, TXT)`;
}
// Additional validation for images
if (isImage && file.size > 5 * 1024 * 1024) { // 5MB limit for images
return `Images must be less than 5MB. Use image compression if needed.`;
}
return null;
};
const handleFile = useCallback((file: File) => {
const validationError = validateFile(file);
if (validationError) {
setError(validationError);
return;
}
setError(null);
onFileSelect(file);
}, [onFileSelect, maxSizeMB, acceptedTypes]);
const handleDrag = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDragIn = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
setDragActive(true);
}
}, []);
const handleDragOut = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
handleFile(file);
}
}, [handleFile]);
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFile(file);
}
};
const handleButtonClick = () => {
fileInputRef.current?.click();
};
const 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];
};
const getFileIcon = (file: File) => {
if (file.type.startsWith('image/')) {
return <Image className="w-5 h-5" />;
}
return <FileText className="w-5 h-5" />;
};
return (
<div className={`relative ${className}`}>
{/* File Input */}
<input
ref={fileInputRef}
type="file"
accept={acceptedTypes}
onChange={handleFileInput}
className="hidden"
/>
{/* Upload Button */}
{!selectedFile && (
<button
type="button"
onClick={handleButtonClick}
disabled={uploading}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Attach file"
>
<Paperclip className="w-5 h-5" />
</button>
)}
{/* Selected File Preview */}
<AnimatePresence>
{selectedFile && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute bottom-full right-0 mb-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg p-3 min-w-72"
>
{/* Image preview for image files */}
{selectedFile.type.startsWith('image/') ? (
<div className="space-y-3">
<div className="relative">
<img
src={URL.createObjectURL(selectedFile)}
alt={selectedFile.name}
className="w-full max-w-64 max-h-32 object-cover rounded-lg border border-gray-200 dark:border-gray-600"
onLoad={(e) => {
// Clean up object URL after image loads
const img = e.target as HTMLImageElement;
setTimeout(() => URL.revokeObjectURL(img.src), 1000);
}}
/>
<button
onClick={onRemoveFile}
disabled={uploading}
className="absolute -top-2 -right-2 p-1 bg-red-500 hover:bg-red-600 text-white rounded-full shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Remove image"
>
<X className="w-3 h-3" />
</button>
</div>
<div className="text-center">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{selectedFile.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatFileSize(selectedFile.size)}
</p>
{uploading && (
<div className="mt-2">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div className="bg-blue-500 h-1.5 rounded-full animate-pulse w-1/2"></div>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Uploading...</p>
</div>
)}
</div>
</div>
) : (
<div className="flex items-start gap-3">
<div className="flex-shrink-0 text-gray-500 dark:text-gray-400">
{getFileIcon(selectedFile)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{selectedFile.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatFileSize(selectedFile.size)}
</p>
{uploading && (
<div className="mt-2">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div className="bg-blue-500 h-1.5 rounded-full animate-pulse w-1/2"></div>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Uploading...</p>
</div>
)}
</div>
<button
onClick={onRemoveFile}
disabled={uploading}
className="flex-shrink-0 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
title="Remove file"
>
<X className="w-4 h-4" />
</button>
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Error Message */}
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute bottom-full right-0 mb-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg shadow-lg p-3 min-w-72"
>
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
<button
onClick={() => setError(null)}
className="absolute top-2 right-2 p-1 text-red-400 hover:text-red-600 dark:hover:text-red-200"
>
<X className="w-4 h-4" />
</button>
</motion.div>
)}
</AnimatePresence>
{/* Drag Overlay */}
<AnimatePresence>
{dragActive && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-black/20 flex items-center justify-center"
onDragEnter={handleDragIn}
onDragLeave={handleDragOut}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<div className="bg-white dark:bg-gray-800 border-2 border-dashed border-blue-400 rounded-xl p-8 text-center max-w-md">
<Image className="w-12 h-12 text-blue-500 mx-auto mb-4" />
<p className="text-lg font-medium text-gray-900 dark:text-white">
Drop your file here
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
<strong>Images:</strong> JPG, PNG, GIF, WebP, BMP, SVG
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<strong>Documents:</strong> PDF, DOC, DOCX, TXT
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-2">
Maximum size: {maxSizeMB}MB
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default FileUpload;