'use client'; import { useCallback, useRef, useState } from 'react'; import { Upload, X } from 'lucide-react'; import { cn } from '@/lib/utils'; interface UploadingFile { id: string; name: string; progress: number; error?: string; } interface FileUploadZoneProps { entityType?: string; entityId?: string; clientId?: string; yachtId?: string; companyId?: string; /** * Optional folder to deposit the file into. Hub uploads pass the * currently-selected folderId so files land where the user expects. */ folderId?: string | null; /** * Fires per successful upload with the file metadata. The wizard / * inline-upload flows use the returned id to wire follow-up actions * (e.g. set as the source PDF for a Documenso signing flow). */ onUploadComplete?: (file?: { id: string; filename?: string }) => void; } export function FileUploadZone({ entityType, entityId, clientId, yachtId, companyId, folderId, onUploadComplete, }: FileUploadZoneProps) { const [isDragOver, setIsDragOver] = useState(false); const [uploading, setUploading] = useState([]); const inputRef = useRef(null); const uploadFiles = useCallback( async (fileList: FileList) => { const newUploads: UploadingFile[] = Array.from(fileList).map((f) => ({ id: crypto.randomUUID(), name: f.name, progress: 0, })); setUploading((prev) => [...prev, ...newUploads]); await Promise.all( Array.from(fileList).map(async (file, i) => { const uploadId = newUploads[i]!.id; try { const formData = new FormData(); formData.append('file', file); formData.append('filename', file.name); if (clientId) formData.append('clientId', clientId); if (yachtId) formData.append('yachtId', yachtId); if (companyId) formData.append('companyId', companyId); if (entityType) formData.append('entityType', entityType); if (entityId) formData.append('entityId', entityId); if (folderId) formData.append('folderId', folderId); setUploading((prev) => prev.map((u) => (u.id === uploadId ? { ...u, progress: 50 } : u)), ); // Use fetch directly for FormData (apiFetch JSON-encodes body) const portId = (await import('@/stores/ui-store')).useUIStore.getState().currentPortId; const headers = new Headers(); if (portId) headers.set('X-Port-Id', portId); const uploadRes = await fetch('/api/v1/files/upload', { method: 'POST', headers, credentials: 'include', body: formData, }); if (!uploadRes.ok) { throw new Error('Upload failed'); } const uploadJson = (await uploadRes.json().catch(() => null)) as { data?: { id?: string; filename?: string }; } | null; if (uploadJson?.data?.id) { onUploadComplete?.({ id: uploadJson.data.id, filename: uploadJson.data.filename, }); } setUploading((prev) => prev.map((u) => (u.id === uploadId ? { ...u, progress: 100 } : u)), ); } catch { setUploading((prev) => prev.map((u) => (u.id === uploadId ? { ...u, error: 'Upload failed' } : u)), ); } }), ); // Clear completed uploads after a moment setTimeout(() => { setUploading((prev) => prev.filter((u) => u.error)); onUploadComplete?.(); }, 1500); }, [clientId, yachtId, companyId, entityType, entityId, folderId, onUploadComplete], ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); if (e.dataTransfer.files.length > 0) { void uploadFiles(e.dataTransfer.files); } }, [uploadFiles], ); const handleChange = useCallback( (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { void uploadFiles(e.target.files); e.target.value = ''; } }, [uploadFiles], ); return (
{ e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={handleDrop} onClick={() => inputRef.current?.click()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') inputRef.current?.click(); }} >

Drop files here or click to upload

PDF, Word, Excel, images up to 50MB

{uploading.length > 0 && (
{uploading.map((u) => (
{u.name} {u.error ? ( {u.error} ) : (
{u.progress}%
)} {u.error && ( )}
))}
)}
); }