# Record calendar

> Rows on a month grid by a date column, with the month in the URL and a count of rows without a date.

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

## 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-calendar.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/projects/components/task-calendar.tsx):

```tsx
const SHOWN_PER_DAY = 3;

/**
 * Tasks on a month grid by their due date. The month lives in the URL
 * (`?month=YYYY-MM`), so the arrows are links and the page stays a server
 * component.
 */
export async function TaskCalendar({
  rows,
  first,
  today,
  monthHref,
}: {
  rows: readonly TaskRow[];
  first: Temporal.PlainDate;
  today: Temporal.PlainDate;
  monthHref: (month: string) => string;
}) {
  const [t, format] = await Promise.all([
    getExtracted("projects"),
    getFormatter(),
  ]);

  const weeks = weeksOf(first);
  const byDay = new Map<string, TaskRow[]>();

  for (const row of rows) {
    if (row.dueOn !== null) {
      byDay.set(row.dueOn, [...(byDay.get(row.dueOn) ?? []), row]);
    }
  }

  const label = (date: Temporal.PlainDate, options: DateTimeFormatOptions) =>
    format.dateTime(new Date(`${date.toString()}T12:00:00Z`), {
      ...options,
      timeZone: "UTC",
    });

  const unscheduled = rows.filter((row) => row.dueOn === null).length;

  const monthOf = (date: Temporal.PlainDate) =>
    date.toPlainYearMonth().toString();

  return (
    <Card data-recipe="record-calendar">
      <CardHeader>
        <CardTitle>
          {label(first, { month: "long", year: "numeric" })}
        </CardTitle>
        <CardDescription>
          {t(
            "{count, plural, =0 {Every task has a due date.} one {# task has no due date.} other {# tasks have no due date.}}",
            { count: unscheduled },
          )}
        </CardDescription>
        <CardAction className="flex items-center gap-2">
          <Link
            href={monthHref(monthOf(first.subtract({ months: 1 })))}
            aria-label={t("Previous month")}
            className={buttonVariants({ variant: "outline", size: "icon-sm" })}
          >
            <ChevronLeftIcon />
          </Link>
          <Link
            href={monthHref(monthOf(today))}
            className={buttonVariants({ variant: "outline", size: "sm" })}
          >
            {t("Today")}
          </Link>
          <Link
            href={monthHref(monthOf(first.add({ months: 1 })))}
            aria-label={t("Next month")}
            className={buttonVariants({ variant: "outline", size: "icon-sm" })}
          >
            <ChevronRightIcon />
          </Link>
        </CardAction>
      </CardHeader>
      <CardContent>
        <div className="overflow-x-auto">
          <div className="min-w-[42rem] overflow-hidden rounded-lg border">
            <div
              aria-hidden="true"
              className="grid grid-cols-7 border-b bg-muted/50"
            >
              {weeks[0]?.map((day) => (
                <span
                  key={day.toString()}
                  className="px-2 py-1.5 text-xs font-medium text-muted-foreground"
                >
                  {label(day, { weekday: "short" })}
                </span>
              ))}
            </div>
            <ol
              aria-label={label(first, { month: "long", year: "numeric" })}
              className="grid grid-cols-7"
            >
              {weeks.flat().map((day) => {
                const key = day.toString();
                const tasks = byDay.get(key) ?? [];
                const outside = day.month !== first.month;

                return (
                  <li
                    key={key}
                    className={cn(
                      "flex min-h-24 flex-col gap-1 border-e border-b p-1.5 [&:nth-child(7n)]:border-e-0",
                      outside && "bg-muted/30",
                    )}
                  >
                    <span className="sr-only">
                      {label(day, { dateStyle: "full" })}
                    </span>
                    <span
                      aria-hidden="true"
                      className={cn(
                        "text-xs",
                        outside ? "text-muted-foreground" : "font-medium",
                      )}
                    >
                      {day.equals(today) ? (
                        <Badge variant="default">{day.day}</Badge>
                      ) : (
                        day.day
                      )}
                    </span>
                    {tasks.slice(0, SHOWN_PER_DAY).map((task) => (
                      <Link
                        key={task.id}
                        href={`/projects/${task.projectId}`}
                        title={task.title}
                        className={cn(
                          "truncate rounded-sm bg-primary/10 px-1.5 py-0.5 text-xs text-foreground transition-colors hover:bg-primary/20",
                          task.status === "done" &&
                            "text-muted-foreground line-through",
                        )}
                      >
                        {task.title}
                      </Link>
                    ))}
                    {tasks.length > SHOWN_PER_DAY ? (
                      <span className="text-xs text-muted-foreground">
                        {t("{count} more", {
                          count: String(tasks.length - SHOWN_PER_DAY),
                        })}
                      </span>
                    ) : null}
                  </li>
                );
              })}
            </ol>
          </div>
        </div>
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Record calendar" in a React web app that uses shadcn/ui components. Rows on a month grid by a date column, with the month in the URL and a count of rows without a date.

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