banyaro/backend/static/js/pages/notifications.js

237 lines
7.4 KiB
JavaScript

/* BAN YARO — Notification Center */
window.Page_notifications = (() => {
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
/** Relativer Zeitstempel: "vor 2h", "vor 3 Tagen", etc. */
function _relTime(isoStr) {
if (!isoStr) return '';
const diff = Date.now() - new Date(isoStr + (isoStr.includes('Z') ? '' : 'Z')).getTime();
const min = Math.floor(diff / 60000);
if (min < 1) return 'gerade eben';
if (min < 60) return `vor ${min} Min.`;
const h = Math.floor(min / 60);
if (h < 24) return `vor ${h}h`;
const d = Math.floor(h / 24);
if (d < 30) return `vor ${d} Tag${d !== 1 ? 'en' : ''}`;
const mo = Math.floor(d / 30);
return `vor ${mo} Monat${mo !== 1 ? 'en' : ''}`;
}
/** Phosphor-Icon-Name je nach Notification-Typ */
function _iconForType(type) {
switch (type) {
case 'chat_message': return 'chat-circle';
case 'friend_request': return 'user-plus';
case 'health_reminder': return 'first-aid';
case 'milestone': return 'star';
case 'poison_alert': return 'warning';
default: return 'bell';
}
}
/** Rendert eine einzelne Notification als HTML-String */
function _renderItem(n) {
const unread = !n.read_at;
const iconName = unread ? _iconForType(n.type) : 'bell';
const cls = ['notif-item', unread ? 'notif-unread' : ''].filter(Boolean).join(' ');
return `
<div class="${cls}" data-id="${n.id}" data-page="${UI.escape((n.data && JSON.parse(n.data || '{}').page) || '')}">
<span class="notif-icon">${UI.icon(iconName)}</span>
<div class="notif-content">
<div class="notif-title">${UI.escape(n.title)}</div>
${n.body ? `<div class="notif-body">${UI.escape(n.body)}</div>` : ''}
<div class="notif-time">${_relTime(n.created_at)}</div>
</div>
<button class="notif-del-btn icon-btn" data-del="${n.id}" title="Löschen"
aria-label="Benachrichtigung löschen">
${UI.icon('x')}
</button>
</div>`;
}
// ----------------------------------------------------------
// init
// ----------------------------------------------------------
async function init(container, appState, params) {
container.innerHTML = `
<div class="page-header">
<h2>${UI.icon('bell')} Benachrichtigungen</h2>
<button class="btn btn-sm btn-ghost" id="notif-read-all">Alle gelesen</button>
</div>
<div id="notif-list" class="notif-list">
<div class="loading-spinner"></div>
</div>`;
_addStyles();
// Daten laden
let items = [];
try {
items = await API.notifications.list();
} catch (e) {
document.getElementById('notif-list').innerHTML =
`<div class="empty-state">${UI.icon('warning')} Fehler beim Laden.</div>`;
return;
}
_render(items);
// "Alle gelesen"-Button
document.getElementById('notif-read-all')?.addEventListener('click', async () => {
try {
await API.notifications.readAll();
// Alle als gelesen markieren (lokal)
items = items.map(n => ({ ...n, read_at: new Date().toISOString() }));
_render(items);
} catch (e) {
UI.toast?.('Fehler beim Markieren.', 'error');
}
});
}
// ----------------------------------------------------------
// Render-Helfer
// ----------------------------------------------------------
function _render(items) {
const list = document.getElementById('notif-list');
if (!list) return;
if (!items || items.length === 0) {
list.innerHTML = `
<div class="empty-state">
${UI.icon('bell-slash')}
<p>Keine Benachrichtigungen</p>
</div>`;
return;
}
list.innerHTML = items.map(_renderItem).join('');
// Klick auf Notification: als gelesen + ggf. navigieren
list.querySelectorAll('.notif-item').forEach(el => {
el.addEventListener('click', async (e) => {
// Löschen-Button nicht doppelt behandeln
if (e.target.closest('.notif-del-btn')) return;
const id = parseInt(el.dataset.id, 10);
const page = el.dataset.page;
// Optisch sofort als gelesen markieren
el.classList.remove('notif-unread');
try { await API.notifications.read(id); } catch (_) {}
if (page && window.App?.navigate) {
window.App.navigate(page);
}
});
});
// Löschen-Buttons
list.querySelectorAll('.notif-del-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const id = parseInt(btn.dataset.del, 10);
try {
await API.notifications.delete(id);
btn.closest('.notif-item')?.remove();
if (!list.querySelector('.notif-item')) {
list.innerHTML = `
<div class="empty-state">
${UI.icon('bell-slash')}
<p>Keine Benachrichtigungen</p>
</div>`;
}
} catch (_) {}
});
});
}
// ----------------------------------------------------------
// Inline-Styles (einmalig einfügen)
// ----------------------------------------------------------
function _addStyles() {
if (document.getElementById('notif-styles')) return;
const style = document.createElement('style');
style.id = 'notif-styles';
style.textContent = `
.notif-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-4) 0;
}
.notif-item {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
background: var(--c-surface);
border: 1px solid var(--c-border-light);
cursor: pointer;
transition: background var(--transition-fast);
}
.notif-item:hover {
background: var(--c-surface-2);
}
.notif-item.notif-unread {
border-left: 3px solid var(--c-primary);
background: var(--c-primary-subtle);
}
.notif-item.notif-unread .notif-title {
font-weight: var(--weight-semibold);
}
.notif-icon {
flex-shrink: 0;
color: var(--c-primary);
margin-top: 2px;
}
.notif-content {
flex: 1;
min-width: 0;
}
.notif-title {
font-size: var(--text-sm);
color: var(--c-text);
line-height: var(--leading-snug);
}
.notif-body {
font-size: var(--text-xs);
color: var(--c-text-secondary);
margin-top: var(--space-1);
line-height: var(--leading-relaxed);
}
.notif-time {
font-size: var(--text-xs);
color: var(--c-text-muted);
margin-top: var(--space-1);
}
.notif-del-btn {
flex-shrink: 0;
color: var(--c-text-muted);
opacity: 0;
transition: opacity var(--transition-fast);
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.notif-item:hover .notif-del-btn {
opacity: 1;
}
@media (hover: none) {
.notif-del-btn { opacity: 1; }
}
`;
document.head.appendChild(style);
}
return { init };
})();