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.
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
-