141 lines
3.8 KiB
JavaScript
141 lines
3.8 KiB
JavaScript
(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 = `
|
|
<div class="notification-message">${escapeHtml(n.message)}</div>
|
|
<time class="notification-time">${formatDate(n.createdAt)}</time>
|
|
`;
|
|
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();
|
|
})();
|