02examples
Booking calendar
A Cal.com-style availability picker — busy time hatched and refused by constraints, click a free slot to book.
Three days of a host's calendar, clipped to 9–18. The hatched bands are her busy time — not events, just refused time the grid draws and defends. Click any free half-hour and a green draft appears with a confirm card beside it; drag or resize the draft to adjust it, then confirm. Try dragging the draft into a hatch: the snap holds it out.
liveBooking calendar
loading
uses
How it works
- The host's busy time is a
BusyCalendar— windows keyed by ISO date,"2026-09-02": [["12:00", "13:00"]]— the shape an availability endpoint returns.hostConstraintsfolds it into oneconstraintsobject:minDateblocks the past, andisSlotDisabledrefuses those windows, anything outside 9–17, and every confirmed booking. The grid hatches the refused time and refuses create, move, resize and slot clicks into it — no placeholder "Busy" events anywhere. - The constraints are rebuilt from
busyandeventsin auseMemo, so the moment a booking is confirmed its time joins the hatch for the next visitor. onSlotClickfires for a click on free canvas, already snapped. The example re-checks the full 30-minute call withcreateConstraints+slotAllowed— the same pure helpers the grid runs — because a click near the end of the day proposes an end the clicked slot alone can't vouch for.dayStartHour={9}/dayEndHour={18}clip the grid;workingHourstints the bookable band inside it (Settings).snapMinutes={30}andminEventMinutes={30}make every drag land on a half-hour.events/onEventsChangekeep the array in React state (Controlled state): confirmed bookings arereadOnlyevents, and whatever else the grid hands back — a drag-create, a moved draft — folds into the one greendraft.components={{ EventPanel: () => null }}swaps the built-in editor for nothing; the confirm card beside the grid is the panel (Slots). The card is aFormofFields, so a missing name or a malformed address is answered inline, under the input.view={3}withresponsive={false}keeps three columns at every width, and the nav strip (Navinparts.tsx) is built onuseKloq()(Toolbar & chrome).
Take it further
- Feed
isSlotDisabledfrom a fetch keyed ononRangeChange, so paging to next week loads that week's busy windows — the pattern is on the constraints page. - Draw the grid in the host's zone with
displayTimeZone="Europe/London"and add the visitor's as a gutter column withtimeZones(Time zones). - On confirm,
POSTthe booking and hand the visitor a.icswithdownloadICS(booking)(ICS).