UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

1,160 lines (997 loc) 30.8 kB
/** * Notification Banner Component * Implements notification count display with sidebar functionality * Integrates with CosmoDB via APIM internal endpoints */ import centralTokenManager from '../../utils/centralTokenManager.js'; import notificationsService from '../../services/notificationsService.js'; class NotificationBanner { constructor(options = {}) { this.options = { containerId: 'notification-banner-container', position: 'top-right', // top-right, top-left, etc. autoRefresh: true, refreshInterval: 30000, // 30 seconds maxDisplayCount: 9, // Show "9+" for counts above this ...options, }; this.initialized = false; this.isVisible = false; this.sidebarVisible = false; this.currentNotifications = []; this.unreadCounts = {}; this.refreshTimer = null; // Sidebar elements this.rightSidebar = null; this.leftSidebar = null; this.overlay = null; this.init(); } /** * Initialize the notification banner */ async init() { try { // Create banner HTML structure this.createBannerStructure(); // Setup event listeners this.setupEventListeners(); // Load initial notification counts await this.loadNotificationCounts(); // Setup auto-refresh if enabled if (this.options.autoRefresh) { this.startAutoRefresh(); } this.initialized = true; console.log('[NotificationBanner] [SUCCESS] Initialized successfully'); } catch (error) { console.error( '[NotificationBanner] [ERROR] Initialization failed:', error ); } } /** * Create the banner HTML structure */ createBannerStructure() { // Check if already exists if (document.getElementById('notification-banner')) { return; } const bannerHTML = ` <div id="notification-banner" class="notification-banner"> <div class="notification-button" id="notification-button"> <div class="notification-icon"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/> </svg> </div> <div class="notification-counts" id="notification-counts"> <div class="count-badge total-count" id="total-count" style="display: none;">0</div> </div> </div> </div> <!-- Right Sidebar for Notification List --> <div id="notification-right-sidebar" class="notification-sidebar right-sidebar" style="display: none;"> <div class="sidebar-header"> <h3>Notifications</h3> <div class="sidebar-controls"> <button id="mark-all-read-btn" class="control-btn">Mark All Read</button> <button id="close-sidebar-btn" class="control-btn close-btn">×</button> </div> </div> <div class="sidebar-content"> <div class="notification-filters"> <button class="filter-btn active" data-filter="all">All (<span id="filter-count-all">0</span>)</button> <button class="filter-btn" data-filter="unread">Unread (<span id="filter-count-unread">0</span>)</button> <button class="filter-btn" data-filter="system">System (<span id="filter-count-system">0</span>)</button> <button class="filter-btn" data-filter="billing">Billing (<span id="filter-count-billing">0</span>)</button> <button class="filter-btn" data-filter="technical">Technical (<span id="filter-count-technical">0</span>)</button> </div> <div id="notification-list" class="notification-list"> <div class="loading-message">Loading notifications...</div> </div> </div> </div> <!-- Left Sidebar for Full Notification Content --> <div id="notification-left-sidebar" class="notification-sidebar left-sidebar" style="display: none;"> <div class="sidebar-header"> <h3>Notification Details</h3> <button id="close-detail-sidebar-btn" class="control-btn close-btn">×</button> </div> <div class="sidebar-content"> <div id="notification-detail-content" class="notification-detail"> <p>Select a notification to view details</p> </div> <div class="notification-actions"> <button id="mark-read-btn" class="action-btn primary">Mark as Read</button> <button id="delete-notification-btn" class="action-btn secondary">Delete</button> </div> </div> </div> <!-- Overlay --> <div id="notification-overlay" class="notification-overlay" style="display: none;"></div> `; // Insert banner into page const container = document.getElementById(this.options.containerId) || document.body; const bannerElement = document.createElement('div'); bannerElement.innerHTML = bannerHTML; container.appendChild(bannerElement); // Add CSS styles this.addStyles(); } /** * Add CSS styles for the notification banner */ addStyles() { if (document.getElementById('notification-banner-styles')) { return; } const styles = ` <style id="notification-banner-styles"> .notification-banner { position: fixed; top: 20px; right: 20px; z-index: 10000; font-family: Arial, sans-serif; } .notification-button { background: #022d54; color: white; border: none; border-radius: 50%; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); transition: all 0.3s ease; position: relative; } .notification-button:hover { background: #034a8b; transform: scale(1.05); } .notification-icon { display: flex; align-items: center; justify-content: center; } .notification-counts { position: absolute; top: -8px; right: -8px; } .count-badge { background: #dc3545; color: white; border-radius: 10px; padding: 2px 6px; font-size: 12px; font-weight: bold; min-width: 18px; text-align: center; line-height: 1.2; } .notification-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9998; } .notification-sidebar { position: fixed; top: 0; bottom: 0; width: 400px; background: white; z-index: 9999; box-shadow: 0 0 20px rgba(0,0,0,0.3); display: flex; flex-direction: column; transform: translateX(100%); transition: transform 0.3s ease; } .notification-sidebar.visible { transform: translateX(0); } .right-sidebar { right: 0; } .left-sidebar { left: 0; transform: translateX(-100%); } .left-sidebar.visible { transform: translateX(0); } .sidebar-header { background: #022d54; color: white; padding: 1rem; display: flex; justify-content: space-between; align-items: center; } .sidebar-header h3 { margin: 0; font-size: 18px; font-weight: 500; } .sidebar-controls { display: flex; gap: 0.5rem; align-items: center; } .control-btn { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 12px; transition: background 0.2s; } .control-btn:hover { background: rgba(255,255,255,0.3); } .close-btn { width: 28px; height: 28px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: bold; } .sidebar-content { flex: 1; overflow-y: auto; padding: 1rem; } .notification-filters { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 1rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; } .filter-btn { background: #f8f9fa; color: #6c757d; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 12px; transition: all 0.2s; } .filter-btn:hover { background: #e9ecef; } .filter-btn.active { background: #022d54; color: white; border-color: #022d54; } .notification-list { display: flex; flex-direction: column; gap: 0.5rem; } .notification-item { background: white; border: 1px solid #e0e0e0; border-radius: 6px; padding: 0.75rem; cursor: pointer; transition: all 0.2s; position: relative; } .notification-item:hover { background: #f8f9fa; border-color: #022d54; } .notification-item.unread { border-left: 4px solid #dc3545; background: #fff9f9; } .notification-item.unread:hover { background: #fff5f5; } .notification-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; } .notification-title { font-weight: 500; color: #022d54; font-size: 14px; margin: 0; } .notification-time { font-size: 12px; color: #6c757d; } .notification-preview { color: #555; font-size: 13px; line-height: 1.4; margin: 0; } .notification-meta { display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #f0f0f0; } .notification-type { background: #e9ecef; color: #495057; padding: 0.125rem 0.375rem; border-radius: 12px; font-size: 11px; text-transform: uppercase; font-weight: 500; } .notification-actions-inline { display: flex; gap: 0.25rem; } .action-btn-small { background: transparent; color: #6c757d; border: 1px solid #e0e0e0; border-radius: 3px; padding: 0.125rem 0.375rem; cursor: pointer; font-size: 11px; transition: all 0.2s; } .action-btn-small:hover { background: #f8f9fa; color: #022d54; } .notification-detail { background: white; border-radius: 6px; padding: 1rem; margin-bottom: 1rem; } .notification-detail h4 { margin: 0 0 0.5rem 0; color: #022d54; } .notification-detail .detail-content { color: #555; line-height: 1.6; } .notification-actions { display: flex; gap: 0.5rem; padding-top: 1rem; border-top: 1px solid #e0e0e0; } .action-btn { padding: 0.5rem 1rem; border: 1px solid #e0e0e0; border-radius: 4px; cursor: pointer; font-size: 14px; transition: all 0.2s; } .action-btn.primary { background: #022d54; color: white; border-color: #022d54; } .action-btn.primary:hover { background: #034a8b; } .action-btn.secondary { background: white; color: #6c757d; } .action-btn.secondary:hover { background: #f8f9fa; color: #dc3545; } .loading-message { text-align: center; color: #6c757d; padding: 2rem; font-style: italic; } .error-message { text-align: center; color: #dc3545; padding: 2rem; background: #fff5f5; border-radius: 6px; border: 1px solid #f8d7da; } .empty-message { text-align: center; color: #6c757d; padding: 2rem; } @media (max-width: 768px) { .notification-sidebar { width: 100%; } .notification-banner { top: 10px; right: 10px; } .notification-button { width: 44px; height: 44px; } } </style> `; document.head.insertAdjacentHTML('beforeend', styles); } /** * Setup event listeners */ setupEventListeners() { // Banner click - show right sidebar const notificationButton = document.getElementById('notification-button'); if (notificationButton) { notificationButton.addEventListener('click', e => { e.stopPropagation(); this.toggleRightSidebar(); }); } // Close sidebar buttons const closeSidebarBtn = document.getElementById('close-sidebar-btn'); if (closeSidebarBtn) { closeSidebarBtn.addEventListener('click', () => this.hideRightSidebar()); } const closeDetailSidebarBtn = document.getElementById( 'close-detail-sidebar-btn' ); if (closeDetailSidebarBtn) { closeDetailSidebarBtn.addEventListener('click', () => this.hideLeftSidebar() ); } // Overlay click - close sidebars const overlay = document.getElementById('notification-overlay'); if (overlay) { overlay.addEventListener('click', () => this.hideAllSidebars()); } // Mark all read button const markAllReadBtn = document.getElementById('mark-all-read-btn'); if (markAllReadBtn) { markAllReadBtn.addEventListener('click', () => this.markAllAsRead()); } // Filter buttons const filterBtns = document.querySelectorAll('.filter-btn'); filterBtns.forEach(btn => { btn.addEventListener('click', e => { const filter = e.target.dataset.filter; this.applyFilter(filter); }); }); // Detail view action buttons const markReadBtn = document.getElementById('mark-read-btn'); if (markReadBtn) { markReadBtn.addEventListener('click', () => this.markCurrentAsRead()); } const deleteNotificationBtn = document.getElementById( 'delete-notification-btn' ); if (deleteNotificationBtn) { deleteNotificationBtn.addEventListener('click', () => this.deleteCurrentNotification() ); } // Close sidebars on ESC key document.addEventListener('keydown', e => { if (e.key === 'Escape') { this.hideAllSidebars(); } }); } /** * Load notification counts from the server */ async loadNotificationCounts() { try { // Check if user is authenticated if (!centralTokenManager.isAuthenticated()) { console.log( '[NotificationBanner] User not authenticated, hiding banner' ); this.hideBanner(); return; } console.log( '[NotificationBanner] [LOADING] Loading notification counts...' ); // Get notification statistics const stats = await notificationsService.getNotificationStats(); if (stats && stats.counts) { this.updateNotificationCounts(stats.counts); this.showBanner(); } else { console.warn('[NotificationBanner] No notification stats received'); this.hideBanner(); } } catch (error) { console.error( '[NotificationBanner] Failed to load notification counts:', error ); this.hideBanner(); } } /** * Update notification count display */ updateNotificationCounts(counts) { this.unreadCounts = counts; const totalCount = counts.total_unread || 0; const totalCountElement = document.getElementById('total-count'); if (totalCountElement) { if (totalCount > 0) { const displayCount = totalCount > this.options.maxDisplayCount ? `${this.options.maxDisplayCount}+` : totalCount.toString(); totalCountElement.textContent = displayCount; totalCountElement.style.display = 'block'; } else { totalCountElement.style.display = 'none'; } } // Update filter counts in sidebar this.updateFilterCounts(counts); console.log('[NotificationBanner] [SUCCESS] Counts updated:', counts); } /** * Update filter count displays */ updateFilterCounts(counts) { const filterCounts = { all: counts.total || 0, unread: counts.total_unread || 0, system: counts.system || 0, billing: counts.billing || 0, technical: counts.technical || 0, }; Object.entries(filterCounts).forEach(([filter, count]) => { const element = document.getElementById(`filter-count-${filter}`); if (element) { element.textContent = count; } }); } /** * Show the notification banner */ showBanner() { const banner = document.getElementById('notification-banner'); if (banner) { banner.style.display = 'block'; this.isVisible = true; } } /** * Hide the notification banner */ hideBanner() { const banner = document.getElementById('notification-banner'); if (banner) { banner.style.display = 'none'; this.isVisible = false; } } /** * Toggle right sidebar visibility */ toggleRightSidebar() { if (this.sidebarVisible) { this.hideRightSidebar(); } else { this.showRightSidebar(); } } /** * Show right sidebar with notification list */ async showRightSidebar() { const sidebar = document.getElementById('notification-right-sidebar'); const overlay = document.getElementById('notification-overlay'); if (sidebar && overlay) { overlay.style.display = 'block'; sidebar.style.display = 'flex'; // Trigger animation setTimeout(() => { sidebar.classList.add('visible'); }, 10); this.sidebarVisible = true; // Load notifications await this.loadNotifications(); } } /** * Hide right sidebar */ hideRightSidebar() { const sidebar = document.getElementById('notification-right-sidebar'); const overlay = document.getElementById('notification-overlay'); if (sidebar) { sidebar.classList.remove('visible'); setTimeout(() => { sidebar.style.display = 'none'; if (overlay) overlay.style.display = 'none'; }, 300); this.sidebarVisible = false; } } /** * Show left sidebar with notification details */ showLeftSidebar(notification) { const sidebar = document.getElementById('notification-left-sidebar'); if (sidebar) { sidebar.style.display = 'flex'; // Trigger animation setTimeout(() => { sidebar.classList.add('visible'); }, 10); // Update content this.updateNotificationDetail(notification); } } /** * Hide left sidebar */ hideLeftSidebar() { const sidebar = document.getElementById('notification-left-sidebar'); if (sidebar) { sidebar.classList.remove('visible'); setTimeout(() => { sidebar.style.display = 'none'; }, 300); } } /** * Hide all sidebars */ hideAllSidebars() { this.hideRightSidebar(); this.hideLeftSidebar(); } /** * Load notifications list */ async loadNotifications(filter = 'all') { try { const listContainer = document.getElementById('notification-list'); if (!listContainer) return; // Show loading message listContainer.innerHTML = '<div class="loading-message">Loading notifications...</div>'; // Get notifications const options = { page: 1, limit: 50, status: filter === 'unread' ? 'unread' : 'all', type: ['system', 'billing', 'technical'].includes(filter) ? filter : 'all', }; const result = await notificationsService.getNotifications(options); this.currentNotifications = result.notifications || []; // Render notifications this.renderNotifications(this.currentNotifications, filter); } catch (error) { console.error( '[NotificationBanner] Failed to load notifications:', error ); const listContainer = document.getElementById('notification-list'); if (listContainer) { listContainer.innerHTML = '<div class="error-message">Failed to load notifications. Please try again.</div>'; } } } /** * Render notifications in the list */ renderNotifications(notifications, _activeFilter = 'all') { const listContainer = document.getElementById('notification-list'); if (!listContainer) return; if (notifications.length === 0) { listContainer.innerHTML = '<div class="empty-message">No notifications found.</div>'; return; } const notificationsHTML = notifications .map(notification => { const isUnread = notification.status === 'unread'; const timeAgo = this.formatTimeAgo(notification.created_at); const preview = this.truncateText( notification.content || notification.message || '', 100 ); return ` <div class="notification-item ${isUnread ? 'unread' : ''}" data-notification-id="${notification.id}"> <div class="notification-header"> <h4 class="notification-title">${this.escapeHtml(notification.title || 'Notification')}</h4> <span class="notification-time">${timeAgo}</span> </div> <p class="notification-preview">${this.escapeHtml(preview)}</p> <div class="notification-meta"> <span class="notification-type">${notification.type || 'general'}</span> <div class="notification-actions-inline"> ${isUnread ? '<button class="action-btn-small mark-read" data-action="mark-read">Mark Read</button>' : ''} <button class="action-btn-small view-detail" data-action="view">View</button> </div> </div> </div> `; }) .join(''); listContainer.innerHTML = notificationsHTML; // Add click listeners to notification items listContainer.addEventListener('click', e => { const notificationItem = e.target.closest('.notification-item'); if (notificationItem) { const notificationId = notificationItem.dataset.notificationId; const notification = notifications.find(n => n.id === notificationId); if (e.target.dataset.action === 'mark-read') { e.stopPropagation(); this.markNotificationAsRead(notificationId); } else if ( e.target.dataset.action === 'view' || !e.target.dataset.action ) { this.showNotificationDetail(notification); } } }); } /** * Apply filter to notifications */ async applyFilter(filter) { // Update active filter button document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.remove('active'); }); document .querySelector(`[data-filter="${filter}"]`) ?.classList.add('active'); // Load filtered notifications await this.loadNotifications(filter); } /** * Show notification detail in left sidebar */ showNotificationDetail(notification) { if (!notification) return; this.currentNotification = notification; this.showLeftSidebar(notification); } /** * Update notification detail content */ updateNotificationDetail(notification) { const contentContainer = document.getElementById( 'notification-detail-content' ); if (!contentContainer || !notification) return; const formattedDate = new Date(notification.created_at).toLocaleString(); contentContainer.innerHTML = ` <h4>${this.escapeHtml(notification.title || 'Notification')}</h4> <div class="detail-meta" style="margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #e0e0e0;"> <p style="margin: 0; font-size: 13px; color: #6c757d;"> <strong>Type:</strong> ${notification.type || 'general'} | <strong>Date:</strong> ${formattedDate} | <strong>Status:</strong> ${notification.status} </p> </div> <div class="detail-content"> ${notification.content || notification.message || 'No content available.'} </div> `; // Update action buttons state const markReadBtn = document.getElementById('mark-read-btn'); if (markReadBtn) { markReadBtn.style.display = notification.status === 'unread' ? 'inline-block' : 'none'; } } /** * Mark notification as read */ async markNotificationAsRead(notificationId) { try { await notificationsService.updateNotificationStatus( notificationId, 'read' ); // Update local state const notification = this.currentNotifications.find( n => n.id === notificationId ); if (notification) { notification.status = 'read'; } // Update current notification if it's the same if (this.currentNotification?.id === notificationId) { this.currentNotification.status = 'read'; this.updateNotificationDetail(this.currentNotification); } // Refresh counts and list await this.loadNotificationCounts(); await this.loadNotifications(); console.log('[NotificationBanner] [SUCCESS] Notification marked as read'); } catch (error) { console.error( '[NotificationBanner] Failed to mark notification as read:', error ); } } /** * Mark current notification as read */ async markCurrentAsRead() { if (this.currentNotification) { await this.markNotificationAsRead(this.currentNotification.id); } } /** * Mark all notifications as read */ async markAllAsRead() { try { await notificationsService.markAllAsRead(); // Update local state this.currentNotifications.forEach(notification => { notification.status = 'read'; }); // Refresh counts and list await this.loadNotificationCounts(); await this.loadNotifications(); console.log( '[NotificationBanner] [SUCCESS] All notifications marked as read' ); } catch (error) { console.error( '[NotificationBanner] Failed to mark all notifications as read:', error ); } } /** * Delete current notification */ async deleteCurrentNotification() { if (!this.currentNotification) return; if (confirm('Are you sure you want to delete this notification?')) { try { await notificationsService.deleteNotification( this.currentNotification.id ); // Remove from local state this.currentNotifications = this.currentNotifications.filter( n => n.id !== this.currentNotification.id ); // Hide detail sidebar this.hideLeftSidebar(); // Refresh counts and list await this.loadNotificationCounts(); await this.loadNotifications(); console.log('[NotificationBanner] [SUCCESS] Notification deleted'); } catch (error) { console.error( '[NotificationBanner] Failed to delete notification:', error ); } } } /** * Start auto-refresh timer */ startAutoRefresh() { if (this.refreshTimer) { clearInterval(this.refreshTimer); } this.refreshTimer = setInterval(async () => { if (this.isVisible) { await this.loadNotificationCounts(); } }, this.options.refreshInterval); } /** * Stop auto-refresh timer */ stopAutoRefresh() { if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null; } } /** * Format time ago string */ formatTimeAgo(dateString) { const date = new Date(dateString); const now = new Date(); const diffInSeconds = Math.floor((now - date) / 1000); if (diffInSeconds < 60) return 'Just now'; if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`; if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`; if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)}d ago`; return date.toLocaleDateString(); } /** * Truncate text to specified length */ truncateText(text, maxLength) { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; } /** * Escape HTML to prevent XSS */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Destroy the notification banner */ destroy() { this.stopAutoRefresh(); // Remove elements const elements = [ 'notification-banner', 'notification-right-sidebar', 'notification-left-sidebar', 'notification-overlay', ]; elements.forEach(id => { const element = document.getElementById(id); if (element) { element.remove(); } }); // Remove styles const styles = document.getElementById('notification-banner-styles'); if (styles) { styles.remove(); } this.initialized = false; console.log('[NotificationBanner] Destroyed'); } } // Export for use export default NotificationBanner;