import type { UpdateUserProfile, UserFact, UserFactCategory, UserProfile } from "@rakazo/contracts"; import { USER_FACT_CATEGORIES } from "@rakazo/contracts"; /** What the fact editor holds: null spaces means every space. */ export interface UserFactDraft { value: string; category: UserFactCategory; spaceIds: string[] | null; } type UserFactEdit = NonNullable[number]; export type ProfileField = "timeZone" | "language" | "locale" | "communicationStyle" | "summary"; const PROFILE_FIELDS: ProfileField[] = [ "timeZone", "language", "locale", "communicationStyle", "summary", ]; type ClearedProfileField = { timeZone?: null; language?: null; locale?: null; communicationStyle?: ""; summary?: ""; }; /** The `profile.update` input that removes one profile field. */ export const CLEARED_PROFILE_FIELD: Record = { timeZone: { timeZone: null }, language: { language: null }, locale: { locale: null }, communicationStyle: { communicationStyle: "" }, summary: { summary: "" }, }; /** Profile fields that hold a value, in display order. */ export function filledProfileFields( profile: UserProfile, ): Array<{ field: ProfileField; value: string }> { return PROFILE_FIELDS.flatMap((field) => { const value = profile[field]; return value ? [{ field, value }] : []; }); } /** Facts grouped in category order; pending facts lead each group, then the newest. */ export function groupUserFacts( facts: UserFact[], ): Array<{ category: UserFactCategory; facts: UserFact[] }> { return USER_FACT_CATEGORIES.flatMap((category) => { const inCategory = facts .filter((fact) => fact.category === category) .sort((a, b) => a.status === b.status ? b.createdAt.localeCompare(a.createdAt) : a.status === "pending" ? -1 : 1, ); return inCategory.length > 0 ? [{ category, facts: inCategory }] : []; }); } /** A language tag in the reader's language, e.g. "de" → "German"; the tag itself if unknown. */ export function displayLanguageTag(tag: string, uiLocale: string): string { try { return new Intl.DisplayNames([uiLocale], { type: "language" }).of(tag) ?? tag; } catch { return tag; } } /** The region of a locale tag in the reader's language, e.g. "de-AT" → "Austria". */ export function displayRegionTag(tag: string, uiLocale: string): string { try { const region = new Intl.Locale(tag).region; if (!region) return displayLanguageTag(tag, uiLocale); return new Intl.DisplayNames([uiLocale], { type: "region" }).of(region) ?? tag; } catch { return tag; } } export function userFactDraft(fact: UserFact): UserFactDraft { return { value: fact.value, category: fact.category, spaceIds: fact.allSpaces ? null : fact.spaceIds, }; } /** The `editFacts` entry for what changed in the draft, or null when nothing did. */ export function userFactEdit(fact: UserFact, draft: UserFactDraft): UserFactEdit | null { const edit: UserFactEdit = { id: fact.id }; const value = draft.value.trim(); if (value !== fact.value) edit.value = value; if (draft.category !== fact.category) edit.category = draft.category; const before = userFactDraft(fact).spaceIds; const sameSpaces = before === null || draft.spaceIds === null ? before === draft.spaceIds : before.length === draft.spaceIds.length && draft.spaceIds.every((spaceId) => before.includes(spaceId)); if (!sameSpaces) edit.spaceIds = draft.spaceIds; return Object.keys(edit).length > 1 ? edit : null; } /** Checks or unchecks one space; unchecking the last one shows the fact everywhere again. */ export function toggleFactSpace( spaceIds: string[] | null, spaceId: string, checked: boolean, ): string[] | null { const current = spaceIds ?? []; const next = checked ? [...current.filter((id) => id !== spaceId), spaceId] : current.filter((id) => id !== spaceId); return next.length > 0 ? next : null; } /** Names of the spaces a limited fact shows in, in the order of `spaces`; unknown ids are skipped. */ export function factSpaceNames( spaceIds: string[] | null, spaces: ReadonlyArray<{ id: string; name: string }>, ): string[] { if (!spaceIds) return []; return spaces.filter((space) => spaceIds.includes(space.id)).map((space) => space.name); }