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..774f345 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'); @@ -172,13 +227,47 @@ document.addEventListener('click', (event) => { } }); +function applyUnreadBadgeCount(count) { + const badge = document.getElementById('notif-unread-badge'); + if (!badge) return; + if (count > 0) { + badge.textContent = count > 99 ? '99+' : String(count); + badge.classList.remove('hidden'); + badge.setAttribute('aria-label', `${count} unread notifications`); + } else { + badge.classList.add('hidden'); + badge.setAttribute('aria-label', ''); + } + const link = document.getElementById('notif-bell-link'); + if (link) { + link.setAttribute('aria-label', count > 0 ? `Notifications, ${count} unread` : 'Notifications'); + } +} + +window.applyUnreadBadgeCount = applyUnreadBadgeCount; + // ── Initialize on DOM ready ─────────────────────────────────────────── -document.addEventListener('DOMContentLoaded', async () => { - initializeDarkMode(); - await inject('site-navbar', '/partials/navbar.html', updateAuthSection); - await inject('site-footer', '/partials/footer.html'); - updateDarkModeIcon(); -}); +let layoutInitPromise = null; + +async function initLayout() { + if (!layoutInitPromise) { + layoutInitPromise = (async () => { + initializeDarkMode(); + await inject('site-navbar', '/partials/navbar.html', updateAuthSection); + await inject('site-footer', '/partials/footer.html'); + updateDarkModeIcon(); + })(); + } + return layoutInitPromise; +} + +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..7fa4254 --- /dev/null +++ b/public/notification-preferences.html @@ -0,0 +1,126 @@ + + + + + + + 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..ae36987 --- /dev/null +++ b/public/notifications.html @@ -0,0 +1,395 @@ + + + + + + + Notifications - Alpha One Labs + + + + + + + + + +
+
+
+ +

+ Notifications + +

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