import type { CalendlyBookingOffer, CalendlyBookingSlot, } from "./public-contracts.js"; interface BookingOfferCardOptions { offer: CalendlyBookingOffer; selectedSlot: CalendlyBookingSlot | null; isSubmitting: boolean; onSelectSlot(slot: CalendlyBookingSlot): void; onSubmitDetails(details: { name: string; email: string; timezone: string; }): void; } const MAX_QUICK_SLOTS = 6; const MAX_SLOTS_PER_DAY = 6; /** Creates the deterministic booking UI without exposing integration details. */ export function createBookingOfferCard( options: BookingOfferCardOptions, ): HTMLElement { const timezone = getBrowserTimezone(); const card = createElement("section", "booking-offer"); card.setAttribute("aria-label", "Scegli un appuntamento"); const title = createElement("strong", "booking-offer-title"); title.textContent = `Scegli un orario per ${options.offer.eventTypeName}`; const timezoneLabel = createElement("p", "booking-offer-timezone"); timezoneLabel.textContent = `Orari nel tuo fuso: ${timezone}`; const dateFilter = createDateFilter(options.offer, timezone); const slots = createElement("div", "booking-offer-slots"); const renderSlots = (dateKey: string): void => { const visibleSlots = dateKey === "" ? getQuickSlots(options.offer.availableTimes, timezone) : options.offer.availableTimes .filter( (slot) => getLocalDateKey(slot.startTime, timezone) === dateKey, ) .slice(0, MAX_SLOTS_PER_DAY); slots.replaceChildren( ...visibleSlots.map((slot) => createSlotButton(slot, timezone, options)), ); }; dateFilter.addEventListener("change", () => renderSlots(dateFilter.value)); renderSlots(""); card.append(title, timezoneLabel, dateFilter, slots); if (options.selectedSlot !== null) { card.append( createBookingDetailsForm(options, options.selectedSlot, timezone), ); } return card; } function createDateFilter( offer: CalendlyBookingOffer, timezone: string, ): HTMLSelectElement { const select = document.createElement("select"); select.className = "booking-date-filter"; select.setAttribute("aria-label", "Scegli un giorno diverso"); select.append(new Option("Proposte rapide", "")); const firstSlotByDate = new Map(); for (const slot of offer.availableTimes) { const date = getLocalDateKey(slot.startTime, timezone); if (!firstSlotByDate.has(date)) firstSlotByDate.set(date, slot); } const formatter = new Intl.DateTimeFormat(undefined, { weekday: "long", day: "numeric", month: "long", timeZone: timezone, }); for (const [date, firstSlot] of firstSlotByDate) { select.append( new Option(formatter.format(new Date(firstSlot.startTime)), date), ); } return select; } function createSlotButton( slot: CalendlyBookingSlot, timezone: string, options: BookingOfferCardOptions, ): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; button.className = "booking-slot-button"; button.disabled = options.isSubmitting; button.setAttribute( "aria-pressed", String(options.selectedSlot?.startTime === slot.startTime), ); button.textContent = new Intl.DateTimeFormat(undefined, { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", timeZone: timezone, }).format(new Date(slot.startTime)); button.addEventListener("click", () => options.onSelectSlot(slot)); return button; } function createBookingDetailsForm( options: BookingOfferCardOptions, selectedSlot: CalendlyBookingSlot, timezone: string, ): HTMLFormElement { const form = document.createElement("form"); form.className = "booking-details-form"; const summary = createElement("p", "booking-selected-summary"); summary.textContent = `Hai scelto ${formatFullDate(selectedSlot.startTime, timezone)}.`; const name = createTextInput("Nome", "name"); const email = createTextInput("Email", "email"); const submit = document.createElement("button"); submit.type = "submit"; submit.className = "booking-submit-button"; submit.disabled = options.isSubmitting; submit.textContent = options.isSubmitting ? "Preparazione…" : "Continua"; form.append(summary, name.label, email.label, submit); form.addEventListener("submit", (event) => { event.preventDefault(); if (!form.reportValidity()) return; options.onSubmitDetails({ name: name.input.value.trim(), email: email.input.value.trim(), timezone, }); }); return form; } function createTextInput( labelText: string, type: "email" | "name", ): { input: HTMLInputElement; label: HTMLLabelElement } { const label = document.createElement("label"); label.className = "booking-field"; const text = createElement("span", "booking-field-label"); text.textContent = labelText; const input = document.createElement("input"); input.type = type === "name" ? "text" : type; input.autocomplete = type; input.required = true; input.maxLength = type === "name" ? 120 : 320; label.append(text, input); return { input, label }; } function getQuickSlots( slots: CalendlyBookingSlot[], timezone: string, ): CalendlyBookingSlot[] { const firstSlotByDate = new Map(); for (const slot of slots) { const date = getLocalDateKey(slot.startTime, timezone); if (!firstSlotByDate.has(date)) firstSlotByDate.set(date, slot); if (firstSlotByDate.size === MAX_QUICK_SLOTS) break; } return [...firstSlotByDate.values()]; } function getLocalDateKey(value: string, timezone: string): string { return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", timeZone: timezone, }).format(new Date(value)); } function formatFullDate(value: string, timezone: string): string { return new Intl.DateTimeFormat(undefined, { weekday: "long", day: "numeric", month: "long", hour: "2-digit", minute: "2-digit", timeZone: timezone, }).format(new Date(value)); } function getBrowserTimezone(): string { return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } function createElement(tag: string, className: string): HTMLElement { const element = document.createElement(tag); element.className = className; return element; }