testupload / components /FileUploader.tsx
Twan07's picture
Upload 5 files
16d4b20 verified
raw
history blame
4.28 kB
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>
);
};