package main
import (
"fmt"
"strconv"
)
// CalcomBookingWidget renders the main booking widget container.
//
// The widget is divided into four regions, each with a stable ID so HTMX can
// swap them independently:
//
// - #calcom-steps-{id} step indicator (OOB-updated on every transition)
// - #calcom-date-grid-{id} the date picker
// - #calcom-slots-{id} time slots, loaded after a date click
// - #calcom-form-{id} booking form, loaded after a slot click
// - #calcom-confirm-{id} success panel, populated after submit
//
// Each partial returned by the server emits hx-swap-oob directives to keep
// the step indicator in sync with the user's progress, and to clear stale
// regions when the user changes their mind (Change date / Change time) or
// finishes (post-confirmation).
templ CalcomBookingWidget(config BookingConfig) {
// Booking times render server-side in the MEETING's timezone (the
// event's Cal.com availability schedule) — the visitor's browser
// timezone is deliberately never detected or sent. Client-side
// timezone plumbing is what caused the 2026-06-10 incident: a hidden
// input's "UTC" default never got updated, every hx-include sent
// timezone=UTC, and visitors saw UTC slot times.
//
// This script only:
// - captures the browser locale for phone dial-code inference
// - wires the date-grid selection highlight
// - drives the phone country combobox
//
// Init is deferred to DOMContentLoaded: the locale input sits after
// this script in the widget and doesn't exist while it parses.
// Admin-authored fallback copy for a non-fixable booking failure
// (Tier-2). Stamped once from block content here and hx-included into
// the POST /book request (like locale) so HandleCreateBooking can read
// it back. templ auto-escapes the value. Defaults to
// DefaultUnavailableMessage when the admin leaves it blank (resolved in
// block.go), so it is never empty.
if config.Title != "" {
{ config.Title }
}
if config.Description != "" {
{ config.Description }
}
// Step indicator — wrapped in a stable container so partial responses
// can replace its contents via hx-swap-oob.
@calcomStepIndicator(1)
if config.ShowTimezone {
All times shown in{ config.TimeZone }
}
// Saved-booking panel — normally hidden. The widget script reveals it on
// DOMContentLoaded when this visitor's localStorage holds a booking for
// this username/event, hiding the date grid below so they manage the
// existing booking (cancel / redo) instead of double-booking.
@calcomSavedBooking(config.BlockID)
// Date picker section — wrapped so confirmation can OOB-clear it.
@calcomDateGrid(config)
// Time slots section — populated after a date click.
// Booking form section — populated after a slot click.
// Confirmation section — populated after successful booking.
// Central loading indicator shown during HTMX requests.
}
// calcomStepIndicator renders the three-step progress indicator. `active` is
// the user's current step (1 = Select Date, 2 = Select Time, 3 = Confirm,
// 4 = Done — all three checkmarked). When active > n the step is rendered as
// completed; when active == n it is current; when active < n it is upcoming.
templ calcomStepIndicator(active int) {
}
// calcomStep renders one step pill inside the progress indicator.
templ calcomStep(n int, label string, active int) {
= n),
templ.KV("bg-muted text-muted-foreground", active < n),
}>
if active > n {
} else {
{ strconv.Itoa(n) }
}
{ label }
}
// calcomStepIndicatorOOB wraps calcomStepIndicator in the OOB-swap envelope
// so partial responses can update the indicator out-of-band.
templ calcomStepIndicatorOOB(blockID string, active int) {
@calcomStepIndicator(active)
}
// calcomClearRegion emits an empty container with the given ID and an OOB
// swap directive, used to wipe stale regions when the user transitions
// between flow steps (Change date / Change time / post-confirmation).
templ calcomClearRegion(id, extraClasses string) {
}
// calcomDateGrid renders the calendar date picker. The prev/next buttons fire
// hx-get against /api/plugins/calcomblock/date-grid, which returns a fresh
// instance of this template scoped to a new WeekStart — innerHTML-swapped
// into the #calcom-date-grid-{id} container in the widget root.
templ calcomDateGrid(config BookingConfig) {
if config.CanGoBack {
} else {
}
{ config.RangeLabel }
for _, day := range []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} {
{ day }
}
// Offset the first date to its weekday column. Without these pad
// cells a mid-week window pours in at the "Sun" column and every
// date renders under the wrong header — the second bidbuddy
// incident (2026-06-10): tapped the cell under "Tue", got Friday.
for i, pad := 0, leadingBlanks(config.Dates); i < pad; i++ {
}
for _, date := range config.Dates {
if date.IsPast {
} else {
}
}
}
// CalcomTimeSlots renders available time slots for a selected date. Returned
// as the body of GET /slots; also emits OOB updates for the step indicator
// (active=2) and an OOB clear of the form region so a previously-rendered
// form for an earlier slot doesn't linger.
templ CalcomTimeSlots(slots []TimeSlot, config SlotConfig) {
Available times for { config.SelectedDate }
if len(slots) == 0 {
No available times for this date. Please select another date.
} else {
for _, slot := range slots {
}
}
// OOB: advance step indicator to "Select Time" + clear any stale form
// from a previous slot selection.
@calcomStepIndicatorOOB(config.BlockID, 2)
@calcomClearRegion(fmt.Sprintf("calcom-form-%s", config.BlockID), "calcom-booking-form")
}
// CalcomBookingForm renders the contact form for booking. When the event type
// has custom bookingFields configured, each is rendered via bookingField. When
// the list is empty (typical for default event types or a transient fetch
// error), a built-in name + email pair is rendered so the flow still works.
//
// Returned as the body of GET /form. Also emits an OOB update advancing the
// step indicator to "Confirm".
templ CalcomBookingForm(formConfig FormConfig) {
Your Details
Booking for { formConfig.SelectedDate } at { formConfig.SelectedTime }
// hx-include pulls the widget-level hidden inputs into the POST: locale
// (phone dial-code inference) and unavailableMessage (admin-authored
// Tier-2 copy). Both live in the widget root, outside this form partial.
// OOB: advance step indicator to "Confirm".
@calcomStepIndicatorOOB(formConfig.BlockID, 3)
}
// bookingField renders a single Cal.com bookingField in the correct widget for
// its type. Unknown types fall back to a text input. defaultCountry is the ISO
// region pre-selected in the phone-field country combobox ("" = none).
templ bookingField(f BookingField, blockID string, defaultCountry string) {
switch f.Type {
case "textarea":
case "boolean", "checkbox":
{ f.Placeholder }
case "select":
case "multiselect":
for _, opt := range f.Options {
}
case "radio":
for _, opt := range f.Options {
}
case "phone":
for _, c := range countries {
{ countryFlag(c.Code) } { c.Name } ({ c.Dial })
}
default:
}
}
// CalcomConfirmation renders the success panel after a booking is created.
// Returned as the body of POST /book; also emits OOB clears that wipe the
// date grid + slots + form so the visitor is presented with only the
// confirmation (preventing accidental double-submits) and an OOB step-indicator
// update marking all three steps complete.
templ CalcomConfirmation(result BookingResult, blockID string) {
Booking Confirmed!
Your appointment has been scheduled for { result.FormattedDateTime }.
A confirmation email has been sent to { result.Email }.
if result.MeetingLink != "" {
Join meeting
}
// Machine-readable booking identity for the "manage your booking" feature.
// Hidden from view; the script below reads it and persists the booking to
// this visitor's localStorage so they can cancel / redo it on a later
// visit. templ auto-escapes every attribute value.
// Persist {uid, when, email, savedAt} under a STABLE key derived from the
// widget root's data-username + data-event-type (NOT the per-render
// blockId). HTMX evaluates this script when it swaps the confirmation in.
// Skips when uid is empty — the honeypot path renders a fake confirmation
// with no UID, which must never poison the saved-booking panel.
// OOB: mark all three steps complete and clear the upstream regions so
// the visitor cannot re-submit the form (prevents the double-booking
// footgun the adversarial review caught).
@calcomStepIndicatorOOB(blockID, 4)
@calcomClearRegion(fmt.Sprintf("calcom-date-grid-%s", blockID), "calcom-date-picker")
@calcomClearRegion(fmt.Sprintf("calcom-slots-%s", blockID), "calcom-time-slots mt-6")
@calcomClearRegion(fmt.Sprintf("calcom-form-%s", blockID), "calcom-booking-form")
}
// calcomSavedBooking renders the normally-hidden "manage your booking" panel.
// The widget script un-hides it (and hides the date grid) on DOMContentLoaded
// when this visitor's localStorage holds a booking for this username/event,
// filling in the date/time + email from storage. It offers two actions:
//
// - Cancel: an HTMX form POSTing /cancel with a hidden `uid` the script
// populates from localStorage; on a successful swap the script clears the
// storage key (htmx:afterRequest listener in the widget script).
// - Reschedule (redo): cancels the existing booking then reveals the grid to
// rebook — wired in the widget script to trigger the same Cancel POST, so
// the visitor can't end up double-booked.
//
// Cancellation is authorized by the Cal.com booking UID alone (the same
// capability model as Cal.com's emailed cancel link); the server rate-limits
// the POST by IP as the abuse guard.
templ calcomSavedBooking(blockID string) {
You have a booking
Confirmation sent to
// Cancel: HTMX form whose hidden uid the widget script fills from
// localStorage. Targets the confirm region; on success the script
// clears the localStorage key.
// Reschedule / redo: cancels the existing booking then reveals the
// grid to book again (handled in the widget script).
}
// CalcomCancelled is the body returned by POST /cancel on success. It swaps a
// "your booking was cancelled" message into #calcom-confirm-{id} and rewinds
// the step indicator to step 1. The date grid is restored client-side: when
// cancel fires from the saved panel the grid is only CSS-hidden (never removed),
// so the widget script's htmx:afterRequest listener re-shows it (showGrid) —
// avoiding a server-side grid re-render that would need the booking config the
// cancel POST doesn't carry. That same listener also clears the saved booking
// from localStorage and hides the saved panel.
templ CalcomCancelled(blockID string) {
Booking cancelled
Your booking has been cancelled. You can book another time below whenever you're ready.
// OOB: rewind the step indicator to step 1 (the grid is re-shown client-side).
@calcomStepIndicatorOOB(blockID, 1)
}
// CalcomReset is the body returned by GET /reset — empty slots region with
// an OOB clear of the form (so the user pressing "Change date" loses any
// stale form rendered from an earlier slot click) and an OOB rewind of the
// step indicator to step 1.
templ CalcomReset(blockID string) {
// Main response body — empty, replaces #calcom-slots-{id}.
// OOB: clear the form region and rewind the step indicator.
@calcomClearRegion(fmt.Sprintf("calcom-form-%s", blockID), "calcom-booking-form")
@calcomStepIndicatorOOB(blockID, 1)
}
// CalcomError renders an error message.
templ CalcomError(message string, blockID string, canRetry bool) {