diff --git a/web/modules/custom/riverside_pt/css/app.css b/web/modules/custom/riverside_pt/css/app.css index 59b2489..8bfc469 100644 --- a/web/modules/custom/riverside_pt/css/app.css +++ b/web/modules/custom/riverside_pt/css/app.css @@ -729,6 +729,18 @@ html { margin-top: 2vw; } +.mb-1{ + margin-bottom: 0.25rem; +} + +.mb-6{ + margin-bottom: 1.5rem; +} + +.mt-8{ + margin-top: 2rem; +} + .block{ display: block; } @@ -882,6 +894,10 @@ html { cursor: pointer; } +.resize-none{ + resize: none; +} + .resize{ resize: both; } @@ -971,6 +987,10 @@ html { row-gap: 0px; } +.gap-y-5{ + row-gap: 1.25rem; +} + .self-end{ align-self: flex-end; } @@ -1027,18 +1047,6 @@ html { border-top-width: 1px; } -.border-\[pt-blue-200\]{ - border-color: pt-blue-200; -} - -.border-\[pt-blue-500\]{ - border-color: pt-blue-500; -} - -.border-\[pt-navy\]{ - border-color: pt-navy; -} - .border-gray-200{ --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); @@ -1049,25 +1057,16 @@ html { border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); } -.border-pt-blue-300{ - --tw-border-opacity: 1; - border-color: rgb(157 189 203 / var(--tw-border-opacity, 1)); -} - -.border-white{ - --tw-border-opacity: 1; - border-color: rgb(255 255 255 / var(--tw-border-opacity, 1)); -} - -.border-white\/60{ - border-color: rgb(255 255 255 / 0.6); -} - .border-pt-blue-200{ --tw-border-opacity: 1; border-color: rgb(184 212 220 / var(--tw-border-opacity, 1)); } +.border-pt-blue-300{ + --tw-border-opacity: 1; + border-color: rgb(157 189 203 / var(--tw-border-opacity, 1)); +} + .border-pt-blue-500{ --tw-border-opacity: 1; border-color: rgb(48 111 142 / var(--tw-border-opacity, 1)); @@ -1078,44 +1077,29 @@ html { border-color: rgb(30 58 95 / var(--tw-border-opacity, 1)); } -.bg-\[pt-blue-100\]{ - background-color: pt-blue-100; +.border-white{ + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity, 1)); } -.bg-\[pt-blue-400\]{ - background-color: pt-blue-400; -} - -.bg-\[pt-blue-500\]{ - background-color: pt-blue-500; -} - -.bg-\[pt-navy\]{ - background-color: pt-navy; +.border-white\/60{ + border-color: rgb(255 255 255 / 0.6); } .bg-current{ background-color: currentColor; } -.bg-transparent{ - background-color: transparent; -} - -.bg-white{ - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); -} - -.bg-white\/90{ - background-color: rgb(255 255 255 / 0.9); -} - .bg-pt-blue-100{ --tw-bg-opacity: 1; background-color: rgb(221 232 240 / var(--tw-bg-opacity, 1)); } +.bg-pt-blue-300{ + --tw-bg-opacity: 1; + background-color: rgb(157 189 203 / var(--tw-bg-opacity, 1)); +} + .bg-pt-blue-400{ --tw-bg-opacity: 1; background-color: rgb(134 170 182 / var(--tw-bg-opacity, 1)); @@ -1131,11 +1115,6 @@ html { background-color: rgb(30 58 95 / var(--tw-bg-opacity, 1)); } -.bg-pt-blue-300{ - --tw-bg-opacity: 1; - background-color: rgb(157 189 203 / var(--tw-bg-opacity, 1)); -} - .bg-pt-sage-400{ --tw-bg-opacity: 1; background-color: rgb(131 161 161 / var(--tw-bg-opacity, 1)); @@ -1146,6 +1125,19 @@ html { background-color: rgb(111 143 150 / var(--tw-bg-opacity, 1)); } +.bg-transparent{ + background-color: transparent; +} + +.bg-white{ + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); +} + +.bg-white\/90{ + background-color: rgb(255 255 255 / 0.9); +} + .bg-gradient-to-b{ background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); } @@ -1264,6 +1256,11 @@ html { padding-bottom: 1em; } +.px-3{ + padding-left: 0.75rem; + padding-right: 0.75rem; +} + .pb-1{ padding-bottom: 0.25rem; } @@ -1288,6 +1285,10 @@ html { padding-top: 1px; } +.pt-8{ + padding-top: 2rem; +} + .text-left{ text-align: left; } @@ -1430,13 +1431,8 @@ html { letter-spacing: 0.1em; } -.text-\[\#1e3a8a\]{ - --tw-text-opacity: 1; - color: rgb(30 58 138 / var(--tw-text-opacity, 1)); -} - -.text-\[pt-blue-500\]{ - color: pt-blue-500; +.text-\[blue-900\]{ + color: blue-900; } .text-gray-400{ @@ -1469,6 +1465,11 @@ html { color: rgb(17 24 39 / var(--tw-text-opacity, 1)); } +.text-pt-blue-500{ + --tw-text-opacity: 1; + color: rgb(48 111 142 / var(--tw-text-opacity, 1)); +} + .text-white{ --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); @@ -1482,13 +1483,9 @@ html { color: rgb(255 255 255 / 0.8); } -.text-\[blue-900\]{ - color: blue-900; -} - -.text-pt-blue-500{ +.text-red-500{ --tw-text-opacity: 1; - color: rgb(48 111 142 / var(--tw-text-opacity, 1)); + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); } .no-underline{ @@ -1507,6 +1504,10 @@ html { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.filter{ + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + .transition{ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; @@ -1545,42 +1546,18 @@ html { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } -.\[text-shadow\:-56\.21px_2\.55px_10\.22px_\#0000001A\]{ - text-shadow: -56.21px 2.55px 10.22px #0000001A; -} - .\[text-shadow\:-56\.21px_2\.55px_10\.22px_rgb\(0_0_0\/10\%\)\]{ text-shadow: -56.21px 2.55px 10.22px rgb(0 0 0/10%); } -.hover\:border-\[pt-blue-500\]:hover{ - border-color: pt-blue-500; -} - -.hover\:border-\[pt-blue-600\]:hover{ - border-color: pt-blue-600; -} - -.hover\:border-pt-blue-600:hover{ - --tw-border-opacity: 1; - border-color: rgb(31 90 110 / var(--tw-border-opacity, 1)); -} - .hover\:border-pt-blue-500:hover{ --tw-border-opacity: 1; border-color: rgb(48 111 142 / var(--tw-border-opacity, 1)); } -.hover\:bg-\[pt-blue-50\]:hover{ - background-color: pt-blue-50; -} - -.hover\:bg-\[pt-blue-600\]:hover{ - background-color: pt-blue-600; -} - -.hover\:bg-\[pt-sage-500\]:hover{ - background-color: pt-sage-500; +.hover\:border-pt-blue-600:hover{ + --tw-border-opacity: 1; + border-color: rgb(31 90 110 / var(--tw-border-opacity, 1)); } .hover\:bg-gray-100:hover{ @@ -1603,21 +1580,6 @@ html { background-color: rgb(111 143 150 / var(--tw-bg-opacity, 1)); } -.hover\:text-\[\#1e3a8a\]:hover{ - --tw-text-opacity: 1; - color: rgb(30 58 138 / var(--tw-text-opacity, 1)); -} - -.hover\:text-\[\#285a6e\]:hover{ - --tw-text-opacity: 1; - color: rgb(40 90 110 / var(--tw-text-opacity, 1)); -} - -.hover\:text-white:hover{ - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity, 1)); -} - .hover\:text-\[blue-900\]:hover{ color: blue-900; } @@ -1626,10 +1588,29 @@ html { color: pt-blue-600; } +.hover\:text-white:hover{ + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.focus\:border-pt-blue-500:focus{ + --tw-border-opacity: 1; + border-color: rgb(48 111 142 / var(--tw-border-opacity, 1)); +} + +.focus\:outline-none:focus{ + outline: 2px solid transparent; + outline-offset: 2px; +} + .disabled\:opacity-30:disabled{ opacity: 0.3; } +.disabled\:opacity-50:disabled{ + opacity: 0.5; +} + @media not all and (min-width: 768px){ .max-sm\:text-sm{ font-size: 0.875rem; @@ -1844,10 +1825,6 @@ html { justify-content: center; } - .\32xl\:bg-\[pt-blue-400\]{ - background-color: pt-blue-400; - } - .\32xl\:bg-pt-blue-400{ --tw-bg-opacity: 1; background-color: rgb(134 170 182 / var(--tw-bg-opacity, 1)); diff --git a/web/modules/custom/riverside_pt/js/calendar.js b/web/modules/custom/riverside_pt/js/calendar.js index 48c9e93..984e463 100644 --- a/web/modules/custom/riverside_pt/js/calendar.js +++ b/web/modules/custom/riverside_pt/js/calendar.js @@ -9,12 +9,23 @@ var selectedDate = null; var initialized = false; + var currentService = 'diagnostic'; + + function buildEventsUrl(service) { + return drupalSettings.riversidePt.eventsUrl + '?service=' + service; + } + + 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 d.toISOString().substring(0, 10); + return localDateStr(d); } var initDate = nextBusinessDay(); @@ -45,7 +56,7 @@ fetch(drupalSettings.riversidePt.storeSlotUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ start: event.startStr, end: event.endStr }), + body: JSON.stringify({ start: event.startStr, end: event.endStr, service: currentService }), }).then(function (res) { if (res.ok) { window.location.href = drupalSettings.riversidePt.bookingUrl; @@ -85,7 +96,7 @@ }, fixedWeekCount: false, height: 'auto', - events: drupalSettings.riversidePt.eventsUrl, + events: buildEventsUrl(currentService), eventDisplay: 'none', dayMaxEvents: false, diff --git a/web/modules/custom/riverside_pt/js/components/rpt-appt-type.js b/web/modules/custom/riverside_pt/js/components/rpt-appt-type.js index 38c54e6..8c693d2 100644 --- a/web/modules/custom/riverside_pt/js/components/rpt-appt-type.js +++ b/web/modules/custom/riverside_pt/js/components/rpt-appt-type.js @@ -16,6 +16,11 @@ 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
+