From 1eedfc77155072d8b3b0b61114501507590316ce Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:02:48 -0400 Subject: [PATCH] Add birthday celebrations for students MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parents can set a birthday (MM-DD) per student in the Kids panel. During the student's birthday week (Mon–Sun), a festive overlay appears on login with balloons, birthday cake, confetti, and a personalized message β€” shown once per day via sessionStorage. Summer birthdays (June–Aug) show a note in admin suggesting the parent set an alternate school-week date to celebrate early. Co-Authored-By: Claude Sonnet 4.6 --- db.js | 1 + public/admin.html | 3 +++ public/admin.js | 50 +++++++++++++++++++++++++++++++++++++++++++++-- public/app.js | 20 +++++++++++++++++++ public/index.html | 11 +++++++++++ public/styles.css | 26 ++++++++++++++++++++++++ server.js | 43 ++++++++++++++++++++++++++++++++++------ 7 files changed, 146 insertions(+), 8 deletions(-) diff --git a/db.js b/db.js index 543681a..4a21946 100644 --- a/db.js +++ b/db.js @@ -230,6 +230,7 @@ tryAlter(`ALTER TABLE students ADD COLUMN streak_count INTEGER NOT NULL DEFAULT tryAlter(`ALTER TABLE students ADD COLUMN best_streak INTEGER NOT NULL DEFAULT 0`); tryAlter(`ALTER TABLE students ADD COLUMN bg_pattern TEXT NOT NULL DEFAULT 'none'`); tryAlter(`ALTER TABLE students ADD COLUMN young_learner INTEGER NOT NULL DEFAULT 0`); +tryAlter(`ALTER TABLE students ADD COLUMN birthday TEXT`); tryAlter(`ALTER TABLE lists ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`); tryAlter(`ALTER TABLE decks ADD COLUMN group_name TEXT NOT NULL DEFAULT ''`); diff --git a/public/admin.html b/public/admin.html index 8605b9e..2a8d206 100644 --- a/public/admin.html +++ b/public/admin.html @@ -54,6 +54,9 @@ + diff --git a/public/admin.js b/public/admin.js index 9ec7d46..d06c910 100644 --- a/public/admin.js +++ b/public/admin.js @@ -118,6 +118,27 @@ function showPanel(name) { const THEME_COLORS = { blue: '#4f86f7', green: '#2e9e5b', purple: '#7c5cbf', orange: '#e8802a', pink: '#d45d8a', red: '#e84040', teal: '#20a8a0', yellow: '#c8960c', indigo: '#5b6abf' }; +const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + +function formatBirthday(mmdd) { + if (!mmdd) return ''; + const [mm, dd] = mmdd.split('-'); + const month = MONTH_NAMES[Number(mm) - 1] || mm; + return `${month} ${Number(dd)}`; +} + +// Convert stored MM-DD to YYYY-MM-DD for (uses current year as placeholder) +function birthdayToDateInput(mmdd) { + if (!mmdd) return ''; + return `${new Date().getFullYear()}-${mmdd}`; +} + +function isSummerBirthday(mmdd) { + if (!mmdd) return false; + const mm = Number(mmdd.split('-')[0]); + return mm >= 6 && mm <= 8; +} + async function loadKids() { const students = await api('/api/students'); $('#student-rows').innerHTML = students.length @@ -131,6 +152,11 @@ async function loadKids() { const youngLabel = s.young_learner ? `πŸ‘Ά Young` : ''; + const bdayVal = birthdayToDateInput(s.birthday); + const bdayDisplay = s.birthday ? `πŸŽ‚ ${formatBirthday(s.birthday)}` : 'πŸŽ‚ β€”'; + const summerNote = isSummerBirthday(s.birthday) + ? `β˜€οΈ Summer birthday` + : ''; return `
${esc(s.emoji)} ${esc(s.name)} @@ -140,6 +166,13 @@ async function loadKids() { +
+ ${bdayDisplay} + ${summerNote} + +
`; }) @@ -151,7 +184,6 @@ async function loadKids() { const studentId = btn.dataset.studentTheme; const theme = btn.dataset.theme; await api(`/api/students/${studentId}`, { method: 'PUT', body: { theme } }); - // Update selected state without full reload document.querySelectorAll(`[data-student-theme="${studentId}"]`).forEach((b) => b.classList.toggle('selected', b.dataset.theme === theme) ); @@ -167,6 +199,17 @@ async function loadKids() { }) ); + // Birthday edit: auto-save on change, strip year from YYYY-MM-DD β†’ MM-DD + document.querySelectorAll('.birthday-input').forEach((input) => + input.addEventListener('change', async () => { + const studentId = input.dataset.birthdayStudent; + const birthday = input.value ? input.value.slice(5) : ''; + await api(`/api/students/${studentId}`, { method: 'PUT', body: { birthday } }); + msg('Birthday saved.'); + loadKids(); + }) + ); + document.querySelectorAll('[data-del-student]').forEach((btn) => btn.addEventListener('click', async () => { if (!confirm('Remove this kid and all their progress? This cannot be undone.')) return; @@ -181,8 +224,11 @@ $('#add-student-form').addEventListener('submit', async (e) => { e.preventDefault(); const name = $('#new-student-name').value.trim(); if (!name) return; - await api('/api/students', { method: 'POST', body: { name, emoji: $('#new-student-emoji').value } }); + const bdayFull = $('#new-student-birthday').value; // YYYY-MM-DD from + const birthday = bdayFull ? bdayFull.slice(5) : ''; // strip year β†’ MM-DD + await api('/api/students', { method: 'POST', body: { name, emoji: $('#new-student-emoji').value, birthday } }); $('#new-student-name').value = ''; + $('#new-student-birthday').value = ''; msg('Kid added.'); loadKids(); }); diff --git a/public/app.js b/public/app.js index 6ea240c..7e93e88 100644 --- a/public/app.js +++ b/public/app.js @@ -342,6 +342,9 @@ async function openKid(id, tab = 'today', welcome = false) { switchTab(tab); if (welcome) showWelcomeToast(currentStudent.emoji, currentStudent.name); + // Birthday celebration β€” show once per day when it's the student's birthday week + if (state.student.isBirthdayWeek) showBirthdayCelebration(state.student.name); + // Celebrate if all today's tasks are done if (agenda.tasks.length > 0) { const allDone = agenda.tasks.every((t) => @@ -378,6 +381,23 @@ $('#celebration-close').addEventListener('click', () => { $('#celebration-overlay').hidden = true; }); +// ---------- birthday celebration ---------- + +function showBirthdayCelebration(name) { + const key = `bday_shown_${name}_${todayStr()}`; + if (sessionStorage.getItem(key)) return; // only once per session per day + sessionStorage.setItem(key, '1'); + $('#birthday-heading').textContent = `Happy Birthday, ${name}! πŸŽ‰`; + $('#birthday-message').textContent = 'Wishing you an amazing day full of fun and joy!'; + $('#birthday-overlay').hidden = false; + playFanfare(); + popConfetti(); +} + +$('#birthday-close').addEventListener('click', () => { + $('#birthday-overlay').hidden = true; +}); + // ---------- mini celebration (confetti + toast) ---------- let pendingMiniCelebration = false; diff --git a/public/index.html b/public/index.html index db1ea32..89823e0 100644 --- a/public/index.html +++ b/public/index.html @@ -110,6 +110,17 @@ + + +