diff --git a/CHANGELOG.md b/CHANGELOG.md index 105a748..348adcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,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 55f7e5a..dcc8376 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -549,6 +549,49 @@ color: #1a7d2e; } +/* + * 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. + */ +/* + * `[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; +} + +.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));
+ +