Collapse the lesson-type filter behind a Show Only button
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / Tests (PHP 8.2) (pull_request) Successful in 54s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m52s
CI / PHPStan (pull_request) Successful in 3m3s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped

The filter took a row of the booking calendar before a student had asked for
it. The view toggle and a new "Show Only" button now share one control row,
and the lesson-type list is revealed between that row and the calendar.

The list stays open across re-renders once revealed, and collapsing it leaves
the filter applied — the button keeps its active styling and carries the
number of ticked types, so a collapsed filter is never invisible.

Closes #119

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-07-28 12:47:24 -03:00
co-authored by Claude Opus 5
parent add6605141
commit 9d11cc3b01
5 changed files with 95 additions and 37 deletions
+23 -1
View File
@@ -117,6 +117,29 @@
color: #8a6d1a;
}
.us-calendar-controls {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.us-filter-toggle {
padding: 6px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: transparent;
cursor: pointer;
}
.us-filter-toggle.us-active {
background: #333;
border-color: #333;
color: #fff;
}
.us-type-filter {
display: flex;
flex-wrap: wrap;
@@ -150,7 +173,6 @@
.us-view-toggle {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.us-view-toggle button {
+56 -25
View File
@@ -85,8 +85,10 @@
let catalog = [];
// Lesson types the student has filtered the calendar down to; empty means
// "no filter" — every open slot is shown.
// "no filter" — every open slot is shown. The list starts collapsed behind
// the "Show Only" button and stays open across re-renders once revealed.
const selectedTypeIds = new Set();
let filterOpen = false;
// Whether an offering can be booked into a slot — the client-side mirror of
// the rule `POST /bookings` enforces: a slot tied to an offering takes that
@@ -135,14 +137,31 @@
return addDays(key, -((date.getDay() - startOfWeek + 7) % 7));
}
function toggleHtml() {
// The calendar's control row: the view toggle, and the button that reveals
// the lesson-type filter beneath it.
function controlsHtml() {
return `
<div class="us-view-toggle" role="group" aria-label="Calendar view">
<button type="button" id="us-view-list" class="${view === 'list' ? 'us-active' : ''}">List</button>
<button type="button" id="us-view-week" class="${view === 'week' ? 'us-active' : ''}">Week</button>
<div class="us-calendar-controls">
<div class="us-view-toggle" role="group" aria-label="Calendar view">
<button type="button" id="us-view-list" class="${view === 'list' ? 'us-active' : ''}">List</button>
<button type="button" id="us-view-week" class="${view === 'week' ? 'us-active' : ''}">Week</button>
</div>
${filterToggleHtml()}
</div>`;
}
// Nothing to filter with a single bookable type, so the control only
// appears once there is a choice to make.
function filterToggleHtml() {
if (catalog.length < 2) return '';
const count = filterActive() ? ` (${selectedTypeIds.size})` : '';
return `
<button type="button" id="us-filter-toggle" class="us-filter-toggle${filterActive() ? ' us-active' : ''}"
aria-expanded="${filterOpen}" aria-controls="us-type-filter">Show Only${count}</button>`;
}
// "Piano Lesson (30 min)" — the instructor's name is only worth the space
// when the catalog spans more than one of them.
function filterLabel(offering) {
@@ -154,10 +173,10 @@
return `${offering.title}${duration}${who}`;
}
// Lesson-type filter. Pointless with a single bookable type, so it is only
// rendered once there is a choice to make.
// The lesson-type list itself — collapsed until the student opens it, and
// rendered between the control row and the calendar.
function filterHtml() {
if (catalog.length < 2) return '';
if (catalog.length < 2 || !filterOpen) return '';
const choices = catalog.map((o) => `
<label class="us-type-filter-choice">
@@ -167,7 +186,7 @@
`).join('');
return `
<div class="us-type-filter" role="group" aria-label="Filter by lesson type">
<div class="us-type-filter" id="us-type-filter" role="group" aria-label="Filter by lesson type">
<span class="us-type-filter-heading">Lesson type</span>
${choices}
${filterActive() ? '<button type="button" id="us-type-filter-clear" class="us-type-filter-clear">Show all types</button>' : ''}
@@ -221,11 +240,15 @@
function render() {
const slots = visibleSlots();
// Nothing open at all: there is nothing for the controls to act on.
if (!allSlots.length) {
slotList.innerHTML = '<p>No available lesson slots at this time.</p>';
return;
}
if (!slots.length) {
slotList.innerHTML = filterHtml() + (allSlots.length
? '<p>No open times match the selected lesson types.</p>'
: '<p>No available lesson slots at this time.</p>');
wireFilterEvents();
slotList.innerHTML = controlsHtml() + filterHtml() + '<p>No open times match the selected lesson types.</p>';
wireControlEvents();
return;
}
@@ -233,12 +256,29 @@
// API returns slots ordered by start), so the first look is never empty.
if (view === 'week' && !weekStart) weekStart = weekStartOf(dayKey(slots[0].start_dt));
slotList.innerHTML = filterHtml() + toggleHtml() + (view === 'week' ? weekHtml(slots) : listHtml(slots));
wireFilterEvents();
slotList.innerHTML = controlsHtml() + filterHtml() + (view === 'week' ? weekHtml(slots) : listHtml(slots));
wireControlEvents();
wireCalendarEvents();
}
function wireFilterEvents() {
function wireControlEvents() {
document.getElementById('us-view-list').addEventListener('click', () => {
view = 'list';
render();
});
document.getElementById('us-view-week').addEventListener('click', () => {
view = 'week';
render();
});
const toggle = document.getElementById('us-filter-toggle');
if (toggle) {
toggle.addEventListener('click', () => {
filterOpen = !filterOpen;
render();
});
}
slotList.querySelectorAll('.us-type-filter-option').forEach((input) => {
input.addEventListener('change', () => {
const id = Number(input.value);
@@ -265,15 +305,6 @@
}
function wireCalendarEvents() {
document.getElementById('us-view-list').addEventListener('click', () => {
view = 'list';
render();
});
document.getElementById('us-view-week').addEventListener('click', () => {
view = 'week';
render();
});
const prev = document.getElementById('us-week-prev');
const next = document.getElementById('us-week-next');
if (prev) prev.addEventListener('click', () => { weekStart = addDays(weekStart, -7); render(); });