From 723bbb93c8f6c82043ccb1da6921e5b3de3aede2 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:24:44 -0400 Subject: [PATCH] Redesign course unit list into collapsible cards on student and admin views Student view auto-opens the unit whose item due-dates span today (falls back to most-recent past unit, earliest future unit, then first with incomplete work). Admin view starts all units expanded. Both share the same card design: unit-number badge, name, progress/count indicator, animated chevron toggle. Co-Authored-By: Claude Sonnet 4.6 --- public/admin.js | 47 ++++++++++++++++-------- public/app.js | 94 +++++++++++++++++++++++++++++++++++------------ public/styles.css | 88 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 189 insertions(+), 40 deletions(-) diff --git a/public/admin.js b/public/admin.js index 3a1347d..b7bf7f6 100644 --- a/public/admin.js +++ b/public/admin.js @@ -235,26 +235,43 @@ async function openCourseDetail(id) { $('#cd-units').innerHTML = course.units .map( - (u) => `
-
-

${esc(u.name)}

- + (u, uIdx) => `
+
+
+ Unit ${uIdx + 1} + ${esc(u.name)} +
+
+ ${u.items.length} item${u.items.length !== 1 ? 's' : ''} + + +
+
+
+ ${u.items + .map( + (it) => `
+ ${TYPE_ICON[it.type]} + ${esc(it.title)} + ${TYPE_LABEL[it.type]} + +
` + ) + .join('')} +
- ${u.items - .map( - (it) => `
- ${TYPE_ICON[it.type]} - ${esc(it.title)} - ${TYPE_LABEL[it.type]} - -
` - ) - .join('')} -
` ) .join('') || `

No units yet — add one below.

`; + document.querySelectorAll('#cd-units .unit-toggle').forEach((hdr) => { + hdr.addEventListener('click', (e) => { + if (e.target.closest('button')) return; + const block = hdr.closest('.unit-block'); + block.classList.toggle('open'); + }); + }); + document.querySelectorAll('[data-del-unit]').forEach((btn) => btn.addEventListener('click', async () => { if (!confirm('Delete this unit and everything in it?')) return; diff --git a/public/app.js b/public/app.js index 58d18d3..12d704b 100644 --- a/public/app.js +++ b/public/app.js @@ -508,34 +508,82 @@ function printSpellingWorksheet(name, words) { // ---------- course detail ---------- +function activeUnitIndex(units) { + const today = new Date().toISOString().slice(0, 10); + const dated = units.map((u, i) => { + const dates = u.items.map((it) => it.due_date).filter(Boolean).sort(); + return { i, earliest: dates[0] || null, latest: dates[dates.length - 1] || null }; + }).filter((x) => x.earliest); + + if (dated.length) { + const spanning = dated.find((x) => x.earliest <= today && x.latest >= today); + if (spanning) return spanning.i; + const past = dated.filter((x) => x.latest < today).sort((a, b) => b.latest.localeCompare(a.latest)); + if (past.length) return past[0].i; + const future = dated.filter((x) => x.earliest > today).sort((a, b) => a.earliest.localeCompare(b.earliest)); + if (future.length) return future[0].i; + } + const fallback = units.findIndex((u) => u.items.some((it) => it.status === 'not_started' && !it.locked)); + return fallback >= 0 ? fallback : 0; +} + async function openCourse(courseId) { const course = await api(`/api/courses/${courseId}/detail?studentId=${currentStudent.id}`); $('#course-title').textContent = course.name; + const activeIdx = activeUnitIndex(course.units); $('#course-units').innerHTML = course.units - .map( - (u) => `
-

${esc(u.name)}

- ${u.items - .map((it) => { - if (it.locked) { - return `
- 🔒 - ${esc(it.title)}${TYPE_LABEL[it.type]} - Complete previous item first -
`; - } - const badge = statusBadge(it.type, it.status, it.score, it.points_possible, it.status !== 'not_started'); - const due = it.due_date ? `Due ${it.due_date}` : ''; - return ``; - }) - .join('')} -
` - ) + .map((u, uIdx) => { + const total = u.items.length; + const done = u.items.filter((it) => it.status !== 'not_started').length; + const pct = total > 0 ? Math.round((done / total) * 100) : 0; + const isOpen = uIdx === activeIdx; + + const itemsHtml = u.items + .map((it) => { + if (it.locked) { + return `
+ 🔒 + ${esc(it.title)}${TYPE_LABEL[it.type]} + Complete previous item first +
`; + } + const badge = statusBadge(it.type, it.status, it.score, it.points_possible, it.status !== 'not_started'); + const due = it.due_date ? `Due ${it.due_date}` : ''; + return ``; + }) + .join(''); + + return `
+ +
${itemsHtml}
+
`; + }) .join(''); + + document.querySelectorAll('#course-units .unit-toggle').forEach((btn) => { + btn.addEventListener('click', () => { + const block = btn.closest('.unit-block'); + const nowOpen = block.classList.toggle('open'); + btn.setAttribute('aria-expanded', String(nowOpen)); + }); + }); + document.querySelectorAll('#course-units .today-row:not(.locked)').forEach((row) => row.addEventListener('click', () => { backTarget = () => openCourse(courseId); diff --git a/public/styles.css b/public/styles.css index 1b959ab..52204f6 100644 --- a/public/styles.css +++ b/public/styles.css @@ -283,8 +283,92 @@ table.results tr:nth-child(even) td { background: #faf7ee; } .course-pct { font-size: 0.85rem; color: var(--muted); } .progress-track.small { height: 8px; margin: 0.2rem 0; } -.unit-block { margin-bottom: 1.5rem; } -.unit-block h2 { font-size: 1.15rem; margin-bottom: 0.5rem; } +/* student course unit cards */ +#course-units .unit-block { + background: var(--card); border-radius: 16px; margin: 0.75rem 0; + box-shadow: 0 2px 10px rgba(45, 49, 66, 0.08); overflow: hidden; +} +#course-units .unit-toggle { + display: flex; align-items: center; justify-content: space-between; + width: 100%; background: none; border: none; padding: 1rem 1.1rem; + cursor: pointer; font-family: inherit; text-align: left; gap: 0.75rem; + border-bottom: 2px solid transparent; transition: border-color 0.15s; +} +#course-units .unit-block.open .unit-toggle { border-bottom-color: var(--accent); } +#course-units .unit-toggle-left { + display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; +} +#course-units .unit-num-badge { + font-size: 0.72rem; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.05em; background: var(--accent); color: #fff; + padding: 0.2em 0.55em; border-radius: 6px; flex-shrink: 0; +} +#course-units .unit-title-text { + font-size: 1.05rem; font-weight: 700; color: var(--ink); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +#course-units .unit-toggle-right { + display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; +} +#course-units .unit-done-count { + font-size: 0.82rem; font-weight: 700; color: var(--muted); white-space: nowrap; +} +#course-units .unit-mini-progress { width: 56px; height: 6px; margin: 0; } +#course-units .unit-chevron { + font-size: 1.15rem; color: var(--muted); display: inline-block; + transition: transform 0.22s ease; +} +#course-units .unit-block:not(.open) .unit-chevron { transform: rotate(-90deg); } +#course-units .unit-items-list { + max-height: 0; overflow: hidden; + transition: max-height 0.3s ease, padding 0.3s ease; + padding: 0 0.85rem; +} +#course-units .unit-block.open .unit-items-list { + max-height: 4000px; padding: 0.1rem 0.85rem 0.85rem; +} + +/* admin course unit cards */ +#cd-units .unit-block { + background: var(--card); border-radius: 16px; margin: 0.75rem 0; padding: 0; + box-shadow: 0 2px 10px rgba(45, 49, 66, 0.08); overflow: hidden; +} +#cd-units .unit-toggle { + display: flex; align-items: center; justify-content: space-between; + padding: 0.85rem 1rem; cursor: pointer; gap: 0.75rem; user-select: none; + border-bottom: 2px solid transparent; transition: border-color 0.15s; +} +#cd-units .unit-block.open .unit-toggle { border-bottom-color: var(--accent); } +#cd-units .unit-toggle-left { + display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; +} +#cd-units .unit-num-badge { + font-size: 0.72rem; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.05em; background: var(--accent); color: #fff; + padding: 0.2em 0.55em; border-radius: 6px; flex-shrink: 0; +} +#cd-units .unit-title-text { + font-size: 1rem; font-weight: 700; color: var(--ink); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +#cd-units .unit-toggle-right { + display: flex; align-items: center; gap: 0.6rem; flex-shrink: 0; +} +#cd-units .unit-item-count { + font-size: 0.82rem; font-weight: 700; color: var(--muted); white-space: nowrap; +} +#cd-units .unit-chevron { + font-size: 1.1rem; color: var(--muted); transition: transform 0.22s ease; +} +#cd-units .unit-block:not(.open) .unit-chevron { transform: rotate(-90deg); } +#cd-units .unit-items-list { + max-height: 0; overflow: hidden; + transition: max-height 0.3s ease, padding 0.3s ease; + padding: 0 0.85rem; +} +#cd-units .unit-block.open .unit-items-list { + max-height: 4000px; padding: 0.5rem 0.85rem 0.85rem; +} /* ---------- item viewers (lesson/assignment/quiz) ---------- */ .item-card {