From 1b7577fa17ec4e123f7523c3da008583d8a522d9 Mon Sep 17 00:00:00 2001 From: Philip Peterson <1326208+philip-peterson@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:51:43 -0700 Subject: [PATCH] Calendar polish: selection persistence, no-slots overlay, various fixes - Persist selected date across month navigation using module-level vars; datesSet re-applies is-selected and restores slots when returning - Show "No availability this month" overlay after a fetch returns empty; gated on initializedRef+fetchedRef so auto-advance phase is silent - Fix Dec 31 overflow: showNonCurrentDates:false hides adjacent-month days - Fix fc-day-disabled background tint in calendar.css - Gate auto-advance on loading() callback so removeAllEventSources() spurious eventsSet() fires don't trigger premature month jumping - Inline overlay styles to avoid Tailwind cascade uncertainty; document the module-level CX constant pattern as the general fix - firstName added to booking form; storeSlot sends email directly when full contact info present, skipping tempstore redirect - Remove BookingForm.php and /schedule/book route (replaced by inline form) Co-Authored-By: Claude Sonnet 4.6 --- web/modules/custom/riverside_pt/css/app.css | 41 +++++ .../custom/riverside_pt/css/calendar.css | 12 +- .../custom/riverside_pt/js/calendar.js | 1 + .../riverside_pt/js/components/rpt-booking.js | 75 ++++++++- .../riverside_pt/js/components/rpt-faq.js | 2 +- .../custom/riverside_pt/riverside_pt.install | 2 +- .../riverside_pt/riverside_pt.libraries.yml | 1 - .../custom/riverside_pt/riverside_pt.module | 8 - .../riverside_pt/riverside_pt.routing.yml | 16 -- .../src/Controller/HomeController.php | 1 - .../src/Controller/ScheduleController.php | 129 +++++++------- .../riverside_pt/src/Form/BookingForm.php | 159 ------------------ .../templates/riverside-pt-home.html.twig | 4 +- 13 files changed, 188 insertions(+), 263 deletions(-) delete mode 100644 web/modules/custom/riverside_pt/src/Form/BookingForm.php diff --git a/web/modules/custom/riverside_pt/css/app.css b/web/modules/custom/riverside_pt/css/app.css index 8bfc469..3d978ec 100644 --- a/web/modules/custom/riverside_pt/css/app.css +++ b/web/modules/custom/riverside_pt/css/app.css @@ -741,6 +741,14 @@ html { margin-top: 2rem; } +.mt-1{ + margin-top: 0.25rem; +} + +.mt-3{ + margin-top: 0.75rem; +} + .block{ display: block; } @@ -1086,6 +1094,11 @@ html { border-color: rgb(255 255 255 / 0.6); } +.border-green-200{ + --tw-border-opacity: 1; + border-color: rgb(187 247 208 / var(--tw-border-opacity, 1)); +} + .bg-current{ background-color: currentColor; } @@ -1138,6 +1151,11 @@ html { background-color: rgb(255 255 255 / 0.9); } +.bg-green-50{ + --tw-bg-opacity: 1; + background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1)); +} + .bg-gradient-to-b{ background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); } @@ -1488,6 +1506,20 @@ html { color: rgb(239 68 68 / var(--tw-text-opacity, 1)); } +.text-green-700{ + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity, 1)); +} + +.text-green-800{ + --tw-text-opacity: 1; + color: rgb(22 101 52 / var(--tw-text-opacity, 1)); +} + +.underline{ + text-decoration-line: underline; +} + .no-underline{ text-decoration-line: none; } @@ -1593,6 +1625,11 @@ html { color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } +.hover\:text-green-800:hover{ + --tw-text-opacity: 1; + color: rgb(22 101 52 / 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)); @@ -1619,6 +1656,10 @@ html { } @media (min-width: 768px){ + .sm\:col-span-2{ + grid-column: span 2 / span 2; + } + .sm\:block{ display: block; } diff --git a/web/modules/custom/riverside_pt/css/calendar.css b/web/modules/custom/riverside_pt/css/calendar.css index 3586b2e..b3ad190 100644 --- a/web/modules/custom/riverside_pt/css/calendar.css +++ b/web/modules/custom/riverside_pt/css/calendar.css @@ -73,13 +73,6 @@ color: #111; } -#riverside-calendar .fc-toolbar-title::after { - content: ' \25BC'; - font-size: 0.6em; - vertical-align: middle; - color: #374151; -} - #riverside-calendar .fc-button-group, #riverside-calendar .fc-button { background: none !important; @@ -179,6 +172,11 @@ background: none !important; } +/* Disabled days (outside validRange) — no background tint */ +#riverside-calendar .fc-day-disabled { + background: none !important; +} + /* Weekends — same appearance as any other non-available day */ #riverside-calendar .fc-day-sat, #riverside-calendar .fc-day-sun { diff --git a/web/modules/custom/riverside_pt/js/calendar.js b/web/modules/custom/riverside_pt/js/calendar.js index 984e463..b3fabdc 100644 --- a/web/modules/custom/riverside_pt/js/calendar.js +++ b/web/modules/custom/riverside_pt/js/calendar.js @@ -95,6 +95,7 @@ }; }, fixedWeekCount: false, + showNonCurrentDates: false, height: 'auto', events: buildEventsUrl(currentService), eventDisplay: 'none', diff --git a/web/modules/custom/riverside_pt/js/components/rpt-booking.js b/web/modules/custom/riverside_pt/js/components/rpt-booking.js index 7f0bc0e..a2d31fa 100644 --- a/web/modules/custom/riverside_pt/js/components/rpt-booking.js +++ b/web/modules/custom/riverside_pt/js/components/rpt-booking.js @@ -13,7 +13,7 @@ const CHECK = html` `; -const EMPTY_FORM = { lastName: "", phone: "", comments: "" }; +const EMPTY_FORM = { firstName: "", lastName: "", phone: "", comments: "" }; function formatPhone(raw) { let d = String(raw || "").replace(/\D/g, ""); @@ -57,6 +57,8 @@ function Booking({ settings }) { const [formData, setFormData] = useState(EMPTY_FORM); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + const [success, setSuccess] = useState(false); + const [noSlotsInMonth, setNoSlotsInMonth] = useState(false); const calEl = useRef(null); const calRef = useRef(null); @@ -111,6 +113,7 @@ function Booking({ settings }) { d.classList.remove("is-selected"); }); setSelectedSlotId(null); + setNoSlotsInMonth(false); if (selectedDate) { var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + selectedDate + "\"]"); if (dayEl) { @@ -148,6 +151,12 @@ function Booking({ settings }) { cal.next(); } } + if (initializedRef.current && fetchedRef.current) { + var viewStart = cal.view.currentStart; + var viewEnd = cal.view.currentEnd; + var inView = events.filter(function (e) { return e.start >= viewStart && e.start < viewEnd; }); + setNoSlotsInMonth(inView.length === 0); + } }, dayCellClassNames: function (arg) { @@ -168,6 +177,7 @@ function Booking({ settings }) { selectedDateSlots = daySlots; setSelectedSlotId(null); setSubmitError(null); + setSuccess(false); setSlots(daySlots); }, }); @@ -193,6 +203,8 @@ function Booking({ settings }) { setSelectedSlotId(null); setFormData(EMPTY_FORM); setSubmitError(null); + setSuccess(false); + setNoSlotsInMonth(false); cal.gotoDate(initDate); } @@ -203,6 +215,7 @@ function Booking({ settings }) { function handleSlotClick(slot) { setSelectedSlotId(slot.id); setSubmitError(null); + setSuccess(false); } function handleFormChange(field, value) { @@ -222,16 +235,25 @@ function Booking({ settings }) { start: slot.startStr, end: slot.endStr, service: service, + firstName: formData.firstName, lastName: formData.lastName, phone: formData.phone, comments: formData.comments, }), }).then(function (res) { if (res.ok) { - window.location.href = settings.bookingUrl; + setSubmitting(false); + setSubmitError(null); + setSuccess(true); + setSelectedSlotId(null); + setFormData(EMPTY_FORM); } else { setSubmitting(false); - setSubmitError("Something went wrong. Please try again."); + if (res.status === 422) { + setSubmitError("That slot was just booked. Please choose another time."); + } else { + setSubmitError("Something went wrong. Please try again."); + } } }).catch(function () { setSubmitting(false); @@ -280,7 +302,16 @@ function Booking({ settings }) {
-
+
+
+ ${noSlotsInMonth ? html` +
+

+ No availability this month +

+
+ ` : null} +
${slots.length > 0 ? html`
@@ -299,6 +330,28 @@ function Booking({ settings }) { ` : null}
+ ${success ? html` +
+
+

Request received!

+

Thank you. We'll contact you shortly to confirm your appointment.

+ +
+
+ ` : null} + ${selectedSlot ? html`
+
+ + +
-
+
diff --git a/web/modules/custom/riverside_pt/js/components/rpt-faq.js b/web/modules/custom/riverside_pt/js/components/rpt-faq.js index 2681244..43a4930 100644 --- a/web/modules/custom/riverside_pt/js/components/rpt-faq.js +++ b/web/modules/custom/riverside_pt/js/components/rpt-faq.js @@ -21,7 +21,7 @@ const FAQS = [ }, { q: "Can I book an appointment online?", - a: "Yes. Use the booking tool on our Schedule page to pick a service type, choose an available slot, and confirm your appointment. You'll receive a confirmation email immediately.", + a: "Yes. Use the booking tool on this page to pick a service type, choose an available slot, and submit your request. You'll receive a confirmation email immediately.", }, { q: "What should I wear or bring to my session?", diff --git a/web/modules/custom/riverside_pt/riverside_pt.install b/web/modules/custom/riverside_pt/riverside_pt.install index f4e825c..e3701d7 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.install +++ b/web/modules/custom/riverside_pt/riverside_pt.install @@ -223,7 +223,7 @@ function _riverside_pt_build_navigation(): void { ['title' => 'FAQ', 'uri' => 'internal:/faq', 'weight' => 3, 'class' => NULL], ['title' => 'Contact', 'uri' => 'internal:/contact', 'weight' => 4, 'class' => 'nav-cta nav-cta--primary'], - ['title' => 'Book An Appointment', 'uri' => 'internal:/schedule', 'weight' => 5, 'class' => 'nav-cta nav-cta--primary'], + ['title' => 'Book An Appointment', 'uri' => 'internal:/home', 'weight' => 5, 'class' => 'nav-cta nav-cta--primary'], ]; foreach ($defs as $def) { diff --git a/web/modules/custom/riverside_pt/riverside_pt.libraries.yml b/web/modules/custom/riverside_pt/riverside_pt.libraries.yml index b08f972..cf54423 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.libraries.yml +++ b/web/modules/custom/riverside_pt/riverside_pt.libraries.yml @@ -15,7 +15,6 @@ schedule: css/calendar.css: {} js: js/fullcalendar.min.js: { minified: true } - js/calendar.js: {} js/components/rpt-booking.js: { attributes: { type: module } } dependencies: - core/drupalSettings diff --git a/web/modules/custom/riverside_pt/riverside_pt.module b/web/modules/custom/riverside_pt/riverside_pt.module index 86ee670..c068544 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.module +++ b/web/modules/custom/riverside_pt/riverside_pt.module @@ -74,14 +74,6 @@ function riverside_pt_preprocess_riverside_pt_header(array &$variables): void { $variables['current_path'] = \Drupal::request()->getPathInfo(); } -function riverside_pt_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void { - if ($route_match->getRouteName() === 'riverside_pt.booking') { - $breadcrumb = new Breadcrumb(); - $breadcrumb->addLink(Link::createFromRoute('← Back', 'riverside_pt.schedule')); - $breadcrumb->addCacheContexts(['route']); - } -} - function riverside_pt_mail(string $key, array &$message, array $params): void { if ($key !== 'booking_request') { return; diff --git a/web/modules/custom/riverside_pt/riverside_pt.routing.yml b/web/modules/custom/riverside_pt/riverside_pt.routing.yml index ebce8a2..94e1b49 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.routing.yml +++ b/web/modules/custom/riverside_pt/riverside_pt.routing.yml @@ -14,22 +14,6 @@ riverside_pt.home: requirements: _permission: 'access content' -riverside_pt.schedule: - path: '/schedule' - defaults: - _controller: '\Drupal\riverside_pt\Controller\ScheduleController::page' - _title: 'Schedule' - requirements: - _permission: 'access content' - -riverside_pt.booking: - path: '/schedule/book' - defaults: - _form: '\Drupal\riverside_pt\Form\BookingForm' - _title: 'Request Appointment' - requirements: - _permission: 'access content' - riverside_pt.booking_store_slot: path: '/schedule/book/slot' defaults: diff --git a/web/modules/custom/riverside_pt/src/Controller/HomeController.php b/web/modules/custom/riverside_pt/src/Controller/HomeController.php index 040301d..1dba4cf 100644 --- a/web/modules/custom/riverside_pt/src/Controller/HomeController.php +++ b/web/modules/custom/riverside_pt/src/Controller/HomeController.php @@ -21,7 +21,6 @@ class HomeController extends ControllerBase { 'drupalSettings' => [ 'riversidePt' => [ 'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(), - 'bookingUrl' => Url::fromRoute('riverside_pt.booking')->toString(), 'storeSlotUrl' => Url::fromRoute('riverside_pt.booking_store_slot')->toString(), 'holidays' => $holidayMap, ], diff --git a/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php b/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php index 7a3c29d..068a032 100644 --- a/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php +++ b/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php @@ -5,7 +5,8 @@ namespace Drupal\riverside_pt\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\TempStore\PrivateTempStore; use Drupal\Core\TempStore\PrivateTempStoreFactory; -use Drupal\Core\Url; +use Drupal\Core\Mail\MailManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -13,66 +14,23 @@ use Symfony\Component\HttpFoundation\Request; class ScheduleController extends ControllerBase { private PrivateTempStore $tempStore; + private $configFactory; - public function __construct(PrivateTempStoreFactory $tempStoreFactory) { + public function __construct( + PrivateTempStoreFactory $tempStoreFactory, + private readonly MailManagerInterface $mailManager, + ConfigFactoryInterface $configFactory, + ) { $this->tempStore = $tempStoreFactory->get('riverside_pt'); + $this->configFactory = $configFactory; } public static function create(ContainerInterface $container): static { - return new static($container->get('tempstore.private')); - } - - public function page(): array { - return [ - '#type' => 'container', - 'intro' => [ - '#type' => 'html_tag', - '#tag' => 'p', - '#value' => $this->t('View provider availability below. Use the calendar to browse open appointment slots by week.'), - ], - 'booking_wrap' => [ - '#type' => 'html_tag', - '#tag' => 'div', - '#attributes' => ['class' => ['riverside-booking-wrap']], - 'calendar' => [ - '#type' => 'html_tag', - '#tag' => 'div', - '#attributes' => ['id' => 'riverside-calendar'], - '#value' => '', - ], - 'slots_wrap' => [ - '#type' => 'html_tag', - '#tag' => 'div', - '#attributes' => ['id' => 'riverside-slots-wrap', 'hidden' => TRUE], - 'slots' => [ - '#type' => 'html_tag', - '#tag' => 'div', - '#attributes' => ['id' => 'riverside-booking-slots'], - '#value' => '', - ], - ], - ], - '#attached' => [ - 'library' => ['riverside_pt/schedule'], - 'drupalSettings' => [ - 'riversidePt' => [ - 'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(), - 'bookingUrl' => Url::fromRoute('riverside_pt.booking')->toString(), - 'storeSlotUrl' => Url::fromRoute('riverside_pt.booking_store_slot')->toString(), - 'holidays' => $this->buildHolidaysMap(), - ], - ], - ], - ]; - } - - private function buildHolidaysMap(): array { - $holidays = $this->config('riverside_pt.settings')->get('holidays') ?? []; - $map = []; - foreach ($holidays as $holiday) { - $map[$holiday['date']] = $holiday['name']; - } - return $map; + return new static( + $container->get('tempstore.private'), + $container->get('plugin.manager.mail'), + $container->get('config.factory'), + ); } public function storeSlot(Request $request): JsonResponse { @@ -83,14 +41,61 @@ class ScheduleController extends ControllerBase { return new JsonResponse(['error' => 'past'], 422); } + $firstName = trim($data['firstName'] ?? $data['first_name'] ?? ''); + $lastName = trim($data['lastName'] ?? $data['last_name'] ?? ''); + $phone = trim($data['phone'] ?? ''); + $comments = $data['comments'] ?? ''; + $service = $data['service'] ?? 'diagnostic'; + $end = $data['end'] ?? ''; + $providerId = $data['provider_id'] ?? ''; + + // Full contact info present (new embedded booking flow on homepage): + // validate, send the request email immediately, and return success. + // This replaces the previous /schedule/book form page. + if ($firstName && $lastName && $phone) { + // Prevent double-booking against existing appointment nodes (same logic as before). + $conflict = \Drupal::entityQuery('node') + ->condition('type', 'appointment') + ->condition('field_appointment_date', $start) + ->condition('field_provider', $providerId ?: 0) + ->accessCheck(FALSE) + ->count() + ->execute(); + + if ($conflict > 0) { + return new JsonResponse(['error' => 'conflict'], 422); + } + + $to = $this->configFactory->get('riverside_pt.settings')->get('notification_email'); + $lang = \Drupal::languageManager()->getDefaultLanguage()->getId(); + + $sent = $this->mailManager->mail('riverside_pt', 'booking_request', $to, $lang, [ + 'first_name' => $firstName, + 'last_name' => $lastName, + 'phone' => $phone, + 'comments' => $comments, + 'start' => $start, + 'end' => $end, + ]); + + $this->tempStore->delete('booking_slot'); + + if ($sent['result']) { + return new JsonResponse(['ok' => TRUE]); + } + return new JsonResponse(['error' => 'mail_failed'], 500); + } + + // Legacy/minimal path (no contact details): just stash in tempstore (for any + // remaining callers that don't send full info). $this->tempStore->set('booking_slot', [ 'start' => $start, - 'end' => $data['end'] ?? '', - 'service' => $data['service'] ?? 'diagnostic', - 'last_name' => $data['lastName'] ?? '', - 'phone' => $data['phone'] ?? '', - 'comments' => $data['comments'] ?? '', - 'provider_id' => $data['provider_id'] ?? '', + 'end' => $end, + 'service' => $service, + 'last_name' => $lastName, + 'phone' => $phone, + 'comments' => $comments, + 'provider_id' => $providerId, ]); return new JsonResponse(['ok' => TRUE]); diff --git a/web/modules/custom/riverside_pt/src/Form/BookingForm.php b/web/modules/custom/riverside_pt/src/Form/BookingForm.php deleted file mode 100644 index 86b75e1..0000000 --- a/web/modules/custom/riverside_pt/src/Form/BookingForm.php +++ /dev/null @@ -1,159 +0,0 @@ -configFactory = $configFactory; - $this->tempStore = $tempStoreFactory->get('riverside_pt'); - } - - public static function create(ContainerInterface $container): static { - return new static( - $container->get('plugin.manager.mail'), - $container->get('config.factory'), - $container->get('tempstore.private'), - ); - } - - public function getFormId(): string { - return 'riverside_pt_booking_form'; - } - - public function buildForm(array $form, FormStateInterface $form_state): array { - $slot = $this->tempStore->get('booking_slot') ?? []; - $start = $slot['start'] ?? ''; - $end = $slot['end'] ?? ''; - $uid = $slot['provider_id'] ?? ''; - - $slot_display = ''; - if ($start && $end) { - $s = new \DateTime($start); - $e = new \DateTime($end); - $slot_display = $s->format('l, F j, Y') . ', ' . $s->format('g:i A') . '–' . $e->format('g:i A'); - } - - $form['#cache'] = ['max-age' => 0]; - - $form['slot_summary'] = [ - '#type' => 'item', - '#title' => $this->t('Appointment'), - '#markup' => $slot_display ?: $this->t('No slot selected.'), - ]; - - if ($uid && $provider = User::load($uid)) { - $form['provider_summary'] = [ - '#type' => 'item', - '#title' => $this->t('Provider'), - '#markup' => $provider->getDisplayName(), - ]; - } - - $form['first_name'] = [ - '#type' => 'textfield', - '#title' => $this->t('First name'), - '#required' => TRUE, - ]; - - $form['last_name'] = [ - '#type' => 'textfield', - '#title' => $this->t('Last name'), - '#required' => TRUE, - '#default_value' => $slot['last_name'] ?? '', - ]; - - $form['phone'] = [ - '#type' => 'tel', - '#title' => $this->t('Phone number'), - '#required' => TRUE, - '#default_value' => $slot['phone'] ?? '', - '#attributes' => ['class' => ['rpt-phone']], - ]; - - $form['comments'] = [ - '#type' => 'textarea', - '#title' => $this->t('Comments'), - '#rows' => 4, - '#default_value' => $slot['comments'] ?? '', - ]; - - $form['actions'] = ['#type' => 'actions']; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Request appointment'), - ]; - - return $form; - } - - public function validateForm(array &$form, FormStateInterface $form_state): void { - $slot = $this->tempStore->get('booking_slot') ?? []; - $start = $slot['start'] ?? ''; - - if (!$start) { - $form_state->setError($form['slot_summary'], $this->t('No slot selected. Please go back and choose a time.')); - return; - } - - if (new \DateTime($start) < new \DateTime()) { - $form_state->setError($form['slot_summary'], $this->t('That slot is in the past. Please go back and choose another time.')); - return; - } - - $provider_id = $slot['provider_id'] ?? ''; - $conflict = \Drupal::entityQuery('node') - ->condition('type', 'appointment') - ->condition('field_appointment_date', $start) - ->condition('field_provider', $provider_id ?: 0) - ->accessCheck(FALSE) - ->count() - ->execute(); - - if ($conflict > 0) { - $form_state->setError($form['slot_summary'], $this->t('That slot was just booked. Please go back and choose another time.')); - } - } - - public function submitForm(array &$form, FormStateInterface $form_state): void { - $slot = $this->tempStore->get('booking_slot') ?? []; - $this->tempStore->delete('booking_slot'); - - $to = $this->configFactory->get('riverside_pt.settings')->get('notification_email'); - $lang = $this->languageManager()->getDefaultLanguage()->getId(); - - $sent = $this->mailManager->mail('riverside_pt', 'booking_request', $to, $lang, [ - 'first_name' => $form_state->getValue('first_name'), - 'last_name' => $form_state->getValue('last_name'), - 'phone' => $form_state->getValue('phone'), - 'comments' => $form_state->getValue('comments'), - 'start' => $slot['start'] ?? '', - 'end' => $slot['end'] ?? '', - ]); - - if ($sent['result']) { - $this->messenger()->addStatus($this->t('Your request has been submitted. We will contact you to confirm.')); - } - else { - $this->messenger()->addError($this->t('Something went wrong. Please call us to book directly.')); - } - - $form_state->setRedirect('riverside_pt.schedule'); - } - -} 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 490ae27..e5e0716 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 @@ -30,7 +30,7 @@

Every new patient starts with a comprehensive diagnostic assessment. From there we build a personalized plan that may include sports rehabilitation, pre- or post-surgical recovery, or neurological physical therapy.