import { useRef, useState } from 'react'; import { UploadCloud } from 'lucide-react'; import { cn } from '@/lib/utils'; import { ConfirmModal } from '@/components/ui/modal'; import { toast } from '@/components/ui/toast-store'; import { uploadStackFile, UploadConflictError } from '@/lib/stackFilesApi'; const MAX_BYTES = 25 * 1024 * 1024; // 25 MB interface FileUploadDropzoneProps { stackName: string; currentDir: string; canEdit: boolean; onUploaded: () => void; } export function FileUploadDropzone({ stackName, currentDir, canEdit, onUploaded, }: FileUploadDropzoneProps) { const inputRef = useRef(null); const [isOver, setIsOver] = useState(false); const [conflict, setConflict] = useState(null); if (!canEdit) return null; const runUpload = async (file: File, overwrite: boolean): Promise => { const loadingId = toast.loading(`Uploading ${file.name}...`); try { await uploadStackFile(stackName, currentDir, file, { overwrite }); toast.success(overwrite ? 'Replaced.' : 'Uploaded.'); onUploaded(); } catch (e: unknown) { if (e instanceof UploadConflictError) { setConflict(file); return; } toast.error(e instanceof Error ? e.message : 'Upload failed.'); } finally { toast.dismiss(loadingId); } }; const handleFile = (file: File) => { if (file.size > MAX_BYTES) { toast.error('File exceeds 25 MB.'); return; } void runUpload(file, false); }; const handleChange = (ev: React.ChangeEvent) => { const file = ev.target.files?.[0]; if (file) handleFile(file); ev.target.value = ''; }; const handleDragOver = (ev: React.DragEvent) => { if (!ev.dataTransfer.types.includes('Files')) return; ev.preventDefault(); ev.dataTransfer.dropEffect = 'copy'; if (!isOver) setIsOver(true); }; const handleDragLeave = (ev: React.DragEvent) => { // Ignore leave events that bubble from child elements still within the // dropzone (relatedTarget is contained by currentTarget); only react when // the cursor truly leaves the zone. const next = ev.relatedTarget; if (next instanceof Node && ev.currentTarget.contains(next)) return; setIsOver(false); }; const handleDrop = (ev: React.DragEvent) => { ev.preventDefault(); setIsOver(false); const files = ev.dataTransfer.files; if (!files || files.length === 0) return; if (files.length > 1) { toast.error('Drop one file at a time.'); return; } void handleFile(files[0]); }; return ( <>
inputRef.current?.click()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); inputRef.current?.click(); } }} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} > {isOver ? 'Drop to upload' : 'Upload or drop file'}
{ if (!next) setConflict(null); }} onCancel={() => setConflict(null)} kicker="FILES ยท REPLACE EXISTING" title="Replace existing file?" description={conflict ? `${conflict.name} already exists in this folder.` : ''} confirmLabel="Replace" onConfirm={() => { const file = conflict; setConflict(null); if (file) void runUpload(file, true); }} >

{conflict?.name ?? 'The file'} already exists. Replacing it overwrites the current contents and cannot be undone.

); }