"use client"; import * as React from "react"; import { useCallback } from "react"; import { useDropzone, type FileRejection } from "react-dropzone"; import { cn } from "~/lib/utils"; import { Upload, FileText, X, CheckCircle, AlertCircle } from "lucide-react"; import { Button } from "~/components/ui/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: FileRejection[]) => { // Handle accepted files const newFiles = [...files, ...acceptedFiles]; setFiles(newFiles); onFilesSelected(newFiles); // Handle rejected files const newErrors: Record = { ...errors }; rejectedFiles.forEach(({ file, errors: fileErrors }) => { const errorMessage = fileErrors .map((error) => { if (error.code === "file-too-large") { return `File is too large. Max size is ${(maxSize / 1024 / 1024).toFixed(1)}MB`; } if (error.code === "file-invalid-type") { return "File type not supported"; } if (error.code === "too-many-files") { return `Too many files. Max is ${maxFiles}`; } return error.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}
  • ))}
)}
); }