# Record Gantt chart

> A row per record grouped by its parent, with a bar for its planned dates and a marker for rows without dates.

- 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-gantt

## Install the block

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

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
const BAR = {
  todo: "bg-muted-foreground/30",
  doing: "bg-primary/60",
  done: "bg-primary/25",
} as const satisfies Record<TaskStatus, string>;

/**
 * One row per task under its project, with a bar from `startsOn` to `dueOn`.
 * Tasks without dates stay in the list with an empty track, so the plan
 * shows what still needs scheduling.
 */
export async function TaskGantt({
  rows,
  projects,
  from,
  today,
  windowHref,
}: {
  rows: readonly TaskRow[];
  projects: readonly ProjectOption[];
  from: Temporal.PlainDate;
  today: Temporal.PlainDate;
  windowHref: (from: string) => string;
}) {
  const t = await getExtracted("projects");

  const groups = projects
    .map((project) => ({
      project,
      tasks: rows.filter((row) => row.projectId === project.id),
    }))
    .filter((group) => group.tasks.length > 0);

  return (
    <Card data-recipe="record-gantt">
      <CardHeader>
        <CardTitle>{t("Gantt")}</CardTitle>
        <CardDescription>
          {t("Every task by project, with its planned dates.")}
        </CardDescription>
        <CardAction>
          <WindowNav from={from} today={today} windowHref={windowHref} />
        </CardAction>
      </CardHeader>
      <CardContent>
        <div className="overflow-x-auto">
          <div className="min-w-[60rem] overflow-hidden rounded-lg border">
            <div className="grid grid-cols-[14rem_1fr]">
              <span className="border-e border-b bg-muted/50" />
              <WindowDays from={from} today={today} />
              {groups.map(({ project, tasks }) => (
                <div key={project.id} className="contents">
                  <Link
                    href={`/projects/${project.id}`}
                    className="col-span-2 border-b bg-muted/30 px-2 py-1.5 text-sm font-medium hover:underline"
                  >
                    {project.name}
                  </Link>
                  {tasks.map((task) => {
                    const span = spanOf(task, from);

                    return (
                      <div key={task.id} className="contents">
                        <span className="flex items-center justify-between gap-2 border-e border-b px-2 py-1.5 ps-4">
                          <span className="truncate text-sm">{task.title}</span>
                          {span === null &&
                          task.startsOn === null &&
                          task.dueOn === null ? (
                            <Badge variant="outline">{t("No dates")}</Badge>
                          ) : null}
                        </span>
                        <div className="grid grid-cols-28 items-center border-b">
                          {span === null ? null : (
                            <span
                              title={t("{start} to {end}", {
                                start: task.startsOn ?? task.dueOn ?? "",
                                end: task.dueOn ?? task.startsOn ?? "",
                              })}
                              className={`[grid-column:var(--span-start)/span_var(--span-length)] h-4 rounded-sm ${BAR[task.status]}`}
                              // SAFETY: React's CSSProperties has no custom properties; both are numbers.
                              style={
                                {
                                  "--span-start": span.start + 1,
                                  "--span-length": span.length,
                                } as CSSProperties
                              }
                            />
                          )}
                        </div>
                      </div>
                    );
                  })}
                </div>
              ))}
            </div>
          </div>
        </div>
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Record Gantt chart" in a React web app that uses shadcn/ui components. A row per record grouped by its parent, with a bar for its planned dates and a marker for rows without dates.

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 badge 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-gantt.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-gantt.md
- Block docs: https://bettersupabase.com/docs/blocks/calendar
```
