From 5fd750cd6adf04d569356992f928ddbe1196d268 Mon Sep 17 00:00:00 2001 From: Ananya Date: Sun, 31 May 2026 20:16:32 +0530 Subject: [PATCH 1/3] notifications --- .gitignore | 3 + public/js/layout.js | 78 ++++- public/notification-preferences.html | 122 ++++++++ public/notifications.html | 380 ++++++++++++++++++++++++ public/partials/navbar.html | 12 +- schema.sql | 10 + src/worker.py | 383 +++++++++++++++++++++++-- tests/test_join_idempotent.py | 36 +++ tests/test_notification_preferences.py | 75 +++++ tests/test_notification_triggers.py | 86 ++++++ tests/test_notifications_api.py | 122 ++++++++ 11 files changed, 1266 insertions(+), 41 deletions(-) create mode 100644 public/notification-preferences.html create mode 100644 public/notifications.html create mode 100644 tests/test_join_idempotent.py create mode 100644 tests/test_notification_preferences.py create mode 100644 tests/test_notification_triggers.py create mode 100644 tests/test_notifications_api.py diff --git a/.gitignore b/.gitignore index aeda65c..ed515ba 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ Thumbs.db .idea/ *.swp .env* + +# Local reference folder +_django_ref/ diff --git a/public/js/layout.js b/public/js/layout.js index 182ffbe..ee2b44f 100644 --- a/public/js/layout.js +++ b/public/js/layout.js @@ -17,9 +17,9 @@ function logout() { window.location.href = '/login.html'; } -function esc(s) { +window.esc = window.esc || function (s) { return String(s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} +}; // ── Dark mode utilities ─────────────────────────────────────────────── window.toggleDarkMode = function () { @@ -109,15 +109,70 @@ function updateAuthSection() { if (mobileAvatarEl) mobileAvatarEl.textContent = firstLetter; if (mobileGreetingEl) mobileGreetingEl.textContent = `Hi, ${firstName}`; + + startUnreadPolling(); } else { // User is not logged in if (notLoggedInDiv) notLoggedInDiv.classList.remove('hidden'); if (loggedInDiv) loggedInDiv.classList.add('hidden'); if (mobileNotLoggedIn) mobileNotLoggedIn.classList.remove('hidden'); if (mobileLoggedIn) mobileLoggedIn.classList.add('hidden'); + + const badge = document.getElementById('notif-unread-badge'); + if (badge) badge.classList.add('hidden'); + stopUnreadPolling(); + } +} + +window.updateAuthSection = updateAuthSection; + +async function refreshUnreadBadge() { + const badge = document.getElementById('notif-unread-badge'); + const { token } = getAuth(); + if (!badge) return; + if (!token) { + badge.classList.add('hidden'); + return; + } + try { + const res = await fetch('/api/notifications/unread-count', { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return; + const body = await res.json(); + const count = (body.data && body.data.unread_count) || 0; + if (count > 0) { + badge.textContent = count > 99 ? '99+' : String(count); + badge.classList.remove('hidden'); + } else { + badge.classList.add('hidden'); + } + } catch (_) { + /* ignore */ } } +window.refreshUnreadBadge = refreshUnreadBadge; + +var unreadPollTimer = null; + +function startUnreadPolling() { + if (unreadPollTimer) return; + unreadPollTimer = setInterval(() => { + refreshUnreadBadge(); + }, 5000); + refreshUnreadBadge(); +} + +function stopUnreadPolling() { + if (!unreadPollTimer) return; + clearInterval(unreadPollTimer); + unreadPollTimer = null; +} + +window.startUnreadPolling = startUnreadPolling; +window.stopUnreadPolling = stopUnreadPolling; + // ── Mobile menu toggle ──────────────────────────────────────────────── window.toggleMobileMenu = function () { const menu = document.getElementById('mobile-menu'); @@ -173,12 +228,23 @@ document.addEventListener('click', (event) => { }); // ── Initialize on DOM ready ─────────────────────────────────────────── -document.addEventListener('DOMContentLoaded', async () => { +async function initLayout() { initializeDarkMode(); - await inject('site-navbar', '/partials/navbar.html', updateAuthSection); + await inject('site-navbar', '/partials/navbar.html', () => { + updateAuthSection(); + if (window.refreshUnreadBadge) window.refreshUnreadBadge(); + }); await inject('site-footer', '/partials/footer.html'); - updateDarkModeIcon(); -}); + updateDarkModeIcon(); +} + +window.initLayout = initLayout; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initLayout); +} else { + initLayout(); +} // Conditional rendering for the "Join Lobby Classroom" button. // These elements only exist on the homepage, so we guard against nulls. diff --git a/public/notification-preferences.html b/public/notification-preferences.html new file mode 100644 index 0000000..821ceef --- /dev/null +++ b/public/notification-preferences.html @@ -0,0 +1,122 @@ + + + + + + + Notification Preferences - Alpha One Labs + + + + + + + + + +
+

Notification preferences

+

Choose which in-app notifications you receive.

+ + + +
+ + + +
+ + + Back to notifications + +
+
+
+ + + + + + + diff --git a/public/notifications.html b/public/notifications.html new file mode 100644 index 0000000..541071d --- /dev/null +++ b/public/notifications.html @@ -0,0 +1,380 @@ + + + + + + + Notifications - Alpha One Labs + + + + + + + + + +
+
+
+ +

+ Notifications + +

+ +
+ +
+
+ +
+
+
+ + +
+
+ + Preferences + + +
+
+ +
+
+
+ + +
+ + + + + + + diff --git a/public/partials/navbar.html b/public/partials/navbar.html index 727d02a..ea6f2b3 100644 --- a/public/partials/navbar.html +++ b/public/partials/navbar.html @@ -179,8 +179,9 @@ - + + @@ -239,9 +240,8 @@ Dashboard - - - Settings + + Notifications