// §22 S2 — the project collection directory at `/p//`. Lists the // project's caller-visible collections as cards linking into each collection's // `/p//c//` home. When exactly one collection is visible // the directory is skipped and we redirect straight into it (the S1 C3.7/C3.8 // single-collection UX, preserved). // // §22 S4 — role-aware empty states + owner controls. The list response carries // a `viewer` capability block; the directory reads it to render: // * C3.3: a project Owner sees a "Create your first collection" CTA (and a // "New collection" control when the directory is non-empty), opening the // create-collection modal (choose a type + id + visibility). // * C3.4: a contributor without create rights sees the empty directory with // no create action. // * C.2: an Owner with membership-management reach sees a "Members" control // opening the scope-role invitation modal. import { useEffect, useState } from 'react' import { Link, Navigate } from 'react-router-dom' import { listCollections } from '../api' import { collectionHome } from '../lib/entryPaths' import { entryNoun } from './ProjectLayout.jsx' import CreateCollectionModal from './CreateCollectionModal.jsx' import ScopeMembersModal from './ScopeMembersModal.jsx' import JoinRequestModal from './JoinRequestModal.jsx' export default function CollectionDirectory({ projectId }) { const [cols, setCols] = useState(null) const [viewer, setViewer] = useState(null) const [version, setVersion] = useState(0) const [createOpen, setCreateOpen] = useState(false) const [membersOpen, setMembersOpen] = useState(false) const [joinOpen, setJoinOpen] = useState(false) useEffect(() => { let live = true listCollections(projectId) .then(d => { if (live) { setCols(d.items); setViewer(d.viewer || null) } }) .catch(() => { if (live) { setCols([]); setViewer(null) } }) return () => { live = false } }, [projectId, version]) if (cols === null) { return
Loading…
} // C3.7/C3.8: a single visible collection skips the directory. if (cols.length === 1) { return } const canCreate = !!viewer?.can_create_collection const canInvite = !!viewer?.can_invite const canRequestJoin = !!viewer?.can_request_join return (

Collections

{canInvite && ( )} {canRequestJoin && ( )} {canCreate && cols.length > 0 && ( )}
{cols.length === 0 ? ( // C3.3 / C3.4: role-keyed empty state. canCreate ? (

No collections yet.

) : (

No collections yet.

) ) : (
    {cols.map(c => (
  • {c.name || c.id} {entryNoun(c.type)}s
  • ))}
)}
{createOpen && ( setCreateOpen(false)} onCreated={() => { setCreateOpen(false); setVersion(v => v + 1) }} /> )} {membersOpen && ( setMembersOpen(false)} /> )} {joinOpen && ( setJoinOpen(false)} /> )}
) }