/**
* @fileoverview Notification List Component for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Displays user notifications and mentions
* @keywords notification, list, alerts, mentions, updates
*/
import { BaseComponent } from './base-component.js';
import { formatRelativeTime } from '../utils/date.js';
class NotificationList extends BaseComponent {
init() {
this.notifications = [];
this.isLoading = false;
this.unreadCount = 0;
this.render();
this.bindEvents();
}
async load() {
if (!this.isLoggedIn()) {
this.render();
return;
}
this.isLoading = true;
this.render();
try {
const result = await this.getApi()?.getNotifications();
if (result?.success) {
this.notifications = result.notifications || [];
this.unreadCount = result.unread?.total || 0;
}
} catch (error) {
this.notifications = [];
} finally {
this.isLoading = false;
this.render();
}
}
render() {
this.addClass('notification-list');
if (!this.isLoggedIn()) {
this.setHtml(`
<div class="notification-auth">
<p>Sign in to view notifications</p>
<button class="btn btn-primary login-btn">Sign In</button>
</div>
`);
return;
}
if (this.isLoading) {
this.setHtml(`
<div class="notification-loading">
<loading-spinner text="Loading notifications..."></loading-spinner>
</div>
`);
return;
}
if (this.notifications.length === 0) {
this.setHtml(`
<div class="notification-empty">
<svg viewBox="0 0 24 24" width="48" height="48">
<path fill="currentColor" d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"/>
</svg>
<p>No notifications</p>
</div>
`);
return;
}
this.setHtml(`
<header class="notification-header">
<h2>Notifications</h2>
${this.unreadCount > 0 ? `
<button class="clear-btn">Mark all read</button>
` : ''}
</header>
<div class="notification-items">
${this.notifications.map(notif => `
<notification-item
data-notif='${JSON.stringify(notif).replace(/'/g, '&#39;')}'>
</notification-item>
`).join('')}
</div>
`);
this.initNotificationItems();
}
initNotificationItems() {
const items = this.$$('notification-item');
items.forEach(item => {
const notifData = item.dataset.notif;
if (notifData) {
try {
const notif = JSON.parse(notifData.replace(/&#39;/g, "'"));
item.setNotification(notif);
} catch (e) {}
}
});
}
bindEvents() {
this.on(this, 'click', this.handleClick);
}
handleClick(e) {
const loginBtn = e.target.closest('.login-btn');
const clearBtn = e.target.closest('.clear-btn');
if (loginBtn) {
this.getRouter()?.goToLogin();
return;
}
if (clearBtn) {
this.clearNotifications();
}
}
async clearNotifications() {
try {
await this.getApi()?.clearNotifications();
this.unreadCount = 0;
this.emit('notifications-cleared');
await this.load();
} catch (error) {}
}
onConnected() {
this.load();
}
onDisconnected() {
this.isLoading = false;
}
getUnreadCount() {
return this.unreadCount;
}
refresh() {
this.load();
}
}
customElements.define('notification-list', NotificationList);
class NotificationItem extends BaseComponent {
init() {
this.notifData = null;
this.render();
this.bindEvents();
}
setNotification(notif) {
this.notifData = notif;
this.render();
}
render() {
if (!this.notifData) {
this.setHtml('');
return;
}
const notif = this.notifData;
const isUnread = notif.read === 0;
const typeLabel = this.getTypeLabel(notif.type);
const username = notif.username || notif.user_username || notif.name || 'Someone';
this.addClass('notification-item');
if (isUnread) {
this.addClass('unread');
}
this.setHtml(`
<div class="notif-content">
<div class="notif-icon">${this.getTypeIcon(notif.type)}</div>
<div class="notif-body">
<span class="notif-username">${username}</span>
<span class="notif-action">${typeLabel}</span>
</div>
<time class="notif-time">${formatRelativeTime(notif.created_time)}</time>
</div>
`);
}
getTypeLabel(type) {
const labels = {
'comment_mention': 'mentioned you',
'comment_content': 'commented on your rant',
'comment_vote': 'upvoted your comment',
'rant_vote': 'upvoted your rant',
'comment_discuss': 'replied to a discussion'
};
return labels[type] || 'interacted';
}
getTypeIcon(type) {
if (type.includes('mention')) {
return `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>`;
}
if (type.includes('vote')) {
return `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>`;
}
return `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/></svg>`;
}
bindEvents() {
this.on(this, 'click', this.handleClick);
}
handleClick() {
if (this.notifData?.rant_id) {
this.getRouter()?.goToRant(
this.notifData.rant_id,
this.notifData.comment_id
);
}
}
}
customElements.define('notification-item', NotificationItem);
export { NotificationList, NotificationItem };