Skip to contentVibraUI
Dashboards

Team

Who is reading and how the team speaks to them, one way for every band: the reader, the greeting, the team feed's times, tags and links, and signing out.

Server-only, since it reads db: a section imports it from its own data file (or its server file, for the sign-out action) and hands plain values to its island. The reader is the first db.members row, the workspace's owner, with the handle the team feed prints (handleFor) and the face avatarFor draws; a store with no members has no reader, and the feed shows a guest. The greeting is the kit's own rule, vocabulary-ai's greetingFor — good morning from 05:00, afternoon from 12:00 and evening from 18:00, by the hour of REFERENCE_DATE in UTC — with the reader's first name and the day, so every band greets the same hour the same way. The team feed says a time as "38m", "5h" and "3d" within a week and then the day, and in full with UTC for a screen reader; a post's text is split round its #tags; and a post and a topic link to the Projects dashboard's social page as ?post=<id> and ?tag=<tag>, a convention that page has to read (the kit's social page shows the whole feed and does not read them yet). signOut goes through mockAuthAdapter and always answers with a Result: an adapter that throws becomes a refusal the menu shows, never a rejected call. Swap the adapter for your own and the four user menus follow.

Install

npx shadcn@latest add @vibra/team-projects

Needs the @vibra registry in your components.json — set it up once.

Props

PropTypeDefaultDescription
reader() => Reader | null—Who is signed in: the first member, with id, first name, email, role, last visit, handle and face; null with no members.
viewer() => Person—The reader as the team feed shows them — name, handle, face — or a guest.
greeting() => { hello: string; today: string }—"Good afternoon, Sonia" and "Friday, September 4", from REFERENCE_DATE in UTC.
person(name: string) => Person—Anyone as the feed shows them: the name, the handle and the face.
since(at: Date) => string—"38m", "5h", "3d" within a week of REFERENCE_DATE, then the day in UTC: "Aug 28".
moment(at: Date) => Moment—{ iso, short, full }: a time short on screen and in full, with UTC, for a screen reader.
dayLabel(at: Date) => string—A day as the feed names it, in UTC: "Aug 28".
segments(text: string) => TextPart[]—A post's words split round its #tags, each tag without its hash.
postHref / tagHref(id: string) => string / (tag: string) => string—A post's and a topic's page on the team feed: /projects/social?post=<id> and ?tag=<tag>.
FEED_HREFstring"/projects/social"The team feed: the Projects dashboard's social page.
signOut() => Promise<Result<{ signedOut: true }>>—Signs the reader out through the auth adapter; a failure comes back as { ok: false, error }, never a rejection.

Dependencies

Source

lib/dashboards/projects/team.ts
/**
 * Who is reading, and how the team speaks to them, read one way for every
 * band that needs it:
 *
 * - **The reader** is the first `db.members` row, the workspace's owner, with
 *   the handle the team feed prints and the face `avatarFor` draws.
 * - **The greeting** is the kit's own rule (`greetingFor`, from
 *   `vocabulary-ai`) — good morning, afternoon or evening by the hour of
 *   `REFERENCE_DATE` in UTC — with the reader's first name and the day.
 * - **The team feed's words**: a time as the feed prints it ("38m", "5h",
 *   "3d", then the day) and in full for a screen reader, a post's text split
 *   round its `#tags`, and the pages a post and a tag have on the Projects
 *   dashboard's social page. Every time is said in UTC.
 * - **Signing out** goes through the kit's `mockAuthAdapter` and always
 *   answers with a `Result`: an adapter that throws becomes a refusal the
 *   menu can show, never a rejected call.
 *
 * Server-only, because it reads `db`: a section imports it from its own
 * `<name>.data.ts` (or its server file, for the sign-out action) and hands
 * plain values to its island.
 */
import { avatarFor } from "@/lib/avatars"
import { mockAuthAdapter } from "@/lib/auth-adapter"
import { greetingFor } from "@/lib/dashboards/ai/vocabulary"
import { REFERENCE_DATE, db, handleFor, type Member, type Result } from "@/lib/sample-data"

/** The team feed: the Projects dashboard's social page. */
export const FEED_HREF = "/projects/social"

/** A post's page on the team feed. */
export const postHref = (id: string) => `${FEED_HREF}?post=${id}`

/** A topic's page on the team feed; `tag` is written without its hash. */
export const tagHref = (tag: string) => `${FEED_HREF}?tag=${tag}`

/** Someone as the feed shows them: the name, the handle printed in mono, and the face. */
export type Person = { name: string; handle: string; avatarUrl: string }

/** A moment as the feed prints it, short on screen and in full for a screen reader. */
export type Moment = { iso: string; short: string; full: string }

/** A run of a post's text: plain words, or a topic tag the feed links. */
export type TextPart = { text: string; tag?: string }

/** Who is signed in. */
export type Reader = Person & { id: string; first: string; email: string; role: string; lastActiveAt: Date }

const MINUTE_MS = 60_000
const HOUR_MS = 3_600_000
const DAY_MS = 86_400_000

const SHORT_DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" })
const FULL = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  year: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  hourCycle: "h23",
  timeZone: "UTC",
})
const TODAY = new Intl.DateTimeFormat("en-US", { weekday: "long", month: "long", day: "numeric", timeZone: "UTC" })

const ROLE_LABELS: Record<Member["role"], string> = { owner: "Owner", admin: "Admin", member: "Member", viewer: "Viewer" }

export function person(name: string): Person {
  return { name, handle: handleFor(name), avatarUrl: avatarFor(name) }
}

/** The first member, the workspace's owner; null in a store with no members. */
export function reader(): Reader | null {
  const member = db.members.all()[0]
  if (!member) return null
  return {
    ...person(member.name),
    id: member.id,
    first: member.name.split(" ")[0],
    email: member.email,
    role: ROLE_LABELS[member.role],
    lastActiveAt: member.lastActiveAt,
  }
}

/** The reader as the team feed shows them; a guest in a store with no members. */
export function viewer(): Person {
  const who = reader()
  return who ? { name: who.name, handle: who.handle, avatarUrl: who.avatarUrl } : person("Guest")
}

/** "Good afternoon, Sonia", and the day it is: "Friday, September 4". */
export function greeting(): { hello: string; today: string } {
  const who = reader()
  const hello = greetingFor(REFERENCE_DATE)
  return { hello: who ? `${hello}, ${who.first}` : hello, today: TODAY.format(REFERENCE_DATE) }
}

/** A day as the feed names it, in UTC: "Aug 28". */
export function dayLabel(at: Date): string {
  return SHORT_DAY.format(at)
}

/** "38m", "5h", "3d" within a week of `REFERENCE_DATE`, then "Aug 28". */
export function since(at: Date): string {
  const ms = REFERENCE_DATE.getTime() - at.getTime()
  if (ms < HOUR_MS) return `${Math.max(1, Math.floor(ms / MINUTE_MS))}m`
  if (ms < DAY_MS) return `${Math.floor(ms / HOUR_MS)}h`
  if (ms < 7 * DAY_MS) return `${Math.floor(ms / DAY_MS)}d`
  return SHORT_DAY.format(at)
}

export function moment(at: Date): Moment {
  return { iso: at.toISOString(), short: since(at), full: `${FULL.format(at)} UTC` }
}

/** A post's text split round its topic tags: `#onboarding` and the like. */
export function segments(text: string): TextPart[] {
  return text
    .split(/(#[a-z][a-z-]*)/)
    .filter(Boolean)
    .map((part) => (/^#[a-z]/.test(part) ? { text: part, tag: part.slice(1) } : { text: part }))
}

/**
 * Signs the reader out through the auth adapter. Always a `Result`: an
 * adapter that throws — the backend is down, the session store refuses —
 * comes back as a refusal the menu shows where the reader is looking.
 */
export async function signOut(): Promise<Result<{ signedOut: true }>> {
  try {
    await mockAuthAdapter.signOut()
    return { ok: true, data: { signedOut: true } }
  } catch {
    return { ok: false, error: { code: "sign_out_failed", message: "The sign-in service did not answer. You are still signed in." } }
  }
}