import { h, render } from "https://esm.sh/preact@10"; import { useState, useEffect, useRef, useMemo } from "https://esm.sh/preact@10/hooks"; import { html } from "https://esm.sh/htm@3/preact"; const TYPES = [ { id: "diagnostic", label: "Diagnostic Assessment", duration: "60 MINS" }, { id: "sports", label: "Sports Rehabilitation", duration: "60 MINS" }, { id: "surgical", label: "Surgery Rehabilitation", duration: "60 MINS" }, { id: "neuro", label: "Neurological Therapy", duration: "60 MINS" }, ]; const CHECK = html` `; const EMPTY_FORM = { lastName: "", phone: "", comments: "" }; function localDateStr(d) { return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); } function nextBusinessDay() { var d = new Date(); d.setDate(d.getDate() + 1); while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1); return localDateStr(d); } function slotLabel(date) { var h = date.getHours(); return (h % 12 || 12) + (h < 12 ? "AM" : "PM") + " PST"; } function Booking({ settings }) { const [service, setService] = useState("diagnostic"); const [slots, setSlots] = useState([]); const [selectedSlotId, setSelectedSlotId] = useState(null); const [formData, setFormData] = useState(EMPTY_FORM); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); const calEl = useRef(null); const calRef = useRef(null); const initializedRef = useRef(false); const prevServiceRef = useRef(null); const initDate = useMemo(nextBusinessDay, []); function buildEventsUrl(svc) { return settings.eventsUrl + "?service=" + svc; } useEffect(function () { if (!calEl.current || !window.FullCalendar) return; function markDays(events) { calEl.current.querySelectorAll(".fc-daygrid-day.has-availability").forEach(function (d) { d.classList.remove("has-availability"); }); events.forEach(function (event) { var dateStr = event.startStr.substring(0, 10); var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + dateStr + "\"]"); if (dayEl) dayEl.classList.add("has-availability"); }); } var cal = new FullCalendar.Calendar(calEl.current, { initialView: "dayGridMonth", initialDate: initDate, headerToolbar: { left: "prev", center: "title", right: "next" }, titleFormat: { year: "numeric", month: "long" }, dayHeaderFormat: { weekday: "narrow" }, validRange: function (now) { return { start: new Date(now.getFullYear(), now.getMonth(), 1), end: new Date(now.getFullYear(), now.getMonth() + 7, 1), }; }, fixedWeekCount: false, height: "auto", eventDisplay: "none", dayMaxEvents: false, datesSet: function () { calEl.current.querySelectorAll(".fc-daygrid-day.is-selected").forEach(function (d) { d.classList.remove("is-selected"); }); setSlots([]); setSelectedSlotId(null); }, eventsSet: function (events) { markDays(events); if (!initializedRef.current) { initializedRef.current = true; var dates = [...new Set(events.map(function (e) { return e.startStr.substring(0, 10); }))] .filter(function (d) { return d >= initDate; }) .sort(); var firstDate = dates[0]; if (firstDate) { var targetEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + firstDate + "\"]"); if (targetEl) { targetEl.classList.add("is-selected"); setSlots( events .filter(function (e) { return e.startStr.startsWith(firstDate); }) .sort(function (a, b) { return a.start - b.start; }) ); } } } }, dayCellClassNames: function (arg) { var date = arg.date.toISOString().substring(0, 10); if (settings.holidays[date]) return ["is-holiday"]; }, dateClick: function (arg) { if (!arg.dayEl.classList.contains("has-availability")) return; calEl.current.querySelectorAll(".fc-daygrid-day.is-selected").forEach(function (d) { d.classList.remove("is-selected"); }); arg.dayEl.classList.add("is-selected"); setSelectedSlotId(null); setSubmitError(null); setSlots( cal.getEvents() .filter(function (e) { return e.startStr.startsWith(arg.dateStr); }) .sort(function (a, b) { return a.start - b.start; }) ); }, }); cal.render(); calRef.current = cal; return function () { cal.destroy(); }; }, []); useEffect(function () { var cal = calRef.current; if (!cal) return; var isInitial = prevServiceRef.current === null; prevServiceRef.current = service; if (!isInitial) { initializedRef.current = false; setSlots([]); setSelectedSlotId(null); setFormData(EMPTY_FORM); setSubmitError(null); cal.gotoDate(initDate); } cal.removeAllEventSources(); cal.addEventSource(buildEventsUrl(service)); }, [service]); function handleSlotClick(slot) { setSelectedSlotId(slot.id); setSubmitError(null); } function handleFormChange(field, value) { setFormData(function (prev) { return Object.assign({}, prev, { [field]: value }); }); } function handleSubmit(e) { e.preventDefault(); var slot = slots.find(function (s) { return s.id === selectedSlotId; }); if (!slot) return; setSubmitting(true); setSubmitError(null); fetch(settings.storeSlotUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ start: slot.startStr, end: slot.endStr, service: service, lastName: formData.lastName, phone: formData.phone, comments: formData.comments, }), }).then(function (res) { if (res.ok) { window.location.href = settings.bookingUrl; } else { setSubmitting(false); setSubmitError("Something went wrong. Please try again."); } }).catch(function () { setSubmitting(false); setSubmitError("Something went wrong. Please try again."); }); } var selectedSlot = slots.find(function (s) { return s.id === selectedSlotId; }); var inputClass = "w-full border border-pt-blue-200 bg-white px-3 py-2 text-gray-900 text-sm focus:outline-none focus:border-pt-blue-500 transition-colors"; var labelClass = "block text-sm font-medium text-gray-700 mb-1"; return html`

Select Appointment Type

${TYPES.map(function (t) { var active = service === t.id; return html` `; })}
${slots.length > 0 ? html`
${slots.map(function (slot) { return html` `; })}
` : null}
${selectedSlot ? html`

Your Details

${submitError ? html`

${submitError}

` : null}
` : null}
`; } class RptBooking extends HTMLElement { connectedCallback() { render(html`<${Booking} settings=${window.drupalSettings.riversidePt} />`, this); } disconnectedCallback() { render(null, this); } } customElements.define("rpt-booking", RptBooking);