/**
* 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." } }
}
}