// Global notifications: bell with unread badge, dropdown, and WS toasts for // every lifecycle transition (ยง11.1). import { api, toast } from '/static/js/api.js'; import { subscribe, onPollFallback } from '/static/js/ws.js'; if (document.body.dataset.loggedIn === '1') { const bell = document.getElementById('nav-bell'); const badge = document.getElementById('nav-bell-count'); const panel = document.getElementById('notif-panel'); const list = document.getElementById('notif-list'); const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); async function refresh() { try { const res = await api('GET', '/api/v1/notifications?limit=15'); badge.textContent = res.unread > 0 ? String(res.unread) : ''; badge.hidden = res.unread === 0; list.replaceChildren(...res.notifications.map((n) => { const li = document.createElement('li'); li.innerHTML = ` ${esc(n.title)}${n.readAt ? '' : ' new'}
${esc(n.body || '')}
`; return li; })); if (!res.notifications.length) { list.innerHTML = '
  • No notifications.
  • '; } } catch (e) { /* signed out or transient */ } } bell.addEventListener('click', async () => { panel.hidden = !panel.hidden; if (!panel.hidden) { await refresh(); try { await api('POST', '/api/v1/notifications/read', { ids: [] }); } catch (e) { /* ignore */ } badge.hidden = true; badge.textContent = ''; } }); document.addEventListener('click', (e) => { if (!panel.hidden && !panel.contains(e.target) && e.target !== bell) panel.hidden = true; }); subscribe('notifications', (event, n) => { if (event === 'notification' && n) { toast(n.title, 'ok'); refresh(); } }); onPollFallback(refresh); refresh(); }