/** * @fileoverview A popover component to display user notifications. * Grouped by date (Today, Yesterday, Earlier) with mark as read support. */ import notificationService from '../services/notification-service.js'; class DsNotificationCenter extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this._isConnected = false; } connectedCallback() { this._isConnected = true; this.render(); this._updateNotifications = this._updateNotifications.bind(this); notificationService.addEventListener('notifications-updated', this._updateNotifications); // Initialize the service and get initial notifications // Only update if component is still connected when promise resolves notificationService.init().then(() => { if (this._isConnected) { this._updateNotifications({ detail: { notifications: notificationService.getAll() } }); } }).catch((error) => { console.error('[DsNotificationCenter] Failed to initialize notifications:', error); }); this.shadowRoot.getElementById('mark-all-read').addEventListener('click', () => { notificationService.markAllAsRead(); }); this.shadowRoot.getElementById('clear-all').addEventListener('click', () => { notificationService.clearAll(); }); this.shadowRoot.getElementById('notification-list').addEventListener('click', this._handleNotificationClick.bind(this)); } disconnectedCallback() { this._isConnected = false; notificationService.removeEventListener('notifications-updated', this._updateNotifications); } _handleNotificationClick(e) { const notificationEl = e.target.closest('.notification'); if (!notificationEl) return; const id = notificationEl.dataset.id; if (!id) return; // Mark as read if it was unread if (notificationEl.classList.contains('unread')) { notificationService.markAsRead(id); } // Handle action button clicks const actionButton = e.target.closest('[data-event]'); if (actionButton) { let payload = {}; try { payload = JSON.parse(actionButton.dataset.payload || '{}'); } catch (e) { console.error('Invalid action payload:', e); } this.dispatchEvent(new CustomEvent('notification-action', { bubbles: true, composed: true, detail: { event: actionButton.dataset.event, payload } })); // Close the notification center this.removeAttribute('open'); } // Handle delete button const deleteButton = e.target.closest('.delete-btn'); if (deleteButton) { e.stopPropagation(); notificationService.delete(id); } } _updateNotifications({ detail }) { const { notifications } = detail; const listEl = this.shadowRoot?.getElementById('notification-list'); // Null safety check - component may be disconnecting if (!listEl) { console.warn('[DsNotificationCenter] Notification list element not found'); return; } if (!notifications || notifications.length === 0) { listEl.innerHTML = `

No notifications yet

You're all caught up!
`; return; } const grouped = this._groupNotificationsByDate(notifications); let html = ''; for (const [groupTitle, groupNotifications] of Object.entries(grouped)) { html += `
${groupTitle}
${groupNotifications.map(n => this._renderNotification(n)).join('')}
`; } listEl.innerHTML = html; } _groupNotificationsByDate(notifications) { const groups = {}; const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const isSameDay = (d1, d2) => d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); notifications.forEach(n => { const date = new Date(n.timestamp); let groupName; if (isSameDay(date, today)) { groupName = 'Today'; } else if (isSameDay(date, yesterday)) { groupName = 'Yesterday'; } else { groupName = 'Earlier'; } if (!groups[groupName]) { groups[groupName] = []; } groups[groupName].push(n); }); return groups; } _renderNotification(n) { const time = new Date(n.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const actionsHtml = (n.actions || []).map(action => `` ).join(''); return `

${this._escapeHtml(n.title)}

${n.message ? `

${this._escapeHtml(n.message)}

` : ''}
${time} ${n.source ? `${n.source}` : ''}
${actionsHtml ? `
${actionsHtml}
` : ''}
`; } _escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } render() { this.shadowRoot.innerHTML = `

Notifications

`; } } customElements.define('ds-notification-center', DsNotificationCenter);