'use client'; import { useCallback, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { Upload, X, FileImage, FileVideo, FileAudio, File as FileIcon } from 'lucide-react'; import { clsx } from 'clsx'; interface FileUploadProps { onUpload?: (file: File) => void; onUploadMultiple?: (files: File[]) => void; accept?: Record; maxSize?: number; label?: string; currentFile?: File | null; onClear?: () => void; multiple?: boolean; className?: string; } const fileIcons: Record = { image: FileImage, video: FileVideo, audio: FileAudio, }; export default function FileUpload({ onUpload, onUploadMultiple, accept, maxSize = 100 * 1024 * 1024, // 100MB default label = 'Upload a file', currentFile, onClear, multiple = false, className, }: FileUploadProps) { const [error, setError] = useState(null); const onDrop = useCallback( (acceptedFiles: File[], rejectedFiles: any[]) => { setError(null); if (rejectedFiles.length > 0) { const rejection = rejectedFiles[0]; if (rejection.errors[0]?.code === 'file-too-large') { setError(`File too large. Max size is ${Math.round(maxSize / 1024 / 1024)}MB`); } else if (rejection.errors[0]?.code === 'file-invalid-type') { setError('Invalid file type'); } else { setError('File rejected'); } return; } if (acceptedFiles.length > 0) { if (onUploadMultiple) { onUploadMultiple(acceptedFiles); } else if (onUpload) { onUpload(acceptedFiles[0]); } } }, [onUpload, onUploadMultiple, maxSize] ); // Helper to validate mime type against accept prop const checkType = (file: File) => { if (!accept) return true; const fileType = file.type; const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`; // Iterate over accept keys (image/*, etc) for (const [mime, exts] of Object.entries(accept)) { if (mime === '*/*') return true; // Check wildcard if (mime.endsWith('/*')) { const baseMime = mime.split('/')[0]; if (fileType.startsWith(baseMime + '/')) return true; } // Check exact mime if (fileType === mime) return true; // Check extensions if (exts && exts.some(ext => ext.toLowerCase() === fileExt)) return true; } return false; }; const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept, maxSize, multiple: multiple || !!onUploadMultiple, }); const getFileIcon = (file: File) => { const type = file.type.split('/')[0]; const Icon = fileIcons[type] || FileIcon; return ; }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; }; if (currentFile) { return (
{getFileIcon(currentFile)}

{currentFile.name}

{formatFileSize(currentFile.size)}

{onClear && ( )}
); } return (
{ // Needed to allow dropping non-files (like our JSON asset) if (e.dataTransfer.types.includes('application/json')) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } }} onDrop={async (e) => { // Check if we dropped our custom asset const jsonData = e.dataTransfer.getData('application/json'); if (jsonData) { try { const data = JSON.parse(jsonData); if (data.type === 'forge-asset' && data.url) { e.preventDefault(); e.stopPropagation(); const res = await fetch(data.url); const blob = await res.blob(); const file = new File([blob], data.filename, { type: blob.type }); // Validate type if (!checkType(file)) { setError('Invalid file type for this tool'); return; } if (onUploadMultiple) { onUploadMultiple([file]); } else if (onUpload) { onUpload(file); } } } catch (err) { // Not our JSON, ignore } } }} >

{label}

Drag and drop or browse

{accept && (

Accepted: {Object.keys(accept).join(', ')}

)}
{error && (

{error}

)}
); }