From 699e479805f1f1fb630f1e5dc8028e7c7006974b Mon Sep 17 00:00:00 2001 From: James Griffin Date: Wed, 29 Jul 2026 22:55:55 -0300 Subject: [PATCH] Return to a bookable calendar after a booking is confirmed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit showConfirmation() hid the slot list and put the confirmation in its place, which is a dead end: a student wanting a second lesson had nothing to click and no way back short of reloading the page. Enrolling in a group class did the same thing. The confirmation is now a dismissible notice above the calendar. The calendar is reloaded first — so the slot just taken is already gone and the upcoming-lessons panel is current — and the notice is shown over it, which is why loadSlots() had to start returning its promise. "It worked" and "book another" are the same screen. The notice clears when dismissed, when another slot's form is opened, and on any reload of the calendar. group-classes.js gets the identical treatment. It is built from DOM nodes rather than innerHTML because the message can carry a studio's e-transfer address, and it is toggled with the `hidden` attribute rather than an inline display — an inline style would outrank the stylesheet's display:flex and stack the notice's parts. `hidden` needs the !important guard for the same reason the upcoming-lessons panel does: the div{display:block} theme reset outranks the UA sheet. The slotList/list `display = 'block'` lines went with it. Nothing hides those any more, so restoring them each load only implied otherwise. Verified in a headless browser against a stubbed REST API: booking twice in a row without a reload, the booked slot leaving the calendar, the upcoming panel updating, dismissal, the notice clearing when the next form opens, and the notice staying hidden under div{display:block}. Closes #143 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + assets/css/frontend.css | 43 ++++++++++++++++ assets/js/booking.js | 62 +++++++++++++++++++---- assets/js/group-classes.js | 54 +++++++++++++++++--- docs/features/lesson-booking.md | 3 +- templates/frontend/booking-page.php | 11 ++-- templates/frontend/group-classes-page.php | 5 +- 7 files changed, 154 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd7eab8..e226fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ each change under the current top section as you work. - The interface now says **student** where it said "child" and **profile** where it said "family". The `[us_family]` page is headed **Your profile**, its form is **Add a student**, signup asks for a **Student's name**, and the wp-admin students list and student screen both label the relationship **Profile**. Two strings were reworded rather than swapped: the students list reads **Managed by _name_** (a bare "Student of _name_" would read as a teacher's pupil), and a managed account is described as a **managed student account** so it is not confused with the account holder. Internal names — database columns, request parameters, form field names, the `us_family` shortcode and the `us-scheduler/family` block — are unchanged, since they are contracts with existing installs and saved post content. ### Fixed +- **Booking a lesson no longer dead-ends on the confirmation.** The confirmation used to replace the calendar entirely, leaving a student who wanted a second lesson with nothing to click and no way back short of reloading the page. It is now a dismissible notice sitting above a freshly loaded calendar — the slot just taken already gone from it, the upcoming-lessons panel already updated — so "it worked" and "book another" are the same screen. Enrolling in a group class did the same thing and is fixed the same way. - Upcoming lesson rows no longer render on top of each other. The row's text sits in inline elements that a theme can pull out of normal flow, which dropped the date and time onto the lesson title and the status pill onto the Cancel button; those elements are now pinned into flow alongside the rest of the panel's theme-proofing. The rows held behind **Show all** also stayed visible under the `div { display: block }` reset that many themes still carry, since `[hidden]` is only a browser default — they are now hidden for real. ## [1.3.0] diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 98a9d9d..59e2960 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -572,6 +572,49 @@ margin-top: 8px; } +/* + * `[hidden]` is a UA-stylesheet rule, so the widespread `div { display: block }` + * theme reset outranks it — the same trap the upcoming-lessons panel hit. An + * author !important is the only way to win, and it has to sit before the + * display rule it guards against. + */ +.us-notice[hidden] { + display: none !important; +} + +/* + * The "you're booked" / "you're enrolled" notice. It sits above the calendar + * or class list rather than replacing it, so it needs to read as a banner + * about something that just happened — not as the page's content. + */ +.us-notice { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 8px 16px; + margin-bottom: 16px; + padding: 12px 16px; + border: 1px solid #b7dfc0; + border-left-width: 4px; + border-radius: 4px; + background: #f2faf4; + color: #1a5c2a; +} + +.us-notice p { + margin: 0; +} + +.us-notice-dismiss { + background: transparent; + border: 1px solid currentColor; + border-radius: 4px; + padding: 4px 12px; + color: inherit; + cursor: pointer; +} + /* Shown only in block-editor previews (see BlockPreview). */ .us-editor-note { font-size: 0.85em; diff --git a/assets/js/booking.js b/assets/js/booking.js index af7d9a7..6e1147a 100644 --- a/assets/js/booking.js +++ b/assets/js/booking.js @@ -335,7 +335,12 @@ slotList.querySelectorAll('.us-book-btn[data-slot-id]').forEach((btn) => { const slot = allSlots.find((s) => String(s.id) === btn.dataset.slotId); - if (slot) btn.addEventListener('click', () => openRegistration(slot)); + if (slot) { + btn.addEventListener('click', () => { + hideConfirmation(); + openRegistration(slot); + }); + } }); } @@ -571,8 +576,11 @@ ? window.usPayment.collect('lesson', (res.ids || [])[0], slotList) : null)) .then((result) => { - loadMyLessons(); - showConfirmation(window.usPayment.message(result)); + const message = window.usPayment.message(result); + + // Order matters: loadSlots() clears any standing notice, and it + // is what puts the calendar back with the booked slot gone. + return loadSlots().then(() => showConfirmation(message)); }) .catch((err) => showError(err.message)); } @@ -668,10 +676,43 @@ .catch(() => { myLessons.innerHTML = ''; }); } + /** + * Report a completed booking without taking the calendar away. + * + * This used to hide the slot list and leave the confirmation as the whole + * page, which is a dead end: the student had nothing to click and no way + * back to booking short of reloading. The notice now sits above a freshly + * loaded calendar, so "it worked" and "you can book again" are the same + * screen. + * + * Built from nodes rather than innerHTML because the message can carry a + * studio's e-transfer address. + */ function showConfirmation(message) { - confirm.textContent = message; - slotList.style.display = 'none'; - confirm.style.display = 'block'; + confirm.textContent = ''; + + const text = document.createElement('p'); + text.textContent = message; + + const dismiss = document.createElement('button'); + dismiss.type = 'button'; + dismiss.className = 'us-notice-dismiss'; + dismiss.textContent = 'Dismiss'; + dismiss.addEventListener('click', hideConfirmation); + + confirm.appendChild(text); + confirm.appendChild(dismiss); + + // The `hidden` attribute rather than an inline display, which would + // outrank the stylesheet's `display: flex` and stack the notice's + // parts instead of laying them out in a row. + confirm.hidden = false; + } + + function hideConfirmation() { + if (!confirm) return; + confirm.hidden = true; + confirm.textContent = ''; } // The private-lesson catalog drives both the filter and the registration @@ -696,16 +737,17 @@ }); } + /** Returns the load, so a caller can act once the calendar is back. */ function loadSlots() { clearError(); loadMyLessons(); // An upcoming-lessons-only embed has no calendar to fill. - if (!slotList) return; + if (!slotList) return Promise.resolve(); - slotList.style.display = 'block'; - confirm.style.display = 'none'; - Promise.all([apiFetch('availability'), loadCatalog()]) + hideConfirmation(); + + return Promise.all([apiFetch('availability'), loadCatalog()]) .then(([slots]) => { allSlots = slots; render(); diff --git a/assets/js/group-classes.js b/assets/js/group-classes.js index ede66ef..3e2ad01 100644 --- a/assets/js/group-classes.js +++ b/assets/js/group-classes.js @@ -173,7 +173,10 @@ list.querySelectorAll('.us-enrol-btn').forEach((btn) => { const offering = groups.find((o) => String(o.id) === btn.dataset.offeringId); - btn.addEventListener('click', () => openEnrolment(offering)); + btn.addEventListener('click', () => { + hideConfirmation(); + openEnrolment(offering); + }); }); list.querySelectorAll('.us-withdraw-btn').forEach((btn) => { @@ -254,25 +257,60 @@ .then((res) => (res.payment ? window.usPayment.collect('enrollment', res.id, list) : null)) - .then((result) => showConfirmation(window.usPayment.message(result))) + .then((result) => { + const message = window.usPayment.message(result); + + // Order matters: loadClasses() clears any standing notice, and + // it is what puts the list back showing the new enrolment. + return loadClasses().then(() => showConfirmation(message)); + }) .catch((err) => showError(err.message)); } + /** + * Report a completed enrolment without taking the class list away. Hiding + * the list left the student on a dead-end screen with no way back to + * browsing short of a reload; the notice now sits above a freshly loaded + * list instead. Mirrors booking.js. + * + * Built from nodes rather than innerHTML because the message can carry a + * studio's e-transfer address. + */ function showConfirmation(message) { - confirm.textContent = message; - list.style.display = 'none'; - confirm.style.display = 'block'; + confirm.textContent = ''; + + const text = document.createElement('p'); + text.textContent = message; + + const dismiss = document.createElement('button'); + dismiss.type = 'button'; + dismiss.className = 'us-notice-dismiss'; + dismiss.textContent = 'Dismiss'; + dismiss.addEventListener('click', hideConfirmation); + + confirm.appendChild(text); + confirm.appendChild(dismiss); + + // The `hidden` attribute rather than an inline display, which would + // outrank the stylesheet's `display: flex` and stack the notice's + // parts instead of laying them out in a row. + confirm.hidden = false; } + function hideConfirmation() { + confirm.hidden = true; + confirm.textContent = ''; + } + + /** Returns the load, so a caller can act once the list is back. */ function loadClasses() { clearError(); - list.style.display = 'block'; - confirm.style.display = 'none'; + hideConfirmation(); // The student's own enrolments are fetched alongside the catalog so a // class they already have an active enrolment in shows its status // instead of offering to enrol them again (the API would reject the // duplicate anyway). A cancelled enrolment does not block re-enrolling. - Promise.all([ + return Promise.all([ apiFetch('offerings?kind=group_class'), apiFetch('enrollments'), ]) diff --git a/docs/features/lesson-booking.md b/docs/features/lesson-booking.md index 97771af..d724fb4 100644 --- a/docs/features/lesson-booking.md +++ b/docs/features/lesson-booking.md @@ -30,7 +30,8 @@ Students register for a private lesson by choosing an offering, picking a time ( 8. `POST /bookings` creates the lesson row(s) (`status = pending`), records answers and policy acceptances, marks `us_availability.is_booked = 1`, and links the payment. A booking with nothing owed (a free offering) creates no payment and is `confirmed` immediately. 9. On successful payment (or comp) the lesson is `confirmed` and a receipt is emailed. 10. Instructor sees the booking under **My Lessons** and may update status via `PATCH /bookings/{id}/status`. -11. The booking page also shows the student their upcoming lessons (`GET /bookings`) — each with the booked offering's name and length, when it happens, a per-lesson status badge (pending payment / confirmed), and a **Cancel** button. Only the soonest five are shown; a **Show all** control reveals the rest. `GET /bookings` includes `offering_title` and `duration_minutes` for each lesson so the list needs no extra request. +11. The confirmation is a **dismissible notice above the calendar**, not a screen of its own. The calendar is reloaded first — so the slot just taken is gone and the upcoming-lessons panel is current — and the notice is shown over it. Booking again therefore needs no page reload. The notice clears when it is dismissed, when another slot's booking form is opened, and on any reload of the calendar. `group-classes.js` does the same for enrolments. +12. The booking page also shows the student their upcoming lessons (`GET /bookings`) — each with the booked offering's name and length, when it happens, a per-lesson status badge (pending payment / confirmed), and a **Cancel** button. Only the soonest five are shown; a **Show all** control reveals the rest. `GET /bookings` includes `offering_title` and `duration_minutes` for each lesson so the list needs no extra request. ## Lesson-Type Filter Not every open slot can be booked as every private-lesson type — a slot tied to diff --git a/templates/frontend/booking-page.php b/templates/frontend/booking-page.php index 912b33b..8155817 100644 --- a/templates/frontend/booking-page.php +++ b/templates/frontend/booking-page.php @@ -21,12 +21,17 @@ $studentsJson = wp_json_encode(array_values($students));
+ +

- diff --git a/templates/frontend/group-classes-page.php b/templates/frontend/group-classes-page.php index 90cede9..60363e9 100644 --- a/templates/frontend/group-classes-page.php +++ b/templates/frontend/group-classes-page.php @@ -13,11 +13,10 @@ if (! defined('ABSPATH')) { $studentsJson = wp_json_encode(array_values($students)); ?>
0 ? ' data-offering="' . esc_attr((string) $offeringId) . '"' : ''; ?>> + +

-