server and web app
This commit is contained in:
123
apps/server/internal/httpserver/static/notification-bell.js
Normal file
123
apps/server/internal/httpserver/static/notification-bell.js
Normal file
@@ -0,0 +1,123 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const bell = document.querySelector('[data-notification-bell]');
|
||||
if (!bell) return;
|
||||
|
||||
const countEl = bell.querySelector('[data-unread-count]');
|
||||
const dropdown = bell.querySelector('[data-notification-dropdown]');
|
||||
let isOpen = false;
|
||||
|
||||
bell.addEventListener('click', () => {
|
||||
isOpen = !isOpen;
|
||||
dropdown.hidden = !isOpen;
|
||||
if (isOpen) loadNotifications();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!bell.contains(e.target)) {
|
||||
isOpen = false;
|
||||
dropdown.hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
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 = '';
|
||||
|
||||
if (!notifications.length) {
|
||||
list.innerHTML = '<li class="notification-empty">No notifications</li>';
|
||||
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();
|
||||
})();
|
||||
Reference in New Issue
Block a user