Add Cloudflare email and collaboration workflows

This commit is contained in:
2026-07-22 12:59:16 -04:00
parent 09f51d1ef4
commit 4454e918c2
35 changed files with 1596 additions and 487 deletions

View File

@@ -7,6 +7,7 @@
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) => {
@@ -76,31 +77,42 @@
notifications.forEach(n => {
const li = document.createElement('li');
li.className = n.readAt ? 'notification-item is-read' : 'notification-item';
li.innerHTML = `
const button = document.createElement('button');
button.type = 'button';
button.className = 'notification-item__button';
button.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));
}
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
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);
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) {
async function markRead(id, refresh = true) {
try {
await fetch(`/api/notifications/${id}/read`, { method: 'POST' });
updateBell();
loadNotifications();
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);
}
@@ -108,7 +120,8 @@
async function markAllRead() {
try {
await fetch('/api/notifications/read-all', { method: 'POST' });
const response = await fetch('/api/notifications/read-all', { method: 'POST' });
if (!response.ok) throw new Error(await response.text());
updateBell();
loadNotifications();
} catch (err) {
@@ -127,6 +140,12 @@
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', () => {