import { useState, useEffect, useCallback, useRef } from 'react' import { listCollections, createCollection, getCollection, addEntryToCollection, removeEntryFromCollection, updateEntryVisibility, updateCollection, deleteCollection, } from '../api.js' const VIS_OPTIONS = [ { value: 0, label: 'Private' }, { value: 2, label: 'Users only' }, { value: 3, label: 'Public' }, ] const VIS_LABEL = v => VIS_OPTIONS.find(o => o.value === v)?.label ?? String(v) export default function CollectionsView({ archiveId }) { const [collections, setCollections] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [selectedUid, setSelectedUid] = useState(null) const [collDetail, setCollDetail] = useState(null) const [detailLoading, setDetailLoading] = useState(false) const [detailError, setDetailError] = useState(null) // Create form const [newName, setNewName] = useState('') const [newSlug, setNewSlug] = useState('') const [newVis, setNewVis] = useState(2) const [creating, setCreating] = useState(false) const [createError, setCreateError] = useState(null) // Add-entry form const [addUid, setAddUid] = useState('') const [addVis, setAddVis] = useState(2) const [adding, setAdding] = useState(false) const [addError, setAddError] = useState(null) // Inline rename state const [renaming, setRenaming] = useState(false) const [renameVal, setRenameVal] = useState('') const renameRef = useRef(null) const selected = collections.find(c => c.collection_uid === selectedUid) ?? null const isDefault = selected?.slug === '_default_' const refreshList = useCallback(async () => { if (!archiveId) return setLoading(true) setError(null) try { const cols = await listCollections(archiveId) setCollections(cols) } catch (e) { setError(e.message) } finally { setLoading(false) } }, [archiveId]) const refreshDetail = useCallback(async (uid) => { if (!uid) { setCollDetail(null); return } setDetailLoading(true) setDetailError(null) try { const d = await getCollection(archiveId, uid) setCollDetail(d) } catch (e) { setDetailError(e.message) } finally { setDetailLoading(false) } }, [archiveId]) useEffect(() => { refreshList() }, [refreshList]) useEffect(() => { refreshDetail(selectedUid) }, [selectedUid, refreshDetail]) // Auto-focus rename input useEffect(() => { if (renaming && renameRef.current) renameRef.current.focus() }, [renaming]) async function handleCreate(e) { e.preventDefault() const name = newName.trim() const slug = newSlug.trim() if (!name || !slug) return setCreating(true) setCreateError(null) try { const coll = await createCollection(archiveId, name, slug, newVis) setNewName('') setNewSlug('') setNewVis(2) await refreshList() setSelectedUid(coll.collection_uid) } catch (err) { setCreateError(err.message) } finally { setCreating(false) } } async function handleRenameCommit() { const name = renameVal.trim() if (!name || !selected) { setRenaming(false); return } try { await updateCollection(archiveId, selected.collection_uid, { name }) await refreshList() setCollDetail(d => d ? { ...d, name } : d) } catch (e) { setError(e.message) } finally { setRenaming(false) } } async function handleVisChange(newVisVal) { if (!selected) return try { await updateCollection(archiveId, selected.collection_uid, { default_visibility_bits: newVisVal }) await refreshList() setCollDetail(d => d ? { ...d, default_visibility_bits: newVisVal } : d) } catch (e) { setError(e.message) } } async function handleDelete() { if (!selected) return if (!window.confirm(`Delete collection "${selected.name}"? Entries will not be deleted.`)) return try { await deleteCollection(archiveId, selected.collection_uid) setSelectedUid(null) setCollDetail(null) await refreshList() } catch (e) { setError(e.message) } } async function handleAddEntry(e) { e.preventDefault() const uid = addUid.trim() if (!uid || !selected) return setAdding(true) setAddError(null) try { await addEntryToCollection(archiveId, selected.collection_uid, uid, addVis) setAddUid('') await refreshDetail(selected.collection_uid) } catch (err) { setAddError(err.message) } finally { setAdding(false) } } async function handleRemoveEntry(entryUid) { if (!selected) return try { await removeEntryFromCollection(archiveId, selected.collection_uid, entryUid) await refreshDetail(selected.collection_uid) } catch (e) { setDetailError(e.message) } } async function handleEntryVisChange(entryUid, vis) { if (!selected) return try { await updateEntryVisibility(archiveId, selected.collection_uid, entryUid, vis) setCollDetail(d => d ? { ...d, entries: d.entries.map(en => en.entry_uid === entryUid ? { ...en, collection_visibility_bits: vis } : en ), } : d) } catch (e) { setDetailError(e.message) } } if (!archiveId) return
Select an archive.
return (

Collections

{loading &&
Loading…
} {error &&
{error}
}
{/* Sidebar */}
{collections.map(c => ( ))} {collections.length === 0 && !loading && (
No collections yet.
)}
{/* Detail pane */} {selected ? (
{/* Header */}
{renaming ? ( setRenameVal(e.target.value)} onBlur={handleRenameCommit} onKeyDown={e => { if (e.key === 'Enter') handleRenameCommit() if (e.key === 'Escape') setRenaming(false) }} /> ) : (

{ if (!isDefault) { setRenameVal(selected.name); setRenaming(true) } }} > {collDetail?.name ?? selected.name} {!isDefault && }

)} {!isDefault && ( )}
{/* Visibility */}
Default visibility
{/* Entries */}
Entries
{detailLoading &&
Loading…
} {detailError &&
{detailError}
} {!detailLoading && collDetail && ( collDetail.entries.length === 0 ? (
No entries in this collection.
) : (
    {collDetail.entries.map(entry => (
  • {entry.title || entry.entry_uid} {entry.source_kind}
    {!isDefault && ( )}
  • ))}
) )}
{/* Add entry (non-default only) */} {!isDefault && (
Add entry
setAddUid(e.target.value)} placeholder="entry_uid" required />
{addError &&
{addError}
}
)}
) : (
Select a collection to view details.
)}
{/* Create form */}
+ Create collection
{ setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }} placeholder="My Collection" required />
setNewSlug(e.target.value)} placeholder="my-collection" required />
{createError &&
{createError}
}
) }