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