(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]'); if (!trigger || !countEl || !dropdown) return; 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'; const button = document.createElement('button'); button.type = 'button'; button.className = 'notification-item__button'; button.innerHTML = `
`; button.addEventListener('click', async () => { if (!n.readAt) await markRead(n.id, false); const target = notificationURL(n); if (target) window.location.assign(target); }); li.appendChild(button); list.appendChild(li); }); // Add "Mark all read" button if (notifications.some(notification => !notification.readAt)) { 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, refresh = true) { try { const response = await fetch(`/api/notifications/${encodeURIComponent(id)}/read`, { method: 'POST' }); if (!response.ok) throw new Error(await response.text()); if (refresh) { updateBell(); loadNotifications(); } } catch (err) { console.error('mark read', err); } } async function markAllRead() { try { const response = await fetch('/api/notifications/read-all', { method: 'POST' }); if (!response.ok) throw new Error(await response.text()); 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(); } function notificationURL(notification) { if (notification.resourceType !== 'document' || !notification.resourceId) return ''; const path = notification.resourceId.replace(/^doc:/, '').split('/').map(encodeURIComponent).join('/'); return '/docs/' + path; } // Listen for WebSocket notification events if (window.__cairnquireRealtime) { window.__cairnquireRealtime.on('notification', () => { updateBell(); }); } // Poll every 60s as fallback setInterval(updateBell, 60000); updateBell(); })();