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
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:
+23
-1
@@ -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
@@ -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(); });
|
||||
|
||||
Reference in New Issue
Block a user