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:
+66
-7
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user