diff --git a/web/modules/custom/riverside_pt/css/calendar.css b/web/modules/custom/riverside_pt/css/calendar.css index 347a49e..78e44de 100644 --- a/web/modules/custom/riverside_pt/css/calendar.css +++ b/web/modules/custom/riverside_pt/css/calendar.css @@ -1,6 +1,53 @@ +.riverside-booking-wrap { + display: flex; + flex-direction: column; + gap: 2rem; + align-items: flex-start; +} + +@media (min-width: 640px) { + .riverside-booking-wrap { + flex-direction: row; + gap: 3rem; + } +} + #riverside-calendar { max-width: 480px; font-size: 0.9rem; + flex-shrink: 0; +} + +#riverside-slots-wrap { + flex: 1; + padding-top: 3rem; +} + +#riverside-booking-slots { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} + +.riverside-slot-btn { + padding: 0.6rem 0.5rem; + border: 1.5px solid #306f8e; + border-radius: 6px; + font-size: 0.8rem; + color: #306f8e; + background: #fff; + cursor: pointer; + text-align: center; + transition: background 0.15s, color 0.15s; +} + +.riverside-slot-btn:hover:not(.is-selected) { + background: #dde8f0; +} + +.riverside-slot-btn.is-selected { + background: #306f8e; + color: #fff; } /* ── Strip all borders ── */ @@ -139,65 +186,3 @@ height: 3.5rem; } -/* ── Booking backdrop & panel ── */ -#riverside-booking-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.45); - z-index: 999; -} - -#riverside-booking-panel { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 90%; - max-width: 340px; - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 6px; - padding: 1rem; - z-index: 1000; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); -} - -.riverside-booking-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; - font-weight: 600; -} - -#riverside-booking-close { - background: none; - border: none; - cursor: pointer; - font-size: 1rem; - line-height: 1; - padding: 0; - color: #6b7280; -} - -#riverside-booking-slots { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -#riverside-booking-slots li { - padding: 0.5rem 0.75rem; - border: 1px solid #306f8e; - border-radius: 4px; - cursor: pointer; - font-size: 0.85rem; - color: #306f8e; -} - -#riverside-booking-slots li:hover { - background: #dde8f0; -} diff --git a/web/modules/custom/riverside_pt/js/calendar.js b/web/modules/custom/riverside_pt/js/calendar.js index 92180dd..48c9e93 100644 --- a/web/modules/custom/riverside_pt/js/calendar.js +++ b/web/modules/custom/riverside_pt/js/calendar.js @@ -4,25 +4,76 @@ if (!el) return; requestAnimationFrame(function () { + var slotsWrap = document.getElementById('riverside-slots-wrap'); + var slotsGrid = document.getElementById('riverside-booking-slots'); + var selectedDate = null; + var initialized = false; - var panel = document.getElementById('riverside-booking-panel'); - var backdrop = document.getElementById('riverside-booking-backdrop'); - var panelDate = document.getElementById('riverside-booking-date'); - var panelSlots = document.getElementById('riverside-booking-slots'); - - function closePanel() { - panel.hidden = true; - backdrop.hidden = true; + 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); } - function openPanel() { - backdrop.hidden = false; - panel.hidden = false; + var initDate = nextBusinessDay(); + + function slotLabel(date) { + var h = date.getHours(); + return (h % 12 || 12) + (h < 12 ? 'AM' : 'PM') + ' PST'; + } + + function renderSlots(dateStr, events) { + var dayEvents = events + .filter(function (e) { return e.startStr.startsWith(dateStr); }) + .sort(function (a, b) { return a.start - b.start; }); + + if (!slotsWrap || !slotsGrid || dayEvents.length === 0) return; + + slotsGrid.innerHTML = ''; + dayEvents.forEach(function (event, idx) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = slotLabel(event.start); + btn.className = 'riverside-slot-btn' + (idx === 0 ? ' is-selected' : ''); + btn.addEventListener('click', function () { + slotsGrid.querySelectorAll('.riverside-slot-btn').forEach(function (b) { + b.classList.remove('is-selected'); + }); + btn.classList.add('is-selected'); + fetch(drupalSettings.riversidePt.storeSlotUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ start: event.startStr, end: event.endStr }), + }).then(function (res) { + if (res.ok) { + window.location.href = drupalSettings.riversidePt.bookingUrl; + } else { + btn.textContent += ' (unavailable)'; + btn.disabled = true; + } + }); + }); + slotsGrid.appendChild(btn); + }); + + slotsWrap.hidden = false; + } + + function selectDay(dateStr, events) { + el.querySelectorAll('.fc-daygrid-day.is-selected').forEach(function (d) { + d.classList.remove('is-selected'); + }); + var dayEl = el.querySelector('.fc-daygrid-day[data-date="' + dateStr + '"]'); + if (dayEl) dayEl.classList.add('is-selected'); + selectedDate = dateStr; + renderSlots(dateStr, events); } var calendar = new FullCalendar.Calendar(el, { initialView: 'dayGridMonth', + initialDate: initDate, headerToolbar: { left: 'prev', center: 'title', right: 'next' }, titleFormat: { year: 'numeric', month: 'long' }, dayHeaderFormat: { weekday: 'narrow' }, @@ -43,6 +94,7 @@ d.classList.remove('is-selected'); }); selectedDate = null; + if (slotsWrap) slotsWrap.hidden = true; }, eventsSet: function (events) { @@ -54,6 +106,14 @@ var dayEl = el.querySelector('.fc-daygrid-day[data-date="' + dateStr + '"]'); if (dayEl) dayEl.classList.add('has-availability'); }); + + if (!initialized) { + initialized = true; + var targetEl = el.querySelector('.fc-daygrid-day[data-date="' + initDate + '"]'); + if (targetEl && targetEl.classList.contains('has-availability')) { + selectDay(initDate, events); + } + } }, dayCellClassNames: function (arg) { @@ -63,62 +123,11 @@ dateClick: function (arg) { if (!arg.dayEl.classList.contains('has-availability')) return; - - var dateStr = arg.dateStr; - var dayEvents = calendar.getEvents().filter(function (e) { - return e.startStr.startsWith(dateStr); - }); - if (dayEvents.length === 0) return; - - // Update selected highlight. - el.querySelectorAll('.fc-daygrid-day.is-selected').forEach(function (d) { - d.classList.remove('is-selected'); - }); - arg.dayEl.classList.add('is-selected'); - selectedDate = dateStr; - - // Build slot list. - panelDate.textContent = arg.date.toLocaleDateString(undefined, { - weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', - }); - panelSlots.innerHTML = ''; - dayEvents.sort(function (a, b) { return a.start - b.start; }).forEach(function (event) { - var startLabel = event.start.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); - var endLabel = event.end.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); - var li = document.createElement('li'); - var a = document.createElement('a'); - a.href = '#'; - a.textContent = startLabel + ' – ' + endLabel; - a.addEventListener('click', function (e) { - e.preventDefault(); - fetch(drupalSettings.riversidePt.storeSlotUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ start: event.startStr, end: event.endStr }), - }).then(function (res) { - if (res.ok) { - window.location.href = drupalSettings.riversidePt.bookingUrl; - } else { - a.textContent += ' (no longer available)'; - a.style.pointerEvents = 'none'; - a.style.opacity = '0.5'; - } - }); - }); - li.appendChild(a); - panelSlots.appendChild(li); - }); - openPanel(); + selectDay(arg.dateStr, calendar.getEvents()); }, }); - document.getElementById('riverside-booking-close').addEventListener('click', closePanel); - backdrop.addEventListener('click', closePanel); - document.addEventListener('keydown', function (e) { - if (e.key === 'Escape') closePanel(); - }); - calendar.render(); - }); // end requestAnimationFrame + }); }); })(drupalSettings); diff --git a/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php b/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php index 4834834..2361ad2 100644 --- a/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php +++ b/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php @@ -30,43 +30,26 @@ class ScheduleController extends ControllerBase { '#tag' => 'p', '#value' => $this->t('View provider availability below. Use the calendar to browse open appointment slots by week.'), ], - 'calendar' => [ + 'booking_wrap' => [ '#type' => 'html_tag', '#tag' => 'div', - '#attributes' => ['id' => 'riverside-calendar'], - ], - 'booking_backdrop' => [ - '#type' => 'html_tag', - '#tag' => 'div', - '#attributes' => ['id' => 'riverside-booking-backdrop', 'hidden' => TRUE], - '#value' => '', - ], - 'booking_panel' => [ - '#type' => 'html_tag', - '#tag' => 'div', - '#attributes' => ['id' => 'riverside-booking-panel', 'hidden' => TRUE], - 'header' => [ + '#attributes' => ['class' => ['riverside-booking-wrap']], + 'calendar' => [ '#type' => 'html_tag', '#tag' => 'div', - '#attributes' => ['class' => ['riverside-booking-header']], - 'title' => [ + '#attributes' => ['id' => 'riverside-calendar'], + '#value' => '', + ], + 'slots_wrap' => [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => ['id' => 'riverside-slots-wrap', 'hidden' => TRUE], + 'slots' => [ '#type' => 'html_tag', - '#tag' => 'span', - '#attributes' => ['id' => 'riverside-booking-date'], + '#tag' => 'div', + '#attributes' => ['id' => 'riverside-booking-slots'], '#value' => '', ], - 'close' => [ - '#type' => 'html_tag', - '#tag' => 'button', - '#attributes' => ['id' => 'riverside-booking-close', 'type' => 'button'], - '#value' => $this->t('✕'), - ], - ], - 'slots' => [ - '#type' => 'html_tag', - '#tag' => 'ul', - '#attributes' => ['id' => 'riverside-booking-slots'], - '#value' => '', ], ], '#attached' => [ @@ -123,17 +106,20 @@ class ScheduleController extends ControllerBase { $id = 1; while ($current < $until) { - $i = (int) floor($current->getTimestamp() / 86400); - $count = ($i % 5 + $i % 7 + $i % 11) % 6; - for ($n = 0; $n < $count; $n++) { - $slot = clone $current; - $slot->setTime(9 + $n, 0); - $events[] = [ - 'id' => $id++, - 'title' => 'Available', - 'start' => $slot->format('Y-m-d\TH:i:s'), - 'end' => (clone $slot)->modify('+1 hour')->format('Y-m-d\TH:i:s'), - ]; + $dow = (int) $current->format('N'); // 1=Mon … 7=Sun + if ($dow <= 5) { + $i = (int) floor($current->getTimestamp() / 86400); + $count = ($i % 5 + $i % 7 + $i % 11) % 6; + for ($n = 0; $n < $count; $n++) { + $slot = clone $current; + $slot->setTime(9 + $n, 0); + $events[] = [ + 'id' => $id++, + 'title' => 'Available', + 'start' => $slot->format('Y-m-d\TH:i:s'), + 'end' => (clone $slot)->modify('+1 hour')->format('Y-m-d\TH:i:s'), + ]; + } } $current->modify('+1 day'); } diff --git a/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig b/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig index 0f0d8a6..f0708e5 100644 --- a/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig +++ b/web/modules/custom/riverside_pt/templates/riverside-pt-home.html.twig @@ -116,16 +116,13 @@
-
+

Book An Appointment

-
- -