Reference
Members

Members

Part of @cobuntu/embed-react: headless, data via props, links through your router.

The directory

"use client";
import { useState } from "react";
import Link from "next/link";
import { MemberDirectory, type RenderLink } from "@cobuntu/embed-react";
import type { DirectoryResult } from "@cobuntu/embed";
 
const renderLink: RenderLink = ({ href, className, children, ...rest }) => (
  <Link href={href} className={className} {...rest}>{children}</Link>
);
 
export function Directory({ initial }: { initial: DirectoryResult }) {
  const [members, setMembers] = useState(initial.members);
  const [cursor, setCursor] = useState(initial.nextCursor);
  const [busy, setBusy] = useState(false);
 
  async function loadMore() {
    setBusy(true);
    const r = await fetch(`/api/community/directory?cursor=${cursor}`); // your own route
    const d = await r.json();
    setMembers((m) => [...m, ...d.members]);
    setCursor(d.nextCursor);
    setBusy(false);
  }
 
  return (
    <MemberDirectory
      members={members}
      hasMore={!!cursor}
      onLoadMore={loadMore}
      loadingMore={busy}
      profileHref={(m) => `/community/${m.usertag}`}
      renderLink={renderLink}
    />
  );
}

What it does, out of the box:

  • Sorted by name, accent-aware (localeCompare, base sensitivity).
  • Search across the whole profile (case- and accent-insensitive, every term must match): name, @handle, signature, bio, role, tier, location, languages and notes. Every word must match somewhere, so "marketing portuguese" finds someone who does marketing and speaks Portuguese.
  • Facet filters behind a Filters button: type, tier, place, languages, notes. OR within a facet ("Porto or Lisbon"), AND across facets. A facet is only offered when it has more than one option. Every option shows how many people it would leave.
  • The contact peek: tapping a card opens a dialog with the bio, notes, links and details, and a Profile link. Escape, the backdrop and Close dismiss it.
  • Load more while hasMore is true.

MemberDirectory props

PropTypeNotes
membersCbMember[]Required. DirectoryResult.members fits as is.
facetsCbDirectoryFacetsDirectoryResult.facets. When given, the sheet offers these (whole roster); otherwise facets are built from members.
search / defaultSearch / onSearchChangestring / string / (q) => voidControlled or uncontrolled search.
filters / defaultFilters / onFiltersChangeFilterValueControlled or uncontrolled filters ({ tiers: ["Mentor"], locations: [...] }).
filterMode"client" | "server"Default "client": narrows the loaded members in the browser. "server": shows members as given, so refetch in the callbacks.
totalCountnumberSummary count in server mode.
hasMore / onLoadMore / loadingMore / loadMoreErrorPagination.
profileHref(m) => stringDefault /members/<usertag>.
renderLinkRenderLinkDefault: a plain <a>.
peekbooleanCard tap opens the contact peek. Default true.
viewerIdstringMarks the viewer's own card "You".
localestringFor dates.
labelsPartial<CbLabels>Replace any string.
renderCardActions, renderPeekActions, presenceOf, renderNotesPlatform slots. Empty by default.
renderLinkIcon(network, url) => ReactNodeAn icon inside each link pill.

The profile

import { MemberProfile } from "@cobuntu/embed-react";
 
<MemberProfile
  member={member}                         // MemberProfile from cobuntu.me.member(handle)
  renderLink={renderLink}
  eventHref={(e) => `/events/${e.slug}`}
/>

The avatar sits beside the name, handle, pronouns, signature, bio and link pills; the action row runs beneath at full width; then the tabs. The Events tab lists the events the member hosts, or an empty state.

PropTypeNotes
memberCbMemberProfileRequired.
renderLinkRenderLinkFor the event links.
eventHref(event) => stringDefault /events/<slug>.
formatEventDate(iso) => stringDefault: day, month, year in locale.
onShare() => voidShows a Share button.
eventsTabIconReactNodeIcon in the Events tab and its empty state.
renderActions, renderStats, renderAvatar, presence, extraTabsPlatform slots.
labels, locale, renderLinkIconAs above.

The gate

When the API answers 401 (signed out) or 403 MEMBERS_ONLY (signed in, not a member), render the gate instead:

<MembersGate reason="signed-out" signInHref="/login?next=/community" joinHref="/join" renderLink={renderLink} />
<MembersGate reason="not-member" joinHref="/join" renderLink={renderLink} />
PropNotes
reason"signed-out" offers sign in and join; "not-member" offers join.
signInHref, joinHrefRendered through renderLink.
signInSlot, joinSlotReplace an action entirely (a button that opens your modal).
asHeading level, default h1.

Smaller pieces

MemberCard, MemberFilterBar and MemberContactPeek are exported for custom layouts. The pure helpers behind the directory are too: filterMembersBySearch, buildMemberFacets, facetsFromServer, memberPassesFilters, toMemberFilters, sortMembersByName, normalizeProfileLinks, getSocialNetwork.

Class map

See the class map conventions.

Directory

ClassElementState attributes
cb-directoryRootdata-state="ready | no-results | empty", data-filter-mode
cb-directory__notesNotes slot wrapper
cb-directory__grid / __item<ul> of cards / each <li>
cb-directory__more / __load-more / __errorLoad more row, button, errordata-state="idle | loading"
cb-empty / __title / __hint / __iconEmpty statesdata-kind="members | search | events"

Filter bar

ClassElementState attributes
cb-filterRootdata-active
cb-filter__rowToolbar row
cb-filter__search / __search-icon / __search-input / __search-clearSearch fielddata-filled
cb-filter__toggle / __toggle-label / __badgeFilters button, its word, the countdata-active, aria-expanded
cb-filter__clearClear beside the toolbar
cb-filter__summary / __count"12 people · Lisbon" line
cb-filter__backdrop / __sheet / __handleSheet overlay, dialog, drag handle
cb-filter__sheet-header / __sheet-title / __closeSheet header
cb-filter__body / __facet / __facet-title / __optionsFacet groupsdata-facet, data-single
cb-filter__option / __check / __option-label / __option-countOne optiondata-state="on | off", aria-pressed; check: data-shape="radio | checkbox"
cb-filter__footer / __reset / __doneClear and "Show N"disabled

Card

ClassElementState attributes
cb-member-cardRootdata-interactive, data-own
cb-member-card__identity / __textAvatar row / name column
cb-member-card__avatar (+ cb-avatar)Avatardata-has-image
cb-member-card__name / __name-textName line
cb-member-card__handle / __subline@handle / place · languages
cb-member-card__pillsPill row
cb-member-card__signatureHeadline (clamp it to 3 lines)
cb-member-card__spacerGrows so actions sit on the floor
cb-member-card__actions / __profileActions row / Profile link

Peek

ClassElementState attributes
cb-peek / __backdrop / __frame / __surfaceRoot, overlay, positioning frame, dialogdata-state="open"
cb-peek__header / __close-x / __eyebrowHeader, X (show it on phones), "Contact"
cb-peek__body / __identity / __avatar / __name / __handleIdentity
cb-peek__sections / __section / __section-labelSection blocksdata-section="about | notes | links | details"
cb-peek__signature / __bioAbout
cb-peek__notes / __noteNotesdata-category
cb-peek__details / __detail / __detail-label / __detail-valueDetails rows
cb-peek__actions / __action / __profileQuick actions
cb-peek__closeFooter Close (show it on desktop)

Profile

ClassElementState attributes
cb-profileRootdata-has-notes
cb-profile__header / __media / __avatar / __infoTwo columns
cb-profile__identity / __name / __handle / __pronounsIdentity
cb-profile__statsStats slot
cb-profile__about / __signature / __bioAbout
cb-profile__linksLink pills list
cb-profile__actions / __shareAction row
cb-profile__tabs, cb-tabs / __tab / __icon / __label / __panelTabsdata-state="active | inactive", aria-selected; panel: data-tab
cb-profile__events, cb-event / __link / __banner / __img / __name / __dateHosted eventsbanner: data-has-image

Gate

ClassElementState attributes
cb-gate / __title / __body / __actions / __sign-in / __joinThe gatedata-reason="signed-out | not-member"