(function () { 'use strict'; const bell = document.querySelector('[data-notification-bell]'); if (!bell) return; const trigger = bell.querySelector('.notification-bell__trigger'); const countEl = bell.querySelector('[data-unread-count]'); const dropdown = bell.querySelector('[data-notification-dropdown]'); let isOpen = false; trigger.addEventListener('click', (e) => { e.stopPropagation(); isOpen = !isOpen; dropdown.hidden = !isOpen; trigger.setAttribute('aria-expanded', String(isOpen)); if (isOpen) loadNotifications(); }); dropdown.addEventListener('click', (e) => { e.stopPropagation(); }); document.addEventListener('click', () => { if (isOpen) { isOpen = false; dropdown.hidden = true; trigger.setAttribute('aria-expanded', 'false'); } }); async function updateBell() { try { const res = await fetch('/api/notifications/bell'); if (!res.ok) return; const data = await res.json(); const count = data.unreadCount || 0; countEl.textContent = count > 99 ? '99+' : String(count); countEl.hidden = count === 0; bell.classList.toggle('has-unread', count > 0); } catch (err) { console.error('bell update', err); } } async function loadNotifications() { try { const res = await fetch('/api/notifications?limit=20'); if (!res.ok) return; const data = await res.json(); renderNotifications(data.notifications || []); } catch (err) { console.error('load notifications', err); } } function renderNotifications(notifications) { const list = dropdown.querySelector('ul'); if (!list) return; list.innerHTML = ''; // Header const header = document.createElement('li'); header.className = 'notification-header'; header.textContent = 'Notifications'; list.appendChild(header); if (!notifications.length) { const empty = document.createElement('li'); empty.className = 'notification-empty'; empty.textContent = 'No notifications'; list.appendChild(empty); return; } notifications.forEach(n => { const li = document.createElement('li'); li.className = n.readAt ? 'notification-item is-read' : 'notification-item'; li.innerHTML = `
${escapeHtml(n.message)}
`; if (!n.readAt) { li.addEventListener('click', () => markRead(n.id)); } list.appendChild(li); }); // Add "Mark all read" button const footer = document.createElement('li'); footer.className = 'notification-footer'; const btn = document.createElement('button'); btn.textContent = 'Mark all read'; btn.addEventListener('click', markAllRead); footer.appendChild(btn); list.appendChild(footer); } async function markRead(id) { try { await fetch(`/api/notifications/${id}/read`, { method: 'POST' }); updateBell(); loadNotifications(); } catch (err) { console.error('mark read', err); } } async function markAllRead() { try { await fetch('/api/notifications/read-all', { method: 'POST' }); updateBell(); loadNotifications(); } catch (err) { console.error('mark all read', err); } } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatDate(iso) { const d = new Date(iso); return d.toLocaleString(); } // Listen for WebSocket notification events if (window.__cairnquireRealtime) { window.__cairnquireRealtime.on('notification', () => { updateBell(); }); } // Poll every 60s as fallback setInterval(updateBell, 60000); updateBell(); })();