Record timeline
Rows as bars from a start date to an end date over four weeks, with a lane per assignee. 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 button cardInstall the glue
Install the block with
npx better-supabase add calendar. 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 screen
React and shadcn
View in the exampletask-timeline.tsx const UNASSIGNED = ""; /** * Tasks as bars from `startsOn` to `dueOn`, one lane per assignee, over a * four-week window that lives in the URL (`?from=YYYY-MM-DD`). */ export async function TaskTimeline({ rows, members, from, today, windowHref, }: { rows: readonly TaskRow[]; members: readonly MemberOption[]; from: Temporal.PlainDate; today: Temporal.PlainDate; windowHref: (from: string) => string; }) { const t = await getExtracted("projects"); const lanes = new Map<string, TaskRow[]>(); for (const row of rows) { if (spanOf(row, from) !== null) { const lane = row.assigneeId ?? UNASSIGNED; lanes.set(lane, [...(lanes.get(lane) ?? []), row]); } } const laneName = (id: string) => id === UNASSIGNED ? t("Unassigned") : (members.find((member) => member.userId === id)?.name ?? id); return ( <Card data-recipe="record-timeline"> <CardHeader> <CardTitle>{t("Timeline")}</CardTitle> <CardDescription> {t( "Tasks with a start or due date in these four weeks, by assignee.", )} </CardDescription> <CardAction> <WindowNav from={from} today={today} windowHref={windowHref} /> </CardAction> </CardHeader> <CardContent> <div className="overflow-x-auto"> <div className="min-w-[56rem] overflow-hidden rounded-lg border"> <div className="grid grid-cols-[10rem_1fr]"> <span className="border-e border-b bg-muted/50" /> <WindowDays from={from} today={today} /> {lanes.size === 0 ? ( <p className="col-span-2 p-4 text-sm text-muted-foreground"> {t("No tasks fall in these weeks.")} </p> ) : null} {[...lanes].map(([lane, tasks]) => ( <div key={lane} className="contents"> <span className="truncate border-e border-b p-2 text-sm font-medium"> {laneName(lane)} </span> <ol className="grid grid-cols-28 gap-y-1 border-b py-1.5"> {tasks.map((task) => { const span = spanOf(task, from); return span === null ? null : ( <li key={task.id} className="[grid-column:var(--span-start)/span_var(--span-length)]" // SAFETY: React's CSSProperties has no custom properties; both are numbers. style={ { "--span-start": span.start + 1, "--span-length": span.length, } as CSSProperties } > <Link href={`/projects/${task.projectId}`} title={task.title} className={cn( "block truncate rounded-sm bg-primary/10 px-1.5 py-0.5 text-xs transition-colors hover:bg-primary/20", task.status === "done" && "text-muted-foreground line-through", )} > {task.title} </Link> </li> ); })} </ol> </div> ))} </div> </div> </div> </CardContent> </Card> ); }