feat(dataroom): bilingual descriptions, drag-drop multi-file upload, edit existing upload descriptions
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
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
- 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>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { Upload, FileText, Trash2, X, Share2, Users, ChevronDown, Check, Download } from 'lucide-react'
|
||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { FileText, Trash2, X, Share2, Users, Check, Download, Pencil, Globe } from 'lucide-react'
|
||||
|
||||
interface Doc {
|
||||
id: string
|
||||
filename: string
|
||||
display_name: string
|
||||
description_de: string | null
|
||||
description_en: string | null
|
||||
mime_type: string
|
||||
file_size: number
|
||||
uploaded_by: string
|
||||
@@ -37,6 +39,8 @@ interface InvestorUpload {
|
||||
id: string
|
||||
filename: string
|
||||
display_name: string
|
||||
description_de: string | null
|
||||
description_en: string | null
|
||||
mime_type: string
|
||||
file_size: number
|
||||
created_at: string
|
||||
@@ -48,22 +52,35 @@ function fmt(bytes: number) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
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 [investors, setInvestors] = useState<Investor[]>([])
|
||||
const [selected, setSelected] = useState<Doc | null>(null)
|
||||
const [releases, setReleases] = useState<Release[]>([])
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [toast, setToast] = useState<string | null>(null)
|
||||
const [tab, setTab] = useState<'documents' | 'uploads'>('documents')
|
||||
const [investorUploads, setInvestorUploads] = useState<Record<string, InvestorUpload[]>>({})
|
||||
const [descEdit, setDescEdit] = useState<{ text: string; lang: 'de' | 'en' } | null>(null)
|
||||
const [editingUpload, setEditingUpload] = useState<{ invId: string; uploadId: string; text: string; lang: 'de' | 'en' } | null>(null)
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
function flash(msg: string) {
|
||||
setToast(msg)
|
||||
setTimeout(() => setToast(null), 3000)
|
||||
}
|
||||
function flash(msg: string) { setToast(msg); setTimeout(() => setToast(null), 3000) }
|
||||
|
||||
async function loadDocs() {
|
||||
const r = await fetch('/api/admin/dataroom/documents')
|
||||
@@ -96,23 +113,27 @@ export default function DataroomPage() {
|
||||
useEffect(() => { if (tab === 'uploads' && investors.length > 0) loadInvestorUploads() }, [tab, investors])
|
||||
|
||||
async function selectDoc(doc: Doc) {
|
||||
setSelected(doc)
|
||||
setSelected(doc); setDescEdit(null)
|
||||
await loadReleases(doc.id)
|
||||
}
|
||||
|
||||
async function uploadFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
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()
|
||||
fd.append('file', file)
|
||||
list.forEach(f => fd.append('file', f))
|
||||
const r = await fetch('/api/admin/dataroom/documents', { method: 'POST', body: fd })
|
||||
setUploading(false)
|
||||
if (r.ok) { flash('Uploaded'); loadDocs() }
|
||||
if (r.ok) { flash(`Uploaded ${list.length} file${list.length > 1 ? 's' : ''}`); loadDocs() }
|
||||
else { const d = await r.json().catch(() => ({})); flash(d.error || 'Upload failed') }
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault(); setDragging(false)
|
||||
if (e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files)
|
||||
}, [])
|
||||
|
||||
async function deleteDoc(id: string) {
|
||||
if (!confirm('Delete this document? All releases will be removed.')) return
|
||||
setBusy(true)
|
||||
@@ -129,27 +150,55 @@ export default function DataroomPage() {
|
||||
await fetch(`/api/admin/dataroom/documents/${selected.id}/release/${investorId}`, { method: 'DELETE' })
|
||||
} else {
|
||||
await fetch(`/api/admin/dataroom/documents/${selected.id}/release`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ investor_ids: [investorId] }),
|
||||
})
|
||||
}
|
||||
setBusy(false)
|
||||
await loadReleases(selected.id)
|
||||
loadDocs()
|
||||
setBusy(false); await loadReleases(selected.id); loadDocs()
|
||||
}
|
||||
|
||||
async function releaseAll() {
|
||||
if (!selected || investors.length === 0) return
|
||||
setBusy(true)
|
||||
await fetch(`/api/admin/dataroom/documents/${selected.id}/release`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ investor_ids: investors.map(i => i.id) }),
|
||||
})
|
||||
setBusy(false); await loadReleases(selected.id); loadDocs()
|
||||
}
|
||||
|
||||
async function saveDescription() {
|
||||
if (!selected || !descEdit) return
|
||||
setBusy(true)
|
||||
const r = await fetch(`/api/admin/dataroom/documents/${selected.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: descEdit.text || null, description_lang: descEdit.lang }),
|
||||
})
|
||||
setBusy(false)
|
||||
await loadReleases(selected.id)
|
||||
loadDocs()
|
||||
if (r.ok) {
|
||||
const updated = (await r.json()).document as Doc
|
||||
setDocs(prev => prev.map(d => d.id === updated.id ? { ...d, ...updated } : d))
|
||||
setSelected(prev => prev ? { ...prev, ...updated } : prev)
|
||||
setDescEdit(null); flash('Description saved & translated')
|
||||
} else flash('Save failed')
|
||||
}
|
||||
|
||||
async function saveUploadDescription() {
|
||||
if (!editingUpload) return
|
||||
setBusy(true)
|
||||
const r = await fetch(`/api/admin/dataroom/investors/${editingUpload.invId}/uploads`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ upload_id: editingUpload.uploadId, description: editingUpload.text || null, description_lang: editingUpload.lang }),
|
||||
})
|
||||
setBusy(false)
|
||||
if (r.ok) {
|
||||
const updated = (await r.json()).upload as InvestorUpload
|
||||
setInvestorUploads(prev => ({
|
||||
...prev,
|
||||
[editingUpload.invId]: (prev[editingUpload.invId] || []).map(u => u.id === updated.id ? updated : u),
|
||||
}))
|
||||
setEditingUpload(null); flash('Description saved & translated')
|
||||
} else flash('Save failed')
|
||||
}
|
||||
|
||||
const releasedIds = new Set(releases.map(r => r.investor_id))
|
||||
@@ -161,55 +210,50 @@ export default function DataroomPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-white">Data Room</h1>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setTab('documents')}
|
||||
className={`text-sm px-4 py-2 rounded-lg transition-colors ${tab === 'documents' ? 'bg-indigo-500/20 text-indigo-300' : 'text-white/50 hover:text-white/80'}`}
|
||||
>
|
||||
Documents
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('uploads')}
|
||||
className={`text-sm px-4 py-2 rounded-lg transition-colors ${tab === 'uploads' ? 'bg-indigo-500/20 text-indigo-300' : 'text-white/50 hover:text-white/80'}`}
|
||||
>
|
||||
Investor Uploads
|
||||
</button>
|
||||
{(['documents', 'uploads'] as const).map(t => (
|
||||
<button key={t} onClick={() => setTab(t)}
|
||||
className={`text-sm px-4 py-2 rounded-lg transition-colors ${tab === t ? 'bg-indigo-500/20 text-indigo-300' : 'text-white/50 hover:text-white/80'}`}>
|
||||
{t === 'documents' ? 'Documents' : 'Investor Uploads'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tab === 'documents' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Document list */}
|
||||
{/* Drop zone + document list */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-white/50">{docs.length} document{docs.length !== 1 ? 's' : ''}</span>
|
||||
<button
|
||||
onClick={() => fileRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="bg-indigo-500 hover:bg-indigo-600 disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
{uploading ? 'Uploading…' : 'Upload'}
|
||||
</button>
|
||||
<input ref={fileRef} type="file" className="hidden" onChange={uploadFile} />
|
||||
<div
|
||||
onDragOver={e => { e.preventDefault(); setDragging(true) }}
|
||||
onDragLeave={() => setDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className={`border-2 border-dashed rounded-xl p-8 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>
|
||||
: <>
|
||||
<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>
|
||||
|
||||
{docs.length === 0 && (
|
||||
<div className="bg-white/[0.03] border border-dashed border-white/10 rounded-xl p-10 text-center text-white/30 text-sm">
|
||||
No documents yet. Upload the first one.
|
||||
</div>
|
||||
{docs.length > 0 && (
|
||||
<p className="text-xs text-white/30">{docs.length} document{docs.length !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
|
||||
{docs.map(doc => (
|
||||
<button
|
||||
key={doc.id}
|
||||
onClick={() => selectDoc(doc)}
|
||||
className={`w-full text-left bg-white/[0.03] border rounded-xl p-4 transition-colors ${selected?.id === doc.id ? 'border-indigo-500/40 bg-indigo-500/5' : 'border-white/[0.06] hover:border-white/[0.12]'}`}
|
||||
>
|
||||
<button key={doc.id} onClick={() => selectDoc(doc)}
|
||||
className={`w-full text-left bg-white/[0.03] border rounded-xl p-4 transition-colors ${selected?.id === doc.id ? 'border-indigo-500/40 bg-indigo-500/5' : 'border-white/[0.06] hover:border-white/[0.12]'}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText className="w-5 h-5 text-indigo-400 mt-0.5 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-white truncate">{doc.display_name || doc.filename}</div>
|
||||
<div className="text-xs text-white/40 mt-0.5">{fmt(doc.file_size)} · {new Date(doc.created_at).toLocaleDateString()}</div>
|
||||
{(doc.description_en || doc.description_de) && (
|
||||
<div className="text-xs text-white/30 mt-1 line-clamp-1">{doc.description_en || doc.description_de}</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-white/40 shrink-0">
|
||||
{doc.release_count > 0 ? <span className="text-emerald-400">{doc.release_count} released</span> : 'not released'}
|
||||
@@ -219,7 +263,7 @@ export default function DataroomPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Release panel */}
|
||||
{/* Detail panel */}
|
||||
{selected ? (
|
||||
<div className="bg-white/[0.04] border border-white/[0.06] rounded-2xl p-5 space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -228,19 +272,13 @@ export default function DataroomPage() {
|
||||
<div className="text-xs text-white/40 mt-0.5">{fmt(selected.file_size)} · {selected.mime_type}</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<button
|
||||
onClick={releaseAll}
|
||||
disabled={busy || allInvestors.length === 0}
|
||||
className="text-xs bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 px-3 py-1.5 rounded-lg flex items-center gap-1.5 disabled:opacity-40"
|
||||
>
|
||||
<button onClick={releaseAll} disabled={busy || allInvestors.length === 0}
|
||||
className="text-xs bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 px-3 py-1.5 rounded-lg flex items-center gap-1.5 disabled:opacity-40">
|
||||
<Share2 className="w-3.5 h-3.5" /> Release all
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteDoc(selected.id)}
|
||||
disabled={busy}
|
||||
className="text-xs bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 px-3 py-1.5 rounded-lg flex items-center gap-1.5 disabled:opacity-40"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" /> Delete
|
||||
<button onClick={() => deleteDoc(selected.id)} disabled={busy}
|
||||
className="text-xs bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 px-3 py-1.5 rounded-lg flex items-center gap-1.5 disabled:opacity-40">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setSelected(null)} className="text-white/40 hover:text-white/80">
|
||||
<X className="w-4 h-4" />
|
||||
@@ -248,24 +286,63 @@ export default function DataroomPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description editor */}
|
||||
<div className="border-t border-white/[0.06] pt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-3.5 h-3.5 text-white/30" />
|
||||
<span className="text-xs font-semibold text-white/50 uppercase tracking-wider">Description</span>
|
||||
</div>
|
||||
{!descEdit && (
|
||||
<button onClick={() => setDescEdit({ text: selected.description_en || selected.description_de || '', lang: selected.description_en ? 'en' : 'de' })}
|
||||
className="text-xs text-white/30 hover:text-white/60 flex items-center gap-1">
|
||||
<Pencil className="w-3 h-3" /> Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{descEdit ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2 items-start">
|
||||
<textarea
|
||||
value={descEdit.text}
|
||||
onChange={e => setDescEdit(d => d ? { ...d, text: e.target.value } : d)}
|
||||
rows={3}
|
||||
placeholder="Enter 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/40"
|
||||
/>
|
||||
<LangToggle lang={descEdit.lang} onChange={l => setDescEdit(d => d ? { ...d, lang: l } : d)} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={saveDescription} disabled={busy}
|
||||
className="text-xs bg-indigo-500/20 hover:bg-indigo-500/30 text-indigo-300 px-3 py-1.5 rounded-lg disabled:opacity-40">
|
||||
Save & translate
|
||||
</button>
|
||||
<button onClick={() => setDescEdit(null)} className="text-xs text-white/30 hover:text-white/60 px-3 py-1.5">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (selected.description_en || selected.description_de) ? (
|
||||
<div className="space-y-1.5">
|
||||
{selected.description_de && <p className="text-xs text-white/40 leading-relaxed"><span className="text-white/20 uppercase text-[10px] mr-1">DE</span>{selected.description_de}</p>}
|
||||
{selected.description_en && <p className="text-xs text-white/40 leading-relaxed"><span className="text-white/20 uppercase text-[10px] mr-1">EN</span>{selected.description_en}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-white/20">No description yet.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Investor access */}
|
||||
<div className="border-t border-white/[0.06] pt-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Users className="w-4 h-4 text-white/40" />
|
||||
<span className="text-xs font-semibold text-white/60 uppercase tracking-wider">Investor Access</span>
|
||||
</div>
|
||||
{allInvestors.length === 0 && (
|
||||
<p className="text-xs text-white/30">No active investors yet.</p>
|
||||
)}
|
||||
{allInvestors.length === 0 && <p className="text-xs text-white/30">No active investors yet.</p>}
|
||||
<div className="space-y-2">
|
||||
{allInvestors.map(inv => {
|
||||
const has = releasedIds.has(inv.id)
|
||||
return (
|
||||
<button
|
||||
key={inv.id}
|
||||
onClick={() => toggleRelease(inv.id, has)}
|
||||
disabled={busy}
|
||||
className="w-full flex items-center gap-3 p-2.5 rounded-lg hover:bg-white/[0.04] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<button key={inv.id} onClick={() => toggleRelease(inv.id, has)} disabled={busy}
|
||||
className="w-full flex items-center gap-3 p-2.5 rounded-lg hover:bg-white/[0.04] transition-colors disabled:opacity-50">
|
||||
<div className={`w-5 h-5 rounded border flex items-center justify-center shrink-0 ${has ? 'bg-emerald-500 border-emerald-500' : 'border-white/20'}`}>
|
||||
{has && <Check className="w-3 h-3 text-white" />}
|
||||
</div>
|
||||
@@ -310,22 +387,53 @@ export default function DataroomPage() {
|
||||
<span className="ml-auto text-xs text-white/40">{uploads.length} file{uploads.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{uploads.map(u => (
|
||||
<div key={u.id} className="flex items-center gap-3 p-2.5 bg-white/[0.03] rounded-lg">
|
||||
<FileText className="w-4 h-4 text-white/40 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm text-white truncate">{u.display_name || u.filename}</div>
|
||||
<div className="text-xs text-white/40">{fmt(u.file_size)} · {new Date(u.created_at).toLocaleString()}</div>
|
||||
{uploads.map(u => {
|
||||
const isEditing = editingUpload?.uploadId === u.id
|
||||
return (
|
||||
<div key={u.id} className="bg-white/[0.03] rounded-lg p-3 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-white/40 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm text-white truncate">{u.display_name || u.filename}</div>
|
||||
<div className="text-xs text-white/40">{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({ invId: inv.id, uploadId: u.id, text: u.description_en || u.description_de || '', lang: u.description_en ? 'en' : 'de' })}
|
||||
className="text-xs text-white/30 hover:text-white/60 flex items-center gap-1">
|
||||
<Pencil className="w-3 h-3" />
|
||||
</button>
|
||||
<a href={`/api/admin/dataroom/investors/${inv.id}/uploads?download=${u.id}`} download
|
||||
className="text-xs bg-white/[0.06] hover:bg-white/[0.1] text-white/70 px-3 py-1.5 rounded-lg flex items-center gap-1.5">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{(u.description_de || u.description_en) && !isEditing && (
|
||||
<div className="pl-7 space-y-1">
|
||||
{u.description_de && <p className="text-xs text-white/30"><span className="text-white/15 uppercase text-[10px] mr-1">DE</span>{u.description_de}</p>}
|
||||
{u.description_en && <p className="text-xs text-white/30"><span className="text-white/15 uppercase text-[10px] mr-1">EN</span>{u.description_en}</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/40" />
|
||||
<LangToggle lang={editingUpload.lang} onChange={l => setEditingUpload(d => d ? { ...d, lang: l } : d)} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={saveUploadDescription} disabled={busy}
|
||||
className="text-xs bg-indigo-500/20 hover:bg-indigo-500/30 text-indigo-300 px-3 py-1.5 rounded-lg disabled:opacity-40">
|
||||
Save & translate
|
||||
</button>
|
||||
<button onClick={() => setEditingUpload(null)} className="text-xs text-white/30 hover:text-white/60 px-2">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={`/api/admin/dataroom/investors/${inv.id}/uploads?download=${u.id}`}
|
||||
className="text-xs bg-white/[0.06] hover:bg-white/[0.1] text-white/70 px-3 py-1.5 rounded-lg flex items-center gap-1.5"
|
||||
download
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" /> Download
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user