Phase 19: notifications tab with mark-read support

Notifications tab now fully functional. Each notification shows a type
icon, colored left border (gold=level_up, green=objective, amber=warning,
red=expired, blue=season), bold title when unread, body text, timestamp,
and a per-item Mark read button for persistent notifications.

Header badge and the unread count label now use unread_count from the
Core response rather than total notification count.

A Mark all read button calls POST /notifications/read-all and refreshes
the list. Per-item PATCH /notifications/:id/read hides the button and
dims the card on success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 17:57:40 -07:00
parent 30dfb25cf7
commit 44c3a0ce5b
+66 -7
View File
@@ -640,6 +640,10 @@ h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;
<!-- ── Notifications tab ─────────────────────────────────────────────────── -->
<div id="tab-notifications" class="tab-panel">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="margin:0">Notifications <span id="notif-unread-label" style="font-size:0.85rem;color:#8b949e;font-weight:400"></span></h2>
<button class="btn btn-secondary btn-sm" onclick="markAllNotificationsRead()">Mark all read</button>
</div>
<div id="notif-list" class="notif-list"><span class="loading">Loading…</span></div>
</div>
@@ -2076,31 +2080,86 @@ async function toggleEvent(eventId, activate, btn) {
// ── Notifications ─────────────────────────────────────────────────────────────
const NOTIF_ICONS = {
level_up: '⬆',
objective_complete: '✓',
loan_expiring: '⏳',
loan_expired: '✕',
season_end: '🏆',
season_ending: '⌛',
};
function notifKindLabel(type) {
return {
level_up: 'Level Up',
objective_complete: 'Objective',
loan_expiring: 'Loan Warning',
loan_expired: 'Loan Expired',
season_end: 'Season',
season_ending: 'Season',
}[type] || type;
}
async function loadNotifications() {
try {
const data = await api('GET', '/notifications');
const notifs = data.notifications || [];
el('notif-badge').textContent = notifs.length > 0 ? ` (${notifs.length})` : '';
const unread = data.unread_count ?? notifs.filter(n => !n.is_read).length;
el('notif-badge').textContent = unread > 0 ? ` (${unread})` : '';
if (el('notif-unread-label')) {
el('notif-unread-label').textContent = unread > 0 ? `${unread} unread` : 'all read';
}
if (!notifs.length) {
el('notif-list').innerHTML = '<span class="empty">No notifications right now.</span>';
el('notif-list').innerHTML = '<span class="empty">No notifications yet. Play matches, complete objectives, and level up to generate notifications.</span>';
return;
}
el('notif-list').innerHTML = notifs.map(n => `
<div class="notif-item ${n.type}">
<div class="notif-title">${n.title}</div>
<div class="notif-msg">${n.message}</div>
<div class="notif-item ${n.type || ''} ${n.is_read ? 'notif-read' : 'notif-unread'}"
style="display:flex;gap:12px;align-items:flex-start;padding:12px 14px;
background:${n.is_read ? '#0d1117' : '#161b22'};border-radius:8px;
margin-bottom:8px;border-left:3px solid ${notifBorderColor(n.type)}">
<span style="font-size:1.2rem;min-width:24px;text-align:center">${NOTIF_ICONS[n.type] || '•'}</span>
<div style="flex:1">
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
<div class="notif-title" style="font-weight:${n.is_read ? '400' : '600'};color:${n.is_read ? '#8b949e' : '#f0f6fc'}">${n.title}</div>
<span style="font-size:0.7rem;color:#6e7681;white-space:nowrap">${notifKindLabel(n.type)}</span>
</div>
<div class="notif-body" style="font-size:0.82rem;color:#8b949e;margin-top:3px">${n.body || n.message || ''}</div>
${n.created_at ? `<div style="font-size:0.7rem;color:#6e7681;margin-top:4px">${new Date(n.created_at).toLocaleString()}</div>` : ''}
</div>
${n.id && !n.is_read ? `<button class="btn btn-secondary btn-sm" onclick="markNotificationRead('${n.id}', this)" style="align-self:center;white-space:nowrap">Mark read</button>` : ''}
</div>`).join('');
} catch (e) { el('notif-list').innerHTML = `<div class="error-msg">${e.message}</div>`; }
}
function notifBorderColor(type) {
return { level_up: '#f5a623', objective_complete: '#3fb950', loan_expiring: '#d29922', loan_expired: '#f85149', season_end: '#58a6ff', season_ending: '#d29922' }[type] || '#30363d';
}
async function markNotificationRead(id, btn) {
btn.disabled = true;
try {
await api('PATCH', `/notifications/${id}/read`);
loadNotifications();
} catch (e) { showToast(e.message, true); btn.disabled = false; }
}
async function markAllNotificationsRead() {
try {
await api('POST', '/notifications/read-all');
showToast('All notifications marked as read');
loadNotifications();
} catch (e) { showToast(e.message, true); }
}
// ── Init ─────────────────────────────────────────────────────────────────────
window.addEventListener('DOMContentLoaded', () => {
loadClub();
// Load notification badge count silently
api('GET', '/notifications').then(d => {
const n = (d.notifications || []).length;
el('notif-badge').textContent = n > 0 ? ` (${n})` : '';
const unread = d.unread_count ?? (d.notifications || []).filter(n => !n.is_read).length;
el('notif-badge').textContent = unread > 0 ? ` (${unread})` : '';
}).catch(() => {});
});
</script>