diff --git a/frontend/src/components/CollectionsView.jsx b/frontend/src/components/CollectionsView.jsx index bd542e7..48e4271 100644 --- a/frontend/src/components/CollectionsView.jsx +++ b/frontend/src/components/CollectionsView.jsx @@ -1,15 +1,25 @@ -import { useState, useEffect, useCallback } from 'react' -import { listCollections, createCollection, getCollection } from '../api.js' +import { useState, useEffect, useCallback, useRef } from 'react' +import { + listCollections, createCollection, getCollection, + addEntryToCollection, removeEntryFromCollection, + updateEntryVisibility, updateCollection, deleteCollection, +} from '../api.js' -const VIS_LABELS = { 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' } +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 [selectedColl, setSelectedColl] = 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('') @@ -18,7 +28,21 @@ export default function CollectionsView({ archiveId }) { const [creating, setCreating] = useState(false) const [createError, setCreateError] = useState(null) - const refresh = useCallback(async () => { + // 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) @@ -32,16 +56,25 @@ export default function CollectionsView({ archiveId }) { } }, [archiveId]) - useEffect(() => { refresh() }, [refresh]) - - useEffect(() => { - if (!selectedColl) { setCollDetail(null); return } + const refreshDetail = useCallback(async (uid) => { + if (!uid) { setCollDetail(null); return } setDetailLoading(true) - getCollection(archiveId, selectedColl.collection_uid) - .then(d => setCollDetail(d)) - .catch(e => setError(e.message)) - .finally(() => setDetailLoading(false)) - }, [selectedColl, archiveId]) + 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() @@ -51,11 +84,12 @@ export default function CollectionsView({ archiveId }) { setCreating(true) setCreateError(null) try { - await createCollection(archiveId, name, slug, newVis) + const coll = await createCollection(archiveId, name, slug, newVis) setNewName('') setNewSlug('') setNewVis(2) - await refresh() + await refreshList() + setSelectedUid(coll.collection_uid) } catch (err) { setCreateError(err.message) } finally { @@ -63,71 +97,240 @@ export default function CollectionsView({ archiveId }) { } } + 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

+

Collections

{loading &&
Loading…
} - {error &&
{error}
} + {error &&
{error}
}
-
+ {/* Sidebar */} +
{collections.map(c => ( -
setSelectedColl(c)} + className={`coll-row${selectedUid === c.collection_uid ? ' is-active' : ''}`} + onClick={() => setSelectedUid(c.collection_uid)} > - {c.name} - - {VIS_LABELS[c.default_visibility_bits] ?? c.default_visibility_bits} - -
+ {c.name} + {VIS_LABEL(c.default_visibility_bits)} + ))} {collections.length === 0 && !loading && ( -
No collections yet.
+
No collections yet.
)}
- {selectedColl && ( -
-

{selectedColl.name}

-
- Default visibility: {VIS_LABELS[selectedColl.default_visibility_bits] ?? selectedColl.default_visibility_bits} -
- {detailLoading ? ( -
Loading entries…
- ) : collDetail ? ( - collDetail.entries.length === 0 ? ( -
No entries visible to you in this collection.
+ {/* Detail pane */} + {selected ? ( +
+ {/* Header */} +
+ {renaming ? ( + setRenameVal(e.target.value)} + onBlur={handleRenameCommit} + onKeyDown={e => { + if (e.key === 'Enter') handleRenameCommit() + if (e.key === 'Escape') setRenaming(false) + }} + /> ) : ( -
    - {collDetail.entries.map(entry => ( -
  • - - {entry.title || entry.entry_uid} - - - {entry.source_kind} - -
  • - ))} -
- ) - ) : null} +

{ 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