From 774979f2e4ceb21903ebb62103db938c55315854 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:47:27 -0400 Subject: [PATCH] Add 4 UX improvements to spelling/flashcards panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Assign → button on each list row opens a popup to pick a kid directly without scrolling up to the assignment dropdowns - Hide/Unhide button on built-in lists (stored in localStorage) keeps UNGROUPED clean; Show hidden toggle reveals them dimmed when needed - Mastery progress bar on each kid showing X/Y words mastered for their current assigned list (server now returns listProgress in /api/overview) - Filter-lists / filter-decks search input for fast lookup by name Co-Authored-By: Claude Sonnet 4.6 --- public/admin.html | 13 +++++ public/admin.js | 119 ++++++++++++++++++++++++++++++++++++++-------- public/styles.css | 32 +++++++++++++ server.js | 9 ++++ 4 files changed, 152 insertions(+), 21 deletions(-) diff --git a/public/admin.html b/public/admin.html index 7c554cf..4e34b1a 100644 --- a/public/admin.html +++ b/public/admin.html @@ -300,6 +300,10 @@

This week's list per kid

Word lists

+
+ + +

Test results

@@ -347,6 +351,9 @@

Flashcard Decks

+
+ +
@@ -437,6 +444,12 @@
+ + + diff --git a/public/admin.js b/public/admin.js index 013f5eb..2a27b08 100644 --- a/public/admin.js +++ b/public/admin.js @@ -1274,9 +1274,11 @@ async function renderGradebook(courseId) { // Spelling (word lists + weekly assignment + results) // ============================================================ +let cachedStudents = []; async function loadSpelling() { const [overview, lists] = await Promise.all([api('/api/overview'), api('/api/lists')]); cachedLists = lists; + cachedStudents = overview.students; renderAssignRows(overview.students); renderLists(lists); renderResults(overview.students); @@ -1287,16 +1289,20 @@ function renderAssignRows(students) { `` + cachedLists.map((l) => ``).join(''); $('#assign-rows').innerHTML = students.length - ? students - .map( - (s) => `
- ${esc(s.emoji)} - ${esc(s.name)} - 🌟 ${s.mastered} mastered - -
` - ) - .join('') + ? students.map((s) => { + const lp = s.listProgress; + const progressHtml = lp + ? `${lp.mastered}/${lp.total} mastered + + ` + : `No list assigned`; + return `
+ ${esc(s.emoji)} + ${esc(s.name)} + ${progressHtml} + +
`; + }).join('') : `

No kids yet.

`; document.querySelectorAll('[data-assign]').forEach((sel) => @@ -1334,28 +1340,51 @@ function renderGroupSections(groups, renderItem) { return html || '

None yet.

'; } -function renderLists(lists) { - const groups = groupByName(lists); +const hiddenLists = new Set(JSON.parse(localStorage.getItem('hiddenLists') || '[]')); +function saveHiddenLists() { localStorage.setItem('hiddenLists', JSON.stringify([...hiddenLists])); } + +function renderLists(lists, filterText = '') { + const q = filterText.toLowerCase(); + const visible = lists.filter((l) => !hiddenLists.has(l.id) && (!q || l.name.toLowerCase().includes(q))); + const hidden = lists.filter((l) => hiddenLists.has(l.id) && (!q || l.name.toLowerCase().includes(q))); + + const showHiddenBtn = $('#list-show-hidden'); + showHiddenBtn.hidden = hidden.length === 0; + showHiddenBtn.textContent = showHiddenBtn.dataset.showing === '1' + ? `Hide hidden (${hidden.length})` + : `Show hidden (${hidden.length})`; + + const displayList = showHiddenBtn.dataset.showing === '1' ? [...visible, ...hidden] : visible; + + const groups = groupByName(displayList); const groupNames = Object.keys(groups).filter((k) => k).sort(); $('#list-groups-datalist').innerHTML = groupNames.map((g) => `