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.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add badge button card emptyInstall the glue
Install the blocks with
npx better-supabase add comments attachments versions. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the examplecustomer-timeline.tsx /** * 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> ); }