# Record timeline

> Rows as bars from a start date to an end date over four weeks, with a lane per assignee.

- Block: [calendar](https://bettersupabase.com/blocks/calendar.md)
- Kind: screen
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/record-timeline

## Install the block

```bash
npx better-supabase add calendar
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add button card
```

From `apps/examples/nextjs/src/features/projects/components/task-timeline.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/projects/components/task-timeline.tsx):

```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>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Record timeline" in a React web app that uses shadcn/ui components. Rows as bars from a start date to an end date over four weeks, with a lane per assignee.

It uses the better-supabase `calendar` block (Calendar). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add calendar` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add button card`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the screen. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/projects/components/task-timeline.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/record-timeline.md
- Block docs: https://bettersupabase.com/docs/blocks/calendar
```
