| 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> |
| ); |
| }; |