"use client"; import { UploadIcon } from "lucide-react"; import type { ReactNode } from "react"; import { createContext, useContext } from "react"; import type { DropEvent, DropzoneOptions, FileRejection } from "react-dropzone"; import { useDropzone } from "react-dropzone"; import { Button } from "@/src/components/ui/button"; import { cn } from "@/src/utils/tailwind"; type DropzoneContextType = { src?: File[]; accept?: DropzoneOptions["accept"]; maxSize?: DropzoneOptions["maxSize"]; minSize?: DropzoneOptions["minSize"]; maxFiles?: DropzoneOptions["maxFiles"]; }; const renderBytes = (bytes: number) => { const units = ["B", "KB", "MB", "GB", "TB", "PB"]; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)}${units[unitIndex]}`; }; const DropzoneContext = createContext( undefined, ); export type DropzoneProps = Omit & { src?: File[]; className?: string; onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent, ) => void; children?: ReactNode; }; export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props }: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message; onError?.(new Error(message)); return; } onDrop?.(acceptedFiles, fileRejections, event); }, ...props, }); return ( ); }; const useDropzoneContext = () => { const context = useContext(DropzoneContext); if (!context) { throw new Error("useDropzoneContext must be used within a Dropzone"); } return context; }; export type DropzoneContentProps = { children?: ReactNode; className?: string; }; const maxLabelItems = 3; export const DropzoneContent = ({ children, className, }: DropzoneContentProps) => { const { src } = useDropzoneContext(); if (!src) { return null; } if (children) { return children; } return (

{src.length > maxLabelItems ? `${new Intl.ListFormat("en").format( src.slice(0, maxLabelItems).map((file) => file.name), )} and ${src.length - maxLabelItems} more` : new Intl.ListFormat("en").format(src.map((file) => file.name))}

Drag and drop or click to replace

); }; export type DropzoneEmptyStateProps = { children?: ReactNode; className?: string; }; export const DropzoneEmptyState = ({ children, className, }: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext(); if (src) { return null; } if (children) { return children; } let caption = ""; if (accept) { caption += "Accepts "; caption += new Intl.ListFormat("en").format(Object.keys(accept)); } if (minSize && maxSize) { caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`; } else if (minSize) { caption += ` at least ${renderBytes(minSize)}`; } else if (maxSize) { caption += ` less than ${renderBytes(maxSize)}`; } return (

Upload {maxFiles === 1 ? "a file" : "files"}

Drag and drop or click to upload

{caption && (

{caption}.

)}
); };