Skip to content

Building Blocks

When neither the generic nor the restaurant widget fits your use case, compose the flow yourself. Every step component is exported under @periscaleai/booking-widget/blocks and (for restaurants) @periscaleai/booking-widget/restaurant.

ts
import {
  CalendarPicker,
  TimeSlotGrid,
  BookingDetailsForm,
  BookingConfirmation,
  MyBookings,
  StepIndicator,
  TabBar,
  BackButton,
} from "@periscaleai/booking-widget/blocks";

Step components

ComponentInputsOutput
CalendarPickerselectedDate, maxAdvanceDays, onSelect(date)Calls onSelect with "YYYY-MM-DD" when the user clicks a day.
TimeSlotGridclient, date, serviceId?, selectedSlot, onSelect(slot)Loads slots from client.getSlots(date) and lets the user pick one.
BookingDetailsFormclient, date, slot, service?, onConfirmed(appointment)Renders a name/phone/email/notes form and calls client.createAppointment.
BookingConfirmationappointment, config, onViewMyBookings, onBookAnotherSuccess screen.
MyBookingsclient, configPhone-lookup + cancel UI. Reads client.getMyAppointments(phone) and client.cancelAppointment(id, reason).

Shell primitives

ComponentPurpose
StepIndicator<K>Generic step indicator. Pass steps: { key: K; label: string }[] and currentStep: K.
TabBar<K>Tab strip with icons. Pass tabs, active, onChange.
BackButtonBack arrow + label. Pass onClick.

Example: a salon flow with a stylist picker

tsx
import * as React from "react";
import { Calendar, ListChecks } from "lucide-react";
import {
  CalendarPicker,
  TimeSlotGrid,
  BookingDetailsForm,
  BookingConfirmation,
  StepIndicator,
  TabBar,
  BackButton,
} from "@periscaleai/booking-widget/blocks";
import { createBookingClient } from "@periscaleai/booking-widget/client";
import type { Appointment, TimeSlot } from "@periscaleai/booking-widget";

import { StylistPicker } from "./StylistPicker"; // your custom step

type Step = "date" | "time" | "stylist" | "details" | "confirm";

const STEPS: { key: Step; label: string }[] = [
  { key: "date", label: "Date" },
  { key: "time", label: "Time" },
  { key: "stylist", label: "Stylist" },
  { key: "details", label: "Details" },
];

export function SalonBookingWidget({ apiBase, apiKey }: { apiBase: string; apiKey: string }) {
  const client = React.useMemo(() => createBookingClient(apiBase, { apiKey }), [apiBase, apiKey]);

  const [tab, setTab] = React.useState<"reserve" | "my-bookings">("reserve");
  const [step, setStep] = React.useState<Step>("date");
  const [date, setDate] = React.useState<string | null>(null);
  const [slot, setSlot] = React.useState<TimeSlot | null>(null);
  const [stylistId, setStylistId] = React.useState<string | null>(null);
  const [confirmed, setConfirmed] = React.useState<Appointment | null>(null);

  return (
    <div className="rounded-2xl bg-white shadow-lg overflow-hidden">
      <TabBar
        tabs={[
          { key: "reserve", label: "Book", icon: <Calendar className="h-4 w-4" /> },
          { key: "my-bookings", label: "My visits", icon: <ListChecks className="h-4 w-4" /> },
        ]}
        active={tab}
        onChange={setTab}
      />

      <div className="p-6">
        {step !== "date" && step !== "confirm" && (
          <BackButton onClick={() => /* …step-back logic… */ undefined} />
        )}
        {step !== "confirm" && <StepIndicator steps={STEPS} currentStep={step} />}

        {step === "date" && (
          <CalendarPicker
            selectedDate={date}
            maxAdvanceDays={30}
            onSelect={(d) => { setDate(d); setStep("time"); }}
          />
        )}

        {step === "time" && date && (
          <TimeSlotGrid
            client={client}
            date={date}
            selectedSlot={slot}
            onSelect={(s) => { setSlot(s); setStep("stylist"); }}
          />
        )}

        {step === "stylist" && date && slot && (
          <StylistPicker
            date={date}
            slot={slot}
            selected={stylistId}
            onSelect={(id) => { setStylistId(id); setStep("details"); }}
          />
        )}

        {step === "details" && date && slot && stylistId && (
          <BookingDetailsForm
            client={client}
            date={date}
            slot={slot}
            service={null}
            onConfirmed={(apt) => { setConfirmed(apt); setStep("confirm"); }}
          />
        )}

        {/* …confirmation, my-bookings tab… */}
      </div>
    </div>
  );
}

Adding a new industry flavor to the package

If your industry pattern is reusable across customers, contribute it back as a new flavor:

  1. Create src/<vertical>/types.ts — extend Appointment with vertical-specific fields.
  2. Create src/<vertical>/client.ts — extend BookingClient with vertical endpoints.
  3. Add components in src/<vertical>/components/ — compose with CalendarPicker, TimeSlotGrid, StepIndicator, TabBar, BackButton.
  4. Add a top-level widget wiring it together.
  5. Re-export via src/<vertical>/index.ts.
  6. Add ./<vertical> to package.json exports and tsup.config.ts entries.
  7. Update docs with a new page.

The restaurant flavor is the canonical reference — see src/restaurant/.

© Periscale