RegistryUI recipes
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; the bookings block must be installed (npx better-supabase add bookings). Its page on bettersupabase.com is /recipes/booking-page.
Web, with shadcn components:
npx shadcn add button card empty skeleton/** 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>
);
}Last updated on