Files
karibeo_backend_admin/src/components/dashboard/EnhancedFileUpload.tsx
gpt-engineer-app[bot] 5ddc52658d Initial commit from remix
2025-09-25 16:01:00 +00:00

163 lines
5.6 KiB
TypeScript

import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, X, Image, File } from 'lucide-react';
interface EnhancedFileUploadProps {
onFilesChange: (files: File[]) => void;
maxFiles?: number;
maxSize?: number; // in bytes
accept?: { [key: string]: string[] };
className?: string;
}
const EnhancedFileUpload: React.FC<EnhancedFileUploadProps> = ({
onFilesChange,
maxFiles = 5,
maxSize = 10 * 1024 * 1024, // 10MB
accept = {
'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'],
'application/pdf': ['.pdf'],
'text/*': ['.txt', '.doc', '.docx']
},
className = ''
}) => {
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({});
const onDrop = useCallback(async (acceptedFiles: File[]) => {
setUploading(true);
const newFiles = [...uploadedFiles, ...acceptedFiles].slice(0, maxFiles);
setUploadedFiles(newFiles);
onFilesChange(newFiles);
// Simulate upload progress
for (const file of acceptedFiles) {
for (let progress = 0; progress <= 100; progress += 10) {
setUploadProgress(prev => ({ ...prev, [file.name]: progress }));
await new Promise(resolve => setTimeout(resolve, 100));
}
}
setUploading(false);
setUploadProgress({});
}, [uploadedFiles, maxFiles, onFilesChange]);
const removeFile = (fileToRemove: File) => {
const newFiles = uploadedFiles.filter(file => file !== fileToRemove);
setUploadedFiles(newFiles);
onFilesChange(newFiles);
};
const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({
onDrop,
accept,
maxSize,
maxFiles: maxFiles - uploadedFiles.length,
disabled: uploading
});
const formatFileSize = (bytes: number) => {
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 getFileIcon = (file: File) => {
if (file.type.startsWith('image/')) {
return <Image className="w-5 h-5" />;
}
return <File className="w-5 h-5" />;
};
return (
<div className={`file-upload-enhanced ${className}`}>
{/* Dropzone */}
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-xl p-6 text-center transition-all cursor-pointer ${
isDragActive
? 'border-primary bg-primary/5'
: 'border-gray-300 hover:border-primary hover:bg-primary/5'
} ${uploading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<input {...getInputProps()} />
<div className="space-y-4">
<div className="mx-auto w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
<Upload className="w-8 h-8 text-primary" />
</div>
<div>
<h6 className="font-semibold text-gray-800 mb-2">
{isDragActive ? 'Drop files here' : 'Drag & drop files here'}
</h6>
<p className="text-gray-600 text-sm mb-3">
or <span className="text-primary font-medium">browse files</span>
</p>
<p className="text-xs text-gray-500">
Max {maxFiles} files, up to {formatFileSize(maxSize)} each
</p>
</div>
</div>
</div>
{/* File Rejections */}
{fileRejections.length > 0 && (
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
<h6 className="text-red-800 font-medium mb-1">Upload Errors:</h6>
{fileRejections.map(({ file, errors }) => (
<div key={file.name} className="text-sm text-red-600">
{file.name}: {errors.map(e => e.message).join(', ')}
</div>
))}
</div>
)}
{/* Uploaded Files List */}
{uploadedFiles.length > 0 && (
<div className="mt-4 space-y-2">
<h6 className="font-medium text-gray-800">Uploaded Files ({uploadedFiles.length})</h6>
{uploadedFiles.map((file, index) => (
<div key={`${file.name}-${index}`} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<div className="text-primary">
{getFileIcon(file)}
</div>
<div>
<p className="font-medium text-gray-800 text-sm">{file.name}</p>
<p className="text-xs text-gray-500">{formatFileSize(file.size)}</p>
</div>
</div>
<div className="flex items-center space-x-2">
{uploadProgress[file.name] !== undefined && (
<div className="w-20 bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${uploadProgress[file.name]}%` }}
></div>
</div>
)}
<button
onClick={() => removeFile(file)}
className="p-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded"
disabled={uploading}
>
<X className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
};
export default EnhancedFileUpload;