# Booking page

> A public page that shows open slots and books one without overlaps.

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

## Install the block

```bash
npx better-supabase add bookings
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
/** The public page of a booking link: open slots for a week, then a form. */
export async function BookingPage({
  params,
  searchParams,
}: {
  params: Promise<{ token: string }>;
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const [{ token }, query, t, format] = await Promise.all([
    params,
    searchParams,
    getExtracted("bookings"),
    getFormatter(),
  ]);

  const parsed = v.safeParse(v.pipe(v.string(), v.isoDate()), query["from"]);
  const from = parsed.success ? parsed.output : undefined;
  const page = await settle(() => loadPublicBooking(token, from));

  if (page.kind === "missing") {
    notFound();
  }

  if (page.kind === "error") {
    return (
      <Card data-recipe="booking-page" className="w-full max-w-xl">
        <CardHeader>
          <CardTitle>{t("Book a time")}</CardTitle>
        </CardHeader>
        <CardContent>
          <BookingsLoadError title={t("The open times didn't load.")} />
        </CardContent>
      </Card>
    );
  }

  const { data } = page;

  const next = Temporal.PlainDate.from(data.from).add({ days: 7 }).toString();

  const days = data.days.map((day) => ({
    ...day,
    label: format.dateTime(new Date(`${day.date}T12:00:00Z`), {
      weekday: "long",
      day: "numeric",
      month: "long",
      timeZone: "UTC",
    }),
  }));

  return (
    <Card data-recipe="booking-page" className="w-full max-w-xl">
      <CardHeader>
        <CardTitle>{data.serviceName}</CardTitle>
        <CardDescription>
          {t("{minutes} minutes with {host}. Times are in {tz}.", {
            minutes: String(data.minutes),
            host: data.hostName,
            tz: data.tz,
          })}
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-6">
        {days.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <CalendarXIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No open times this week")}</EmptyTitle>
              <EmptyDescription>
                {t("Look at the week after for an open time.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Link
                href={`/book/${token}?from=${next}`}
                className={buttonVariants({ variant: "outline" })}
              >
                {t("Next week")}
                <ChevronRightIcon />
              </Link>
            </EmptyContent>
          </Empty>
        ) : (
          <>
            <SlotPicker token={token} days={days} />
            <div className="flex justify-end">
              <Link
                href={`/book/${token}?from=${next}`}
                className={buttonVariants({ variant: "ghost", size: "sm" })}
              >
                {t("Next week")}
                <ChevronRightIcon />
              </Link>
            </div>
          </>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Booking page" in a React web app that uses shadcn/ui components. A public page that shows open slots and books one without overlaps.

It uses the better-supabase `bookings` block (Bookings). 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 bookings` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add 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/bookings/components/booking-page.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/booking-page.md
- Block docs: https://bettersupabase.com/docs/blocks/bookings
```
