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 installing

The 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_URLrequired

    PostgreSQL connection string.

    e.g. postgresql://user:pass@localhost:5432/mydb

Install via CLI

npx feature101@latest add booking

Import

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

  1. 01availableFrom / availableTo are UTC — adjust generateSlots in BookingWidget.tsx if your app needs local timezone support
  2. 02resourceId is opaque — the primitive doesn't model what a resource is, pass any ID from your app
  3. 03BookingList also accepts className and onCancelConfirmed?: (bookingId: string) => void, fired on confirm only
  4. 04Visit /booking after install to see both components composed on a real page — delete app/booking/page.tsx once you're done evaluating

Props

PropTypeDescription
resourceId*
stringOpaque ID for the thing being booked — a userId, roomId, serviceId, or any stable string from your app.
resourceName*
stringDisplay name shown in the booking card header.
availableFrom*
stringStart of bookable window in "HH:mm" UTC format (e.g. "09:00"). Enforced server-side — not just a UI hint.
availableTo*
stringEnd of bookable window in "HH:mm" UTC format (e.g. "17:00"). Enforced server-side — not just a UI hint.
slotDurationMinutes*
numberSlot length in minutes (e.g. 30, 60). Server validates that booked slots match this duration exactly.
onBookingConfirmed
(booking: Booking) => voidCallback fired with real server result on confirm only. Never fires optimistically.
className
stringCustom 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.

Files12
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&apos;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&apos;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>
  );
}