TimeSlotPicker
Displays available time slots as a selectable grid or list for a chosen date.
Install
Section titled “Install”npx thebookingkit add time-slot-pickerimport { TimeSlotPicker } from "./components/time-slot-picker";
function TimeStep({ slots }) { const [selected, setSelected] = useState<Slot | null>(null);
return ( <TimeSlotPicker slots={slots} selectedSlot={selected} onSlotSelect={setSelected} /> );}interface TimeSlotPickerProps { /** Available slots for the selected date */ slots: Slot[]; /** Whether slots are being loaded */ isLoading?: boolean; /** Currently selected slot */ selectedSlot?: Slot | null; /** Callback when a slot is selected */ onSelect: (slot: Slot) => void; /** Time format: 12h or 24h */ timeFormat?: "12h" | "24h"; /** Group slots by period (Morning, Afternoon, Evening) */ groupByPeriod?: boolean; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: React.CSSProperties;}Behavior
Section titled “Behavior”- Renders each slot as a button showing the local start time
- Selected slot is visually highlighted
- Empty state message when no slots are available
- Scrollable container for dates with many slots
- Optional grouping by period (Morning/Afternoon/Evening)
- Optional loading skeleton state
Source
Section titled “Source”import React, { useMemo } from "react";import type { Slot } from "@thebookingkit/core";import { cn } from "../utils/cn.js";
/** Props for the TimeSlotPicker component */export interface TimeSlotPickerProps { /** Available slots for the selected date */ slots: Slot[]; /** Whether slots are being loaded */ isLoading?: boolean; /** Currently selected slot */ selectedSlot?: Slot | null; /** Callback when a slot is selected */ onSelect: (slot: Slot) => void; /** Time format: 12h or 24h */ timeFormat?: "12h" | "24h"; /** Group slots by period (Morning, Afternoon, Evening) */ groupByPeriod?: boolean; /** Additional CSS class name */ className?: string; /** Inline styles */ style?: React.CSSProperties;}
interface SlotGroup { label: string; slots: Slot[];}
/** * Displays available time slots for a selected date. * * Slots are shown as selectable buttons, optionally grouped * by period (Morning, Afternoon, Evening). * * @example * ```tsx * <TimeSlotPicker * slots={availableSlots} * isLoading={isLoading} * selectedSlot={selected} * onSelect={setSelected} * groupByPeriod * /> * ``` */export function TimeSlotPicker({ slots, isLoading = false, selectedSlot, onSelect, timeFormat = "12h", groupByPeriod = false, className, style,}: TimeSlotPickerProps) { const groups = useMemo(() => { if (!groupByPeriod) return [{ label: "", slots }];
const morning: Slot[] = []; const afternoon: Slot[] = []; const evening: Slot[] = [];
for (const slot of slots) { const hour = parseInt(slot.localStart.split("T")[1]?.split(":")[0] ?? "0", 10); if (hour < 12) { morning.push(slot); } else if (hour < 17) { afternoon.push(slot); } else { evening.push(slot); } }
const result: SlotGroup[] = []; if (morning.length > 0) result.push({ label: "Morning", slots: morning }); if (afternoon.length > 0) result.push({ label: "Afternoon", slots: afternoon }); if (evening.length > 0) result.push({ label: "Evening", slots: evening }); return result; }, [slots, groupByPeriod]);
if (isLoading) { return ( <div className={cn("tbk-timeslot-picker tbk-loading", className)} style={style}> <div className="tbk-skeleton" role="status" aria-label="Loading time slots"> {Array.from({ length: 6 }).map((_, i) => ( <div key={i} className="tbk-skeleton-slot" /> ))} </div> </div> ); }
if (slots.length === 0) { return ( <div className={cn("tbk-timeslot-picker tbk-empty", className)} style={style}> <p>No times available for this date. Please select another date.</p> </div> ); }
return ( <div className={cn("tbk-timeslot-picker", className)} style={style}> {groups.map((group) => ( <div key={group.label} className="tbk-slot-group"> {group.label && ( <h3 className="tbk-slot-group-label">{group.label}</h3> )} <div className="tbk-slot-grid"> {group.slots.map((slot) => { const isSelected = selectedSlot?.startTime === slot.startTime; return ( <button key={slot.startTime} type="button" className={cn( "tbk-slot-button", isSelected && "tbk-slot-selected", )} onClick={() => onSelect(slot)} aria-pressed={isSelected} aria-label={`Select ${formatTime(slot.localStart, timeFormat)}`} > {formatTime(slot.localStart, timeFormat)} </button> ); })} </div> </div> ))} </div> );}
function formatTime(localStart: string, format: "12h" | "24h"): string { const timePart = localStart.split("T")[1]; // "HH:mm:ss" if (!timePart) return localStart;
const [hours, minutes] = timePart.split(":").map(Number);
if (format === "24h") { return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; }
const period = hours >= 12 ? "PM" : "AM"; const h12 = hours % 12 || 12; return `${h12}:${String(minutes).padStart(2, "0")} ${period}`;}