Files
breakpilot-core/pitch-deck/app/dataroom/page.tsx
T
Sharang Parnerkar f130c45ca8
Build pitch-deck / build-push-deploy (push) Successful in 1m47s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 39s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
feat(dataroom): bilingual descriptions, drag-drop multi-file upload, edit existing upload descriptions
- lib/translate.ts: LiteLLM DE<>EN translation utility
- Migration 006: description_de/description_en on both dataroom tables
- Admin + investor upload APIs: accept description+lang, auto-translate the other language on save
- PATCH /api/admin/dataroom/documents/[id]: description path in addition to display_name path
- PATCH /api/dataroom/uploads/[id]: investor can edit their own upload descriptions
- PATCH /api/admin/dataroom/investors/[id]/uploads: admin can edit investor upload descriptions
- All GET queries updated to return description fields
- Admin dataroom: drop zone replaces upload button, multi-file, inline description editor per doc and per investor upload
- Investor dataroom: drop zone, multi-file, description+lang textarea before upload, inline description editing on existing uploads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:00:36 +02:00

264 lines
13 KiB
TypeScript

'use client'
import { useEffect, useState, useRef, useCallback } from 'react'
import { FileText, Download, Upload, Eye, LogOut, Pencil, Globe } from 'lucide-react'
interface Doc {
id: string
filename: string
display_name: string | null
description_de: string | null
description_en: string | null
mime_type: string
file_size: number
released_at: string
}
interface MyUpload {
id: string
filename: string
display_name: string | null
description_de: string | null
description_en: string | null
mime_type: string
file_size: number
created_at: string
}
function fmt(bytes: number) {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
function isPDF(mime: string) { return mime === 'application/pdf' }
function LangToggle({ lang, onChange }: { lang: 'de' | 'en'; onChange: (l: 'de' | 'en') => void }) {
return (
<div className="flex rounded-lg overflow-hidden border border-white/10 text-xs shrink-0">
{(['de', 'en'] as const).map(l => (
<button key={l} onClick={() => onChange(l)}
className={`px-2 py-1 uppercase transition-colors ${lang === l ? 'bg-indigo-500/30 text-indigo-300' : 'text-white/40 hover:text-white/70'}`}>
{l}
</button>
))}
</div>
)
}
export default function DataroomPage() {
const [docs, setDocs] = useState<Doc[]>([])
const [uploads, setUploads] = useState<MyUpload[]>([])
const [dragging, setDragging] = useState(false)
const [uploading, setUploading] = useState(false)
const [toast, setToast] = useState<string | null>(null)
const [description, setDescription] = useState('')
const [descLang, setDescLang] = useState<'de' | 'en'>('en')
const [editingUpload, setEditingUpload] = useState<{ id: string; text: string; lang: 'de' | 'en' } | null>(null)
const fileRef = useRef<HTMLInputElement>(null)
function flash(msg: string) { setToast(msg); setTimeout(() => setToast(null), 3500) }
async function loadAll() {
const [dr, ur] = await Promise.all([fetch('/api/dataroom/documents'), fetch('/api/dataroom/uploads')])
if (dr.ok) setDocs((await dr.json()).documents)
if (ur.ok) setUploads((await ur.json()).uploads)
}
useEffect(() => { loadAll() }, [])
async function uploadFiles(files: FileList | File[]) {
const list = Array.from(files).filter(f => f.size > 0)
if (!list.length) return
setUploading(true)
const fd = new FormData()
list.forEach(f => fd.append('file', f))
if (description.trim()) { fd.append('description', description.trim()); fd.append('description_lang', descLang) }
const r = await fetch('/api/dataroom/uploads', { method: 'POST', body: fd })
setUploading(false)
if (r.ok) {
flash(`${list.length} file${list.length > 1 ? 's' : ''} uploaded`)
setDescription(''); loadAll()
} else {
const d = await r.json().catch(() => ({}))
flash(d.error || 'Upload failed')
}
}
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault(); setDragging(false)
if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files)
}, [description, descLang])
async function saveEditDescription() {
if (!editingUpload) return
const r = await fetch(`/api/dataroom/uploads/${editingUpload.id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: editingUpload.text || null, description_lang: editingUpload.lang }),
})
if (r.ok) {
const updated = (await r.json()).upload as MyUpload
setUploads(prev => prev.map(u => u.id === updated.id ? updated : u))
setEditingUpload(null); flash('Description saved & translated')
} else flash('Save failed')
}
return (
<div className="min-h-screen bg-[#0a0a1a] text-white">
<div className="border-b border-white/[0.06] px-6 py-4 flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Data Room</h1>
<p className="text-xs text-white/30 mt-0.5">BreakPilot ComplAI · Investor Portal</p>
</div>
<div className="flex items-center gap-3">
<a href="/" className="text-xs text-white/40 hover:text-white/70 transition-colors"> Back to pitch</a>
<a href="/api/auth/logout" className="text-xs text-white/40 hover:text-white/70 transition-colors flex items-center gap-1.5">
<LogOut className="w-3.5 h-3.5" /> Sign out
</a>
</div>
</div>
<div className="max-w-4xl mx-auto px-6 py-10 space-y-10">
{/* Released documents */}
<section>
<h2 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-4">Documents</h2>
{docs.length === 0 ? (
<div className="bg-white/[0.02] border border-dashed border-white/[0.08] rounded-2xl p-12 text-center">
<FileText className="w-8 h-8 text-white/20 mx-auto mb-3" />
<p className="text-white/30 text-sm">No documents have been shared with you yet.</p>
</div>
) : (
<div className="space-y-3">
{docs.map(doc => (
<div key={doc.id} className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-indigo-500/10 flex items-center justify-center shrink-0 mt-0.5">
<FileText className="w-5 h-5 text-indigo-400" />
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-white">{doc.display_name || doc.filename}</div>
<div className="text-xs text-white/40 mt-0.5">{fmt(doc.file_size)} · Released {new Date(doc.released_at).toLocaleDateString()}</div>
{(doc.description_en || doc.description_de) && (
<div className="mt-1.5 space-y-0.5">
{doc.description_en && <p className="text-xs text-white/40 leading-relaxed">{doc.description_en}</p>}
{doc.description_de && !doc.description_en && <p className="text-xs text-white/40 leading-relaxed">{doc.description_de}</p>}
{doc.description_de && doc.description_en && <p className="text-xs text-white/20 leading-relaxed flex gap-1"><Globe className="w-3 h-3 shrink-0 mt-0.5" />{doc.description_de}</p>}
</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{isPDF(doc.mime_type) && (
<a href={`/api/dataroom/documents/${doc.id}/download?preview=1`} target="_blank" rel="noopener noreferrer"
className="bg-white/[0.06] hover:bg-white/[0.1] text-white/70 text-xs px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors">
<Eye className="w-3.5 h-3.5" /> Preview
</a>
)}
<a href={`/api/dataroom/documents/${doc.id}/download`} download
className="bg-indigo-500/15 hover:bg-indigo-500/25 text-indigo-300 text-xs px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors">
<Download className="w-3.5 h-3.5" /> Download
</a>
</div>
</div>
))}
</div>
)}
</section>
{/* Upload section */}
<section>
<h2 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-4">Your Documents</h2>
<p className="text-xs text-white/30 mb-4">
Upload documents you want to share with us NDAs, term sheets, financial statements, or any other relevant files.
</p>
{/* Description field */}
<div className="flex gap-2 items-start mb-3">
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Optional description for uploaded files…"
rows={2}
className="flex-1 bg-white/[0.04] border border-white/[0.06] rounded-xl px-4 py-3 text-sm text-white placeholder-white/20 resize-none focus:outline-none focus:border-indigo-500/30"
/>
<LangToggle lang={descLang} onChange={setDescLang} />
</div>
{/* Drop zone */}
<div
onDragOver={e => { e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
onClick={() => fileRef.current?.click()}
className={`border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition-all select-none ${dragging ? 'border-indigo-400/60 bg-indigo-500/10' : 'border-white/[0.08] bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.03]'}`}
>
{uploading
? <p className="text-white/40 text-sm">Uploading</p>
: <>
<Upload className="w-7 h-7 text-white/20 mx-auto mb-3" />
<p className="text-white/50 text-sm font-medium">Drop files here</p>
<p className="text-white/25 text-xs mt-1">or click to browse · multiple files supported</p>
</>}
<input ref={fileRef} type="file" multiple className="hidden" onChange={e => { if (e.target.files) uploadFiles(e.target.files); e.target.value = '' }} />
</div>
{/* Uploaded files list */}
{uploads.length > 0 && (
<div className="mt-4 space-y-2">
{uploads.map(u => {
const isEditing = editingUpload?.id === u.id
return (
<div key={u.id} className="bg-white/[0.03] border border-white/[0.05] rounded-xl p-4 space-y-2">
<div className="flex items-center gap-3">
<FileText className="w-4 h-4 text-white/30 shrink-0" />
<div className="min-w-0 flex-1">
<div className="text-sm text-white/80 truncate">{u.display_name || u.filename}</div>
<div className="text-xs text-white/30 mt-0.5">{fmt(u.file_size)} · {new Date(u.created_at).toLocaleString()}</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={() => isEditing ? setEditingUpload(null) : setEditingUpload({ id: u.id, text: u.description_en || u.description_de || '', lang: u.description_en ? 'en' : 'de' })}
className="text-white/25 hover:text-white/60 transition-colors">
<Pencil className="w-3.5 h-3.5" />
</button>
<span className="text-xs text-emerald-400/70">Received</span>
</div>
</div>
{(u.description_en || u.description_de) && !isEditing && (
<div className="pl-7 space-y-0.5">
{u.description_en && <p className="text-xs text-white/30">{u.description_en}</p>}
{u.description_de && !u.description_en && <p className="text-xs text-white/30">{u.description_de}</p>}
</div>
)}
{isEditing && editingUpload && (
<div className="pl-7 space-y-2">
<div className="flex gap-2 items-start">
<textarea value={editingUpload.text} onChange={e => setEditingUpload(d => d ? { ...d, text: e.target.value } : d)}
rows={2} placeholder="Description…"
className="flex-1 bg-white/[0.04] border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-white/20 resize-none focus:outline-none focus:border-indigo-500/30" />
<LangToggle lang={editingUpload.lang} onChange={l => setEditingUpload(d => d ? { ...d, lang: l } : d)} />
</div>
<div className="flex gap-2">
<button onClick={saveEditDescription}
className="text-xs bg-indigo-500/20 hover:bg-indigo-500/30 text-indigo-300 px-3 py-1.5 rounded-lg">
Save & translate
</button>
<button onClick={() => setEditingUpload(null)} className="text-xs text-white/30 hover:text-white/60 px-2">Cancel</button>
</div>
</div>
)}
</div>
)
})}
</div>
)}
</section>
</div>
{toast && (
<div className="fixed bottom-6 right-6 bg-indigo-500/20 border border-indigo-500/40 text-indigo-200 text-sm px-4 py-2 rounded-lg backdrop-blur-sm z-50">
{toast}
</div>
)}
</div>
)
}