"use client"; import * as React from "react"; import { useCallback } from "react"; import { useDropzone } from "react-dropzone"; import { cn } from "~/lib/utils"; import { Upload, FileText, X, CheckCircle, AlertCircle } from "lucide-react"; import { Button } from "./button"; interface FileUploadProps { onFilesSelected: (files: File[]) => void; accept?: Record; maxFiles?: number; maxSize?: number; className?: string; disabled?: boolean; placeholder?: string; description?: string; } interface FilePreviewProps { file: File; onRemove: () => void; status?: "success" | "error" | "pending"; error?: string; } function FilePreview({ file, onRemove, status = "pending", error }: FilePreviewProps) { const getStatusIcon = () => { switch (status) { case "success": return ; case "error": return ; default: return ; } }; const getStatusColor = () => { switch (status) { case "success": return "border-green-200 bg-green-50"; case "error": return "border-red-200 bg-red-50"; default: return "border-gray-200 bg-gray-50"; } }; return (
{getStatusIcon()}

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

{error && (

{error}

)}
); } export function FileUpload({ onFilesSelected, accept, maxFiles = 10, maxSize = 10 * 1024 * 1024, // 10MB default className, disabled = false, placeholder = "Drag & drop files here, or click to select", description }: FileUploadProps) { const [files, setFiles] = React.useState([]); const [errors, setErrors] = React.useState>({}); const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: any[]) => { // Handle accepted files const newFiles = [...files, ...acceptedFiles]; setFiles(newFiles); onFilesSelected(newFiles); // Handle rejected files const newErrors: Record = { ...errors }; rejectedFiles.forEach(({ file, errors }) => { const errorMessage = errors.map((e: any) => { if (e.code === 'file-too-large') { return `File is too large. Max size is ${(maxSize / 1024 / 1024).toFixed(1)}MB`; } if (e.code === 'file-invalid-type') { return 'File type not supported'; } if (e.code === 'too-many-files') { return `Too many files. Max is ${maxFiles}`; } return e.message; }).join(', '); newErrors[file.name] = errorMessage; }); setErrors(newErrors); }, [files, onFilesSelected, errors, maxFiles, maxSize]); const removeFile = (fileToRemove: File) => { const newFiles = files.filter(file => file !== fileToRemove); setFiles(newFiles); onFilesSelected(newFiles); const newErrors = { ...errors }; delete newErrors[fileToRemove.name]; setErrors(newErrors); }; const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({ onDrop, accept, maxFiles, maxSize, disabled }); return (

{isDragActive ? isDragReject ? "File type not supported" : "Drop files here" : placeholder }

{description && (

{description}

)}

Max {maxFiles} file{maxFiles !== 1 ? 's' : ''} • {(maxSize / 1024 / 1024).toFixed(1)}MB each

{/* File List */} {files.length > 0 && (

Selected Files

{files.map((file, index) => ( removeFile(file)} status={errors[file.name] ? "error" : "success"} error={errors[file.name]} /> ))}
)} {/* Error Summary */} {Object.keys(errors).length > 0 && (
Upload Errors
    {Object.entries(errors).map(([fileName, error]) => (
  • {fileName}: {error}
  • ))}
)}
); }