RegistryUI recipes
Subject timeline
One record's comments, files and versions merged newest first, from `recordServer(...).timeline`, skipping the sections the caller can't read.
It loads its data on the server through the block's client; the comments, attachments, versions blocks must be installed (npx better-supabase add comments attachments versions). Its page on bettersupabase.com is /recipes/subject-timeline.
Web, with shadcn components:
npx shadcn add badge button card empty/**
* The customer's history from `customerRecord.timeline`: comments, files and
* versions in one list, newest first.
*/
export async function CustomerTimeline({
timeline,
authors,
}: {
timeline: Loaded<readonly TimelineRow[]>;
authors: Readonly<Record<string, string>>;
}) {
const [t, format] = await Promise.all([
getExtracted("customers"),
getFormatter(),
]);
const kindLabel = (kind: string): string => {
switch (kind) {
case "comment": {
return t("Comment");
}
case "attachment": {
return t("File");
}
case "version": {
return t("Version");
}
default: {
return kind;
}
}
};
return (
<Card size="sm" data-recipe="record-timeline">
<CardHeader>
<CardTitle>{t("Timeline")}</CardTitle>
<CardDescription>
{t("Comments, files and versions, newest first.")}
</CardDescription>
</CardHeader>
<CardContent>
{timeline.kind === "error" ? (
<RecordLoadError title={t("The timeline didn't load")} />
) : timeline.data.length === 0 ? (
<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">
<HistoryIcon />
</EmptyMedia>
<EmptyTitle>{t("Nothing happened yet")}</EmptyTitle>
<EmptyDescription>
{t("Comments, files and versions show up here.")}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<a
href="#comments"
className={buttonVariants({ variant: "outline", size: "sm" })}
>
{t("Write a comment")}
</a>
</EmptyContent>
</Empty>
) : (
<ol className="flex flex-col gap-3">
{timeline.data.map((entry) => (
<li key={entry.id} className="flex items-center gap-2 text-sm">
<Badge variant="outline">{kindLabel(entry.kind)}</Badge>
<span className="truncate">
{(entry.actor === null ? undefined : authors[entry.actor]) ??
t("Someone")}
</span>
<time
dateTime={new Date(entry.at).toISOString()}
className="ml-auto shrink-0 text-xs text-muted-foreground tabular-nums"
>
{format.relativeTime(entry.at)}
</time>
</li>
))}
</ol>
)}
</CardContent>
</Card>
);
}Last updated on