import { useState, useRef } from "react"; import { UploadIcon } from "lucide-react"; import { Button } from "@vertesia/ui/core"; import { useUITranslation } from "@vertesia/ui/i18n"; /** * Props for the DropZone component */ export interface DropZoneProps { /** * Callback when files are dropped or selected */ onDrop: (files: File[], customMessage?: { count: number; message: string }) => void; /** * Message to display in the drop zone */ message: string; /** * Label for the upload button */ buttonLabel?: string; /** * Allow selection of folders * @default true */ allowFolders?: boolean; /** * CSS class to apply to the container */ className?: string; } /** * A reusable drop zone component for file uploads * * @example * */ export function DropZone({ onDrop, message, buttonLabel, allowFolders = true, className = "" }: DropZoneProps) { const { t } = useUITranslation(); const resolvedButtonLabel = buttonLabel ?? t('upload.uploadFiles'); const [isDragging, setIsDragging] = useState(false); const dropZoneRef = useRef(null); const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes('Files')) { setIsDragging(true); } }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); // Only set dragging to false if leaving the container (not entering a child) if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) { setIsDragging(false); } }; const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { // Handle folders using the DataTransferItemList API which supports directory reading const items = Array.from(e.dataTransfer.items); const files: File[] = []; const folders = new Set(); const processEntry = async (entry: any) => { if (entry.isFile) { // Get file const file = await new Promise((resolve) => { entry.file((file: File) => { // Store full path in the file object for location use Object.defineProperty(file, "webkitRelativePath", { writable: true, value: entry.fullPath.substring(1), // Remove leading slash }); resolve(file); }); }); // Skip hidden files if (!file.name.startsWith(".") && file.size > 0) { files.push(file); } // Add folder path to tracking const folderPath = entry.fullPath.substring(1).split("/").slice(0, -1).join("/"); if (folderPath) { folders.add(folderPath); } } else if (entry.isDirectory) { // Get folder reader const reader = entry.createReader(); const entries = await new Promise((resolve) => { reader.readEntries((entries: any[]) => { resolve(entries); }); }); // Process all entries await Promise.all(entries.map(processEntry)); // Add this folder to tracking const folderPath = entry.fullPath.substring(1); if (folderPath) { folders.add(folderPath); } } }; try { // Process all dropped items await Promise.all( items.map((item) => { const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : item; if (entry) { return processEntry(entry); } return Promise.resolve(); }), ); if (files.length > 0) { const topLevelFolders = new Set( Array.from(folders) .map((path) => path.split("/")[0]) .filter(Boolean), ); const folderCount = topLevelFolders.size; const fileCount = files.length; let message = ""; if (folderCount > 0) { message = t('upload.preparingFolder', { count: folderCount, folderCount, fileCount }); } else { message = t('upload.preparingFiles', { count: fileCount }); } onDrop(files, { count: files.length, message }); } } catch (error) { console.error("Error processing dropped files:", error); // Fallback to simple file array if folder processing fails const fileArray = Array.from(e.dataTransfer.files); if (fileArray.length > 0) { onDrop(fileArray); } } } else if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { // Fallback for browsers that don't support items API const fileArray = Array.from(e.dataTransfer.files); onDrop(fileArray); } }; const handleChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const fileArray = Array.from(e.target.files); // Check if there are directories (with webkitRelativePath) const hasDirectories = fileArray.some( (file) => (file as any).webkitRelativePath && (file as any).webkitRelativePath.includes("/"), ); if (hasDirectories) { // Count the unique top-level directories const topLevelDirs = new Set( fileArray.map((file) => (file as any).webkitRelativePath?.split("/")[0]).filter(Boolean), ); const folderCount = topLevelDirs.size; const fileCount = fileArray.length; // Create custom message with folder info const formattedMessage = t('upload.preparingFolder', { count: folderCount, folderCount, fileCount }); const customMessage = { count: fileArray.length, message: formattedMessage, }; onDrop(fileArray, customMessage); } else { // Regular file upload const feedback = { count: fileArray.length, message: t('upload.preparingFiles', { count: fileArray.length }), }; onDrop(fileArray, feedback); } } }; const selectFile = () => { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.multiple = true; fileInput.accept = "*"; fileInput?.click(); fileInput.onchange = (event) => { handleChange(event as unknown as React.ChangeEvent); }; }; const selectFolder = () => { const folderInput = document.createElement("input"); folderInput.type = "file"; folderInput.multiple = true; folderInput.accept = "*"; (folderInput as any).webkitdirectory = true; // webkitdirectory is not standard but widely supported folderInput?.click(); folderInput.onchange = (event) => { handleChange(event as unknown as React.ChangeEvent); }; }; return (

{message}

{allowFolders && ()}
); }