Spaces:
Running
Running
| import React, { useCallback, useState } from 'react'; | |
| import { UploadCloud } 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; | |
| } | |
| /** | |
| * Beautifies filenames: | |
| * 1. Prepends current TIMESTAMP (Date.now()) | |
| * 2. Converts Vietnamese/Accents to English (e.g., "tài liệu" -> "tai-lieu") | |
| * 3. Removes special chars and spaces | |
| * Format: [TIMESTAMP]-[clean-name].[ext] | |
| */ | |
| const sanitizeFileName = (fileName: string): string => { | |
| const timestamp = Date.now(); | |
| // 1. Separate extension | |
| const lastDotIndex = fileName.lastIndexOf('.'); | |
| const name = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName; | |
| const ext = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : ''; | |
| let cleanName = name; | |
| // 2. Remove EXISTING leading digits/timestamps to avoid double timestamps (e.g. 123_123_name) | |
| cleanName = cleanName.replace(/^\d+[-_.\s]*/, ''); | |
| // 3. Normalize Accents (Vietnamese, etc.) to ASCII | |
| cleanName = cleanName.normalize("NFD").replace(/[\u0300-\u036f]/g, "") | |
| .replace(/đ/g, 'd').replace(/Đ/g, 'D'); | |
| // 4. Replace non-alphanumeric characters with hyphens | |
| cleanName = cleanName.replace(/[^a-zA-Z0-9]/g, '-'); | |
| // 5. Collapse multiple hyphens and trim edges | |
| cleanName = cleanName.replace(/-+/g, '-').replace(/^-|-$/g, ''); | |
| // Fallback if name becomes empty | |
| if (cleanName.length === 0) cleanName = 'file'; | |
| // 6. Construct final name: timestamp-slug.ext | |
| 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 = ''; // Reset input | |
| } | |
| }; | |
| const processFiles = (fileList: FileList) => { | |
| const newFiles: FileItem[] = Array.from(fileList).map(file => { | |
| const cleanPath = sanitizeFileName(file.name); | |
| return { | |
| id: generateId(), | |
| file, | |
| path: cleanPath, // Auto-formatted path with timestamp | |
| status: UploadStatus.IDLE | |
| }; | |
| }); | |
| onFilesAdded(newFiles); | |
| }; | |
| return ( | |
| <div | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| className={` | |
| relative border-2 border-dashed rounded-xl p-8 text-center transition-all duration-200 | |
| ${disabled ? 'opacity-50 cursor-not-allowed border-gray-200 bg-gray-50' : 'cursor-pointer'} | |
| ${isDragging ? 'border-yellow-400 bg-yellow-50' : 'border-gray-300 hover:border-yellow-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" | |
| /> | |
| <div className="flex flex-col items-center justify-center space-y-3 pointer-events-none"> | |
| <div className={`p-4 rounded-full ${isDragging ? 'bg-yellow-100 text-yellow-600' : 'bg-gray-100 text-gray-500'}`}> | |
| <UploadCloud className="w-8 h-8" /> | |
| </div> | |
| <div> | |
| <p className="text-lg font-medium text-gray-700"> | |
| {isDragging ? 'Drop files here' : 'Drag & drop files or click to browse'} | |
| </p> | |
| <p className="text-sm text-gray-500 mt-1"> | |
| Files will be renamed: <code>timestamp-filename.ext</code> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; |