# Calendar view

> A month and week calendar of events with a dialog to add one.

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

## Install the block

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

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
export async function CalendarView({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const session = await getSession();

  if (!can(session, "calendar.read")) {
    forbidden();
  }

  const [t, format, query] = await Promise.all([
    getExtracted("calendar"),
    getFormatter(),
    searchParams,
    connection(),
  ]);

  const canWrite = can(session, "calendar.write");
  const canManage = can(session, "calendar.manage");
  const first = firstOfMonth(query["month"], "UTC");
  const weeks = weeksOf(first);
  const from = weeks[0]?.[0] ?? first;
  const until = (weeks.at(-1)?.[6] ?? first).add({ days: 1 });

  const month = await settle(() =>
    loadCalendarMonth(from.toString(), until.toString()),
  );

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

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

  const byDay = new Map<string, MonthEvent[]>();

  if (month.kind === "ok") {
    for (const event of month.data.events) {
      byDay.set(event.day, [...(byDay.get(event.day) ?? []), event]);
    }
  }

  const today =
    month.kind === "ok"
      ? Temporal.Now.plainDateISO(month.data.tz).toString()
      : null;

  const calendars = month.kind === "ok" ? month.data.calendars : [];

  return (
    <Card data-recipe="calendar-view">
      <CardHeader>
        <CardTitle>
          {label(first, { month: "long", year: "numeric" })}
        </CardTitle>
        <CardDescription>
          {month.kind === "ok" && calendars.length > 0
            ? t("{count, plural, one {# event} other {# events}} in view", {
                count: month.data.events.length,
              })
            : t("Events of your organization by day.")}
        </CardDescription>
        <CardAction className="flex items-center gap-2">
          <Link
            href={`/calendar?month=${monthKey(first.subtract({ months: 1 }))}`}
            aria-label={t("Previous month")}
            className={buttonVariants({ variant: "outline", size: "icon-sm" })}
          >
            <ChevronLeftIcon />
          </Link>
          <Link
            href="/calendar"
            className={buttonVariants({ variant: "outline", size: "sm" })}
          >
            {t("Today")}
          </Link>
          <Link
            href={`/calendar?month=${monthKey(first.add({ months: 1 }))}`}
            aria-label={t("Next month")}
            className={buttonVariants({ variant: "outline", size: "icon-sm" })}
          >
            <ChevronRightIcon />
          </Link>
          {canWrite && calendars.length > 0 ? (
            <CreateEventDialog
              calendars={calendars}
              defaultDate={today ?? first.toString()}
            />
          ) : null}
        </CardAction>
      </CardHeader>
      <CardContent>
        {month.kind === "error" ? (
          <CalendarLoadError title={t("The calendar didn't load.")} />
        ) : month.kind === "no-organization" || calendars.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <CalendarIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No calendar yet")}</EmptyTitle>
              <EmptyDescription>
                {month.kind === "no-organization"
                  ? t("Pick an organization to see its calendar.")
                  : t("Create the team calendar, then add events to it.")}
              </EmptyDescription>
            </EmptyHeader>
            {month.kind === "ok" && canManage ? (
              <EmptyContent>
                <CreateCalendarButton />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <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 events = 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",
                        )}
                      >
                        {key === today ? (
                          <Badge variant="default">{day.day}</Badge>
                        ) : (
                          day.day
                        )}
                      </span>
                      {events.slice(0, SHOWN_PER_DAY).map((event) => (
                        <span
                          key={event.key}
                          title={event.title}
                          className="truncate rounded-sm bg-primary/10 px-1.5 py-0.5 text-xs text-foreground"
                        >
                          {event.time ? (
                            <span className="text-muted-foreground tabular-nums">
                              {event.time}{" "}
                            </span>
                          ) : null}
                          {event.title}
                        </span>
                      ))}
                      {events.length > SHOWN_PER_DAY ? (
                        <span className="text-xs text-muted-foreground">
                          {t("{count} more", {
                            count: String(events.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 "Calendar view" in a React web app that uses shadcn/ui components. A month and week calendar of events with a dialog to add one.

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