Booking page
A public page that shows open slots and books one without overlaps. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add button card empty skeletonInstall the glue
Install the block with
npx better-supabase add bookings. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the screen
React and shadcn
View in the examplebooking-page.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> ); }