import { useState, useRef, useCallback } from 'react'; import type { FileUploadProps } from './types'; import type { FileWithPreview } from './types'; import { formatFileSize } from './utils'; import { createPreview } from './utils'; export function useFileUpload({ onFileSelect, accept, multiple = false, maxSize, showPreview = true, disabled = false, }: FileUploadProps) { const [dragActive, setDragActive] = useState(false); const [files, setFiles] = useState([]); const [errors, setErrors] = useState([]); const fileInputRef = useRef(null); const validateFile = useCallback( (file: File): string | null => { if (accept) { const acceptedTypes = accept.split(',').map((t) => t.trim()); const fileExtension = `.${file.name.split('.').pop()?.toLowerCase()}`; const fileType = file.type.toLowerCase(); const isAccepted = acceptedTypes.some((type) => { if (type.startsWith('.')) return type.toLowerCase() === fileExtension; if (type.includes('/')) { return ( fileType === type.toLowerCase() || fileType.startsWith(`${type.toLowerCase().split('/')[0]}/`) ); } return false; }); if (!isAccepted) { return `File type ${file.type || 'unknown'} is not allowed. Accepted types: ${accept}`; } } if (maxSize && file.size > maxSize) { return `File size ${formatFileSize(file.size)} exceeds maximum size ${formatFileSize(maxSize)}`; } return null; }, [accept, maxSize], ); const processFiles = useCallback( async (fileList: File[]) => { const newErrors: string[] = []; const validFiles: FileWithPreview[] = []; const filesToProcess: File[] = []; fileList.forEach((file) => { const error = validateFile(file); if (error) newErrors.push(`${file.name}: ${error}`); else filesToProcess.push(file); }); for (const file of filesToProcess) { const fileWithPreview = file as FileWithPreview; fileWithPreview.status = 'pending'; fileWithPreview.progress = 0; if (showPreview) { const preview = await createPreview(file); if (preview) fileWithPreview.preview = preview; } validFiles.push(fileWithPreview); } setErrors(newErrors); if (validFiles.length > 0) { const updatedFiles = multiple ? [...files, ...validFiles] : validFiles; setFiles(updatedFiles); onFileSelect(updatedFiles.slice() as File[]); } }, [files, multiple, showPreview, onFileSelect, validateFile], ); const handleDrag = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === 'dragenter' || e.type === 'dragover') setDragActive(true); else if (e.type === 'dragleave') setDragActive(false); }, []); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); if (disabled) return; if (e.dataTransfer.files?.length) { processFiles(Array.from(e.dataTransfer.files)); } }, [disabled, processFiles], ); const handleFileInput = useCallback( (e: React.ChangeEvent) => { if (e.target.files?.length) { processFiles(Array.from(e.target.files)); if (fileInputRef.current) fileInputRef.current.value = ''; } }, [processFiles], ); const handleRemoveFile = useCallback( (index: number) => { const updatedFiles = files.filter((_, i) => i !== index); setFiles(updatedFiles); onFileSelect( updatedFiles.map((f) => { const { preview, status, progress, error, ...file } = f; return file as File; }), ); }, [files, onFileSelect], ); const handleClick = useCallback(() => { if (!disabled && fileInputRef.current) fileInputRef.current.click(); }, [disabled]); return { dragActive, files, errors, fileInputRef, accept, multiple, maxSize, showPreview, disabled, handleDrag, handleDrop, handleFileInput, handleRemoveFile, handleClick, }; }