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 ─────────────────────────────────────────────────── -->
|
<!-- ── Notifications tab ─────────────────────────────────────────────────── -->
|
||||||
<div id="tab-notifications" class="tab-panel">
|
<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 id="notif-list" class="notif-list"><span class="loading">Loading…</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2076,31 +2080,86 @@ async function toggleEvent(eventId, activate, btn) {
|
|||||||
|
|
||||||
// ── Notifications ─────────────────────────────────────────────────────────────
|
// ── 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() {
|
async function loadNotifications() {
|
||||||
try {
|
try {
|
||||||
const data = await api('GET', '/notifications');
|
const data = await api('GET', '/notifications');
|
||||||
const notifs = data.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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
el('notif-list').innerHTML = notifs.map(n => `
|
el('notif-list').innerHTML = notifs.map(n => `
|
||||||
<div class="notif-item ${n.type}">
|
<div class="notif-item ${n.type || ''} ${n.is_read ? 'notif-read' : 'notif-unread'}"
|
||||||
<div class="notif-title">${n.title}</div>
|
style="display:flex;gap:12px;align-items:flex-start;padding:12px 14px;
|
||||||
<div class="notif-msg">${n.message}</div>
|
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('');
|
</div>`).join('');
|
||||||
} catch (e) { el('notif-list').innerHTML = `<div class="error-msg">${e.message}</div>`; }
|
} 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 ─────────────────────────────────────────────────────────────────────
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
loadClub();
|
loadClub();
|
||||||
// Load notification badge count silently
|
// Load notification badge count silently
|
||||||
api('GET', '/notifications').then(d => {
|
api('GET', '/notifications').then(d => {
|
||||||
const n = (d.notifications || []).length;
|
const unread = d.unread_count ?? (d.notifications || []).filter(n => !n.is_read).length;
|
||||||
el('notif-badge').textContent = n > 0 ? ` (${n})` : '';
|
el('notif-badge').textContent = unread > 0 ? ` (${unread})` : '';
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user