Spaces:
Sleeping
Sleeping
File size: 4,281 Bytes
16d4b20 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
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>
);
}; |