|
|
import React, { useCallback, useState } from 'react'; |
|
|
import { UploadCloud, FileUp } from 'lucide-react'; |
|
|
import { FileItem, UploadStatus } from '../types'; |
|
|
|
|
|
const generateId = () => Math.random().toString(36).substring(2, 15); |
|
|
|
|
|
interface FileUploaderProps { |
|
|
onFilesAdded: (files: FileItem[]) => void; |
|
|
disabled: boolean; |
|
|
} |
|
|
|
|
|
const sanitizeFileName = (fileName: string): string => { |
|
|
const timestamp = Date.now(); |
|
|
const lastDotIndex = fileName.lastIndexOf('.'); |
|
|
const name = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName; |
|
|
const ext = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : ''; |
|
|
|
|
|
let cleanName = name; |
|
|
cleanName = cleanName.replace(/^\d+[-_.\s]*/, ''); |
|
|
cleanName = cleanName.normalize("NFD").replace(/[\u0300-\u036f]/g, "") |
|
|
.replace(/đ/g, 'd').replace(/Đ/g, 'D'); |
|
|
cleanName = cleanName.replace(/[^a-zA-Z0-9]/g, '-'); |
|
|
cleanName = cleanName.replace(/-+/g, '-').replace(/^-|-$/g, ''); |
|
|
if (cleanName.length === 0) cleanName = 'file'; |
|
|
|
|
|
return `${timestamp}-${cleanName}${ext}`.toLowerCase(); |
|
|
}; |
|
|
|
|
|
export const FileUploader: React.FC<FileUploaderProps> = ({ onFilesAdded, disabled }) => { |
|
|
const [isDragging, setIsDragging] = useState(false); |
|
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent) => { |
|
|
e.preventDefault(); |
|
|
if (!disabled) setIsDragging(true); |
|
|
}, [disabled]); |
|
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => { |
|
|
e.preventDefault(); |
|
|
setIsDragging(false); |
|
|
}, []); |
|
|
|
|
|
const handleDrop = useCallback((e: React.DragEvent) => { |
|
|
e.preventDefault(); |
|
|
setIsDragging(false); |
|
|
if (disabled) return; |
|
|
|
|
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { |
|
|
processFiles(e.dataTransfer.files); |
|
|
} |
|
|
}, [disabled]); |
|
|
|
|
|
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
if (e.target.files && e.target.files.length > 0) { |
|
|
processFiles(e.target.files); |
|
|
e.target.value = ''; |
|
|
} |
|
|
}; |
|
|
|
|
|
const processFiles = (fileList: FileList) => { |
|
|
const newFiles: FileItem[] = Array.from(fileList).map(file => { |
|
|
const cleanPath = sanitizeFileName(file.name); |
|
|
return { |
|
|
id: generateId(), |
|
|
file, |
|
|
path: cleanPath, |
|
|
status: UploadStatus.IDLE |
|
|
}; |
|
|
}); |
|
|
onFilesAdded(newFiles); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div |
|
|
onDragOver={handleDragOver} |
|
|
onDragLeave={handleDragLeave} |
|
|
onDrop={handleDrop} |
|
|
className={` |
|
|
relative group border-2 border-dashed rounded-2xl p-10 text-center transition-all duration-300 ease-out overflow-hidden |
|
|
${disabled ? 'opacity-60 cursor-not-allowed border-gray-200 bg-gray-50' : 'cursor-pointer'} |
|
|
${isDragging |
|
|
? 'border-indigo-500 bg-indigo-50/50 scale-[1.01] shadow-lg' |
|
|
: 'border-gray-200 hover:border-indigo-400 hover:bg-gray-50'} |
|
|
`} |
|
|
> |
|
|
<input |
|
|
type="file" |
|
|
multiple |
|
|
onChange={handleFileInput} |
|
|
disabled={disabled} |
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed z-20" |
|
|
/> |
|
|
|
|
|
{/* Decorative Background Glow */} |
|
|
<div className={`absolute inset-0 bg-gradient-to-tr from-indigo-100/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none`} /> |
|
|
|
|
|
<div className="relative z-10 flex flex-col items-center justify-center space-y-4"> |
|
|
<div className={` |
|
|
p-5 rounded-2xl shadow-sm transition-all duration-300 |
|
|
${isDragging ? 'bg-indigo-100 text-indigo-600 scale-110' : 'bg-white border border-gray-100 text-gray-400 group-hover:text-indigo-500 group-hover:scale-105 group-hover:shadow-md'} |
|
|
`}> |
|
|
<UploadCloud className="w-10 h-10" strokeWidth={1.5} /> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<p className="text-xl font-semibold text-gray-700 group-hover:text-indigo-900 transition-colors"> |
|
|
{isDragging ? 'Drop files instantly' : 'Click or Drag files here'} |
|
|
</p> |
|
|
<p className="text-sm text-gray-400 mt-2 max-w-sm mx-auto"> |
|
|
Supports images, JSON, CSV, and Parquet. |
|
|
<span className="block mt-1 text-xs text-indigo-400 opacity-80">Files are auto-renamed with timestamps</span> |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
<div className="pt-2"> |
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-md bg-gray-100 text-gray-500 text-xs font-medium group-hover:bg-indigo-50 group-hover:text-indigo-600 transition-colors"> |
|
|
<FileUp className="w-3 h-3" /> |
|
|
Bulk Upload Supported |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |