import { type ComponentPropsWithoutRef, memo, type ReactNode, useCallback, useMemo } from "react"; import { twMerge } from "tailwind-merge"; import { useRenderCount } from "ui/hooks/use-render-count"; import { TbDots, TbExternalLink, TbFileTypeCsv, TbFileText, TbJson, TbFileTypePdf, TbMarkdown, TbMusic, TbTrash, TbUpload, TbFileTypeTxt, TbFileTypeXml, TbZip, TbFileTypeSql, } from "react-icons/tb"; import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown"; import { IconButton } from "ui/components/buttons/IconButton"; import { formatNumber } from "bknd/utils"; import type { DropzoneRenderProps, FileState } from "./Dropzone"; import { useDropzoneFileState, useDropzoneState } from "./Dropzone"; function handleUploadError(e: unknown) { if (e && e instanceof XMLHttpRequest) { const res = JSON.parse(e.responseText) as any; alert(`Upload failed with code ${e.status}: ${res.error}`); } else { alert("Upload failed"); } } export const DropzoneInner = ({ wrapperRef, inputProps, showPlaceholder, actions: { uploadFile, deleteFile, openFileInput }, dropzoneProps: { placeholder, flow, maxItems, allowedMimeTypes }, onClick, footer, }: DropzoneRenderProps) => { const { files, isOver, isOverAccepted } = useDropzoneState(); const Placeholder = showPlaceholder && ( ); const uploadHandler = useCallback( async (file: { path: string }) => { try { return await uploadFile(file); } catch (e) { handleUploadError(e); } }, [uploadFile], ); return (
{flow === "start" && Placeholder} {files.map((file) => ( ))} {flow === "end" && Placeholder} {footer}
); }; const UploadPlaceholder = ({ onClick, text = "Upload files" }) => { return (
{text}
); }; type ReducedFile = Omit; export type PreviewComponentProps = { file: ReducedFile; fallback?: (props: { file: ReducedFile }) => ReactNode; className?: string; onClick?: () => void; onTouchStart?: () => void; }; const Wrapper = ({ file, fallback, ...props }: PreviewComponentProps) => { if (file.type.startsWith("image/")) { return ; } if (file.type.startsWith("video/")) { return ; } return fallback ? fallback({ file }) : null; }; export const PreviewWrapperMemoized = memo( Wrapper, (prev, next) => prev.file.path === next.file.path, ); type PreviewProps = { file: FileState; handleUpload: (file: FileState) => Promise; handleDelete: (file: FileState) => Promise; onClick?: (file: { path: string }) => void; }; const Preview = memo( ({ file: _file, handleUpload, handleDelete, onClick }: PreviewProps) => { const rcount = useRenderCount(); const file = useDropzoneFileState(_file, (file) => { const { progress, ...rest } = file; return rest; }); if (!file) return null; const onClickHandler = useCallback(() => { if (onClick) { onClick(file); } }, [onClick, file.path]); return (

{file.name}

{file.type} {formatNumber.fileSize(file.size)}
); }, (prev, next) => prev.file.path === next.file.path && prev.file.state === next.file.state, ); const PreviewUploadProgress = ({ file: _file }: { file: { path: string } }) => { const fileState = useDropzoneFileState(_file.path, (file) => ({ state: file.state, progress: file.progress, })); if (!fileState) return null; if (fileState.state !== "uploading") return null; return (
); }; const PreviewDropdown = memo( ({ file: _file, handleDelete, handleUpload, }: { file: FileState; handleDelete: (file: FileState) => Promise; handleUpload: (file: FileState) => Promise; }) => { const file = useDropzoneFileState(_file.path, (file) => { const { progress, ...rest } = file; return rest; }); if (!file) return null; const dropdownItems = useMemo( () => [ file.state === "uploaded" && typeof file.body === "string" && { label: "Open", icon: TbExternalLink, onClick: () => { window.open(file.body as string, "_blank"); }, }, ["initial", "uploaded"].includes(file.state) && { label: "Delete", destructive: true, icon: TbTrash, onClick: () => handleDelete(file as any), }, ["initial", "pending"].includes(file.state) && { label: "Upload", icon: TbUpload, onClick: () => handleUpload(file as any), }, ] satisfies (DropdownItem | boolean)[], [file, handleDelete, handleUpload], ); return ( ); }, (prev, next) => prev.file.path === next.file.path, ); const StateIndicator = ({ file: _file }: { file: { path: string } }) => { const fileState = useDropzoneFileState(_file.path, (file) => file.state); if (!fileState) return null; if (fileState === "uploaded") { return null; } const color = { failed: "bg-red-500", deleting: "bg-orange-500 animate-pulse", uploading: "bg-blue-500 animate-pulse", }[fileState] ?? "bg-primary/50"; return
; }; const ImagePreview = ({ file, ...props }: { file: ReducedFile } & ComponentPropsWithoutRef<"img">) => { const objectUrl = typeof file.body === "string" ? file.body : URL.createObjectURL(file.body); return ; }; const VideoPreview = ({ file, ...props }: { file: ReducedFile } & ComponentPropsWithoutRef<"video">) => { const objectUrl = typeof file.body === "string" ? file.body : URL.createObjectURL(file.body); return