Initial commit from remix
This commit is contained in:
163
src/components/dashboard/EnhancedFileUpload.tsx
Normal file
163
src/components/dashboard/EnhancedFileUpload.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user