import { useEffect, useMemo, useRef, useState } from "react"; import { ActionIcon, Box, Button, Group, Stack, Text, ThemeIcon, UnstyledButton, } from "@mantine/core"; import { Eye, FileText, Trash2, UploadCloud } from "lucide-react"; import { isViewable } from "@edr/ui-common"; export interface PhasedFileDropzoneProps { label: string; description?: string; value: File | null; onChange: (file: File | null) => void; accept?: string; replaceMode?: boolean; onPreview?: (file: { name: string; url: string; mimeType?: string | null }) => void; disabled?: boolean; } function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`; } function isImageFile(file: File): boolean { if (file.type.startsWith("image/")) return true; const ext = file.name.split(".").pop()?.toLowerCase() ?? ""; return ["png", "jpg", "jpeg", "webp", "gif", "bmp", "svg"].includes(ext); } export function PhasedFileDropzone({ label, description, value, onChange, accept = "application/pdf,image/*", replaceMode = false, onPreview, disabled = false, }: PhasedFileDropzoneProps) { const inputRef = useRef(null); const [dragOver, setDragOver] = useState(false); const previewUrl = useMemo( () => (value ? URL.createObjectURL(value) : null), [value], ); useEffect(() => { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl); }; }, [previewUrl]); const pickFile = (file: File | null) => { if (disabled) return; onChange(file); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); if (disabled) return; const file = e.dataTransfer.files[0]; if (file) pickFile(file); }; if (value && previewUrl) { const canPreview = onPreview && isViewable({ name: value.name, url: previewUrl, mimeType: value.type }); const image = isImageFile(value); return ( {label} {image ? ( canPreview && onPreview?.({ name: value.name, url: previewUrl, mimeType: value.type }) } style={{ width: 52, height: 52, flexShrink: 0, borderRadius: 10, overflow: "hidden", border: "1px solid var(--mantine-color-gray-3)", cursor: canPreview ? "pointer" : "default", }} > ) : ( )} Ready to upload {value.name} {formatBytes(value.size)} {canPreview ? ( ) : null} pickFile(null)} disabled={disabled} > ); } return ( {label} {description ? ( {description} ) : null} { e.preventDefault(); if (!disabled) setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} onClick={() => !disabled && inputRef.current?.click()} style={{ borderRadius: 12, border: `2px dashed ${ dragOver ? "var(--mantine-color-edr-green-5)" : "var(--mantine-color-gray-4)" }`, background: dragOver ? "var(--mantine-color-edr-green-0)" : "var(--mantine-color-gray-0)", padding: "28px 20px", textAlign: "center", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.6 : 1, transition: "border-color 120ms ease, background 120ms ease", }} > pickFile(e.target.files?.[0] ?? null)} /> {dragOver ? "Drop to upload" : replaceMode ? "Drag & drop to replace" : "Drag & drop your file here"} or browse{" "} — PDF or image ); } export interface PhasedMultiFileDropzoneProps { label: string; description?: string; value: File[]; onChange: (files: File[]) => void; accept?: string; replaceMode?: boolean; disabled?: boolean; } export function PhasedMultiFileDropzone({ label, description, value, onChange, accept = "application/pdf,image/*", replaceMode = false, disabled = false, }: PhasedMultiFileDropzoneProps) { const inputRef = useRef(null); const [dragOver, setDragOver] = useState(false); const addFiles = (incoming: FileList | File[]) => { if (disabled) return; const next = [...value]; for (const file of Array.from(incoming)) { if (!next.some((f) => f.name === file.name && f.size === file.size)) { next.push(file); } } onChange(next); }; const removeAt = (index: number) => { if (disabled) return; onChange(value.filter((_, i) => i !== index)); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); if (e.dataTransfer.files.length > 0) addFiles(e.dataTransfer.files); }; return ( {label} {description ? ( {description} ) : null} {value.length > 0 ? ( {value.map((file, index) => ( Ready to upload {file.name} {formatBytes(file.size)} removeAt(index)} disabled={disabled} > ))} ) : null} { e.preventDefault(); if (!disabled) setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} onClick={() => !disabled && inputRef.current?.click()} style={{ borderRadius: 12, border: `2px dashed ${ dragOver ? "var(--mantine-color-edr-green-5)" : "var(--mantine-color-gray-4)" }`, background: dragOver ? "var(--mantine-color-edr-green-0)" : "var(--mantine-color-gray-0)", padding: "28px 20px", textAlign: "center", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.6 : 1, transition: "border-color 120ms ease, background 120ms ease", }} > { if (e.target.files?.length) addFiles(e.target.files); e.target.value = ""; }} /> {dragOver ? "Drop to add files" : replaceMode ? "Drag & drop to replace declaration files" : "Drag & drop declaration files here"} or{" "} browse {" "} — select one or more PDF or image files ); }