Booking
Drop-in slot booking system with calendar picker, server-enforced availability, conflict detection, and cancellation — pure Prisma and Server Actions, no external scheduling service.
@prisma/client@prisma/adapter-pgpgdotenvzustandzoddate-fns@date-fns/tzprisma@types/pg
shadcn/uicalendarbuttonbadgecardskeletontabsauto-installed by CLI
Preview
Add these to your
.env before installingThe CLI checks for these at install time — missing values mean the database step gets skipped, and the feature won't work until it's added.
DATABASE_URLrequiredPostgreSQL connection string.
e.g. postgresql://user:pass@localhost:5432/mydb
Install via CLI
npx feature101@latest add bookingImport
import { BookingWidget, BookingList } from '@/features/booking'Usage
<BookingWidget resourceId="room-42" resourceName="Conference Room" availableFrom="09:00" availableTo="17:00" slotDurationMinutes={60} /><BookingList onCancelConfirmed={(id) => console.log(id)} />PostInstall Guides
- 01availableFrom / availableTo are UTC — adjust generateSlots in BookingWidget.tsx if your app needs local timezone support
- 02resourceId is opaque — the primitive doesn't model what a resource is, pass any ID from your app
- 03BookingList also accepts className and onCancelConfirmed?: (bookingId: string) => void, fired on confirm only
- 04Visit
/bookingafter install to see both components composed on a real page — delete app/booking/page.tsxonce you're done evaluating
Props
| Prop | Type | Default | Description |
|---|---|---|---|
resourceId* | string | — | Opaque ID for the thing being booked — a userId, roomId, serviceId, or any stable string from your app. |
resourceName* | string | — | Display name shown in the booking card header. |
availableFrom* | string | — | Start of bookable window in "HH:mm" UTC format (e.g. "09:00"). Enforced server-side — not just a UI hint. |
availableTo* | string | — | End of bookable window in "HH:mm" UTC format (e.g. "17:00"). Enforced server-side — not just a UI hint. |
slotDurationMinutes* | number | — | Slot length in minutes (e.g. 30, 60). Server validates that booked slots match this duration exactly. |
onBookingConfirmed | (booking: Booking) => void | — | Callback fired with real server result on confirm only. Never fires optimistically. |
className | string | — | Custom CSS classes for the card wrapper (BookingWidget). |
Data Flow
client
What the user clicks and types.
zustand
Updates the screen instantly, before the server replies.
server
Validates and saves the change securely.
prisma
Your data, permanently written to the database.
client
What the user clicks and types.
zustand
Updates the screen instantly, before the server replies.
server
Validates and saves the change securely.
prisma
Your data, permanently written to the database.
Files12
Files
app/booking/page.tsx
import { BookingWidget, BookingList } from "@/features/booking";
export default function BookingPage() {
return (
<main className="mx-auto w-full max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
<div className="mb-10 space-y-1.5">
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
Book a Resource
</h1>
<p className="text-muted-foreground">
Pick a time, manage what you've booked.
</p>
<p className="text-sm text-muted-foreground">
This page is shipped by feature101 for a quick demo of the booking
primitive. Delete it once you're done.
</p>
</div>
<div className="flex flex-col gap-8 lg:flex-row lg:items-start lg:gap-10">
<div className="lg:flex-1">
<BookingWidget
resourceId="demo-resource"
resourceName="Demo Resource"
availableFrom="09:00"
availableTo="17:00"
slotDurationMinutes={60}
/>
</div>
<div className="lg:w-[380px] lg:shrink-0">
<BookingList />
</div>
</div>
</main>
);
}