/** * For on-screen notifications */ class NotificationManager { constructor(options = {}) { // Core configuration this.popupQueue = []; this.isLoading = false; this.cache = window.jvbCache; this.isProcessingQueue = false; this.options = { maxVisibleNotifications: 5, displayDuration: { high: 7000, // High priority stays longer medium: 5000, // Default duration low: 3000 // Low priority fades faster }, position: 'bottom-right', pollingInterval: 60000, ...options }; // Initialize main elements this.button = document.querySelector('.toggle.notifications'); this.submenu = document.querySelector('.notifications-preview'); this.toasts = document.querySelector('.toasts'); this.notificationsLoaded = false; this.pollTimer = null; this.lastCheck = null; if (this.button && this.submenu) { this.init(); } this.clickListeners = this.checkClicks.bind(this); this.updateListeners(); } init() { // Handle mark as read clicks in preview this.submenu.addEventListener('click', e => { const readBtn = e.target.closest('.mark-read'); if (readBtn) { const item = readBtn.closest('.notification-preview'); if (item) { this.markAsRead(item.dataset.id); } } }); // Load initial notifications this.loadNotifications(); // Start polling this.initializePolling(); } checkClicks(e){ if(e.target.closest('.close-toast')){ let toast = e.target.closest('.toast'); toast.classList.add('hiding'); setTimeout(()=>{ toast.remove(); this.updateListeners(); }, 300); } } updateListeners(){ this.toasts.addEventListener('click', this.clickListeners); } toggleDropdown() { if (!this.notificationsLoaded) { this.loadNotifications(); } } async loadNotifications(force = false) { if(this.isLoading) return; try { this.isLoading = true; const params = new URLSearchParams({ user: window.auth.getUser(), status: 'unread', limit: 5, }); const data = await this.cache.fetchWithCache( `${jvbSettings.api}notifications?${params.toString()}`, { method: 'GET', headers: { 'X-WP-Nonce': window.auth.getNonce(), 'action_nonce': window.auth.getNonce('notifications') } }, { context: 'notifications', forceRefresh: true, } ); this.renderPreviewNotifications(data.notifications); this.updateUnreadCount(data.total); this.notificationsLoaded = true; this.lastCheck = new Date().toUTCString(); } catch (error) { console.error('Error loading notifications:', error); // Show error state in UI this.renderErrorState(error.message); } } // Add method to show error state renderErrorState(message) { const viewAllItem = this.submenu.querySelector('#view-all'); this.submenu.querySelectorAll('li:not(#view-all)').forEach(item => item.remove()); const errorItem = document.createElement('li'); errorItem.className = 'error-state'; errorItem.innerHTML = `
${message}
`; this.submenu.insertBefore(errorItem, viewAllItem); } renderPreviewNotifications(notifications) { // Find the View All link so we can insert before it const viewAllItem = this.submenu.querySelector('#view-all'); // Remove existing notification items this.submenu.querySelectorAll('li:not(#view-all)').forEach(item => item.remove()); // Add new notifications notifications.forEach(notification => { let item = window.getTemplate('notificationItem'); item.classList.add(notification.status, `priority-${notification.priority}`); item.dataset.id = notification.id; item.prepend(getIcon(notification.icon)); let message =item.querySelector('p'); let time = item.querySelector('time'); [message.textContent, time.datetime, time.textContent] = [notification.message, new Date(notification.created_at).toISOString(), formatTimeAgo(notification.created_at)]; let actions = window.getTemplate('notificationActions'); let markRead = actions.querySelector('button'); if(notification.actions.length > 0){ notification.actions.forEach(action => { let a = markRead.cloneNode(true); if(action.primary){ a.classList.add('primary'); } [a.dataset.id, a.dataset.action, a.textContent] = [notification.id, action.label.toLowerCase(), action.label]; actions.append(a); }); markRead.remove(); } item.append(actions); this.submenu.prepend(item); }); // Add empty state if no notifications if (notifications.length === 0) { this.submenu.prepend(window.getTemplate('emptyNotification')); } } queuePopupNotification(notification) { this.popupQueue.push(notification); this.processPopupQueue(); } async processPopupQueue() { if (this.isProcessingQueue || this.popupQueue.length === 0) return; this.isProcessingQueue = true; while (this.popupQueue.length > 0) { const notification = this.popupQueue.shift(); await this.showToast(notification.message, notification.type, notification.actions); // Small delay between notifications if (this.popupQueue.length > 0) { await new Promise(resolve => setTimeout(resolve, 300)); } } this.isProcessingQueue = false; } /** * Show toast notification * @param {string} message - Message to show * @param {string} type - Notification type (success, error, info) * @param {object} actions - Notification type (success, error, info) */ showToast(message, type = 'success', actions = {}) { // Check if toast container exists let toast = window.getTemplate('notificationPopup'); toast.classList.add(type); let m = toast.querySelector('p'); m.textContent = message; if(Object.entries(actions).length >0){ let buttons = window.getTemplate('notificationActions'); let markRead = actions.querySelector('button'); notification.actions.forEach(action => { let a = markRead.cloneNode(true); if(action.primary){ a.classList.add('primary'); } [a.dataset.action, a.textContent] = [action.label.toLowerCase(), action.label]; buttons.prepend(a); }); } this.toasts.append(toast); // Animate in setTimeout(() => { toast.classList.add('show'); }, 10); // Auto remove after delay setTimeout(() => { toast.classList.add('hiding'); setTimeout(() => { toast.remove(); }, 300); }, 3000); } createNotificationElement(notification) { this.showToast(notification.message); this.renderPreviewNotifications([notification]); } removePopupNotification(element) { element.classList.remove('show'); setTimeout(() => { element.remove(); }, 300); } updateUnreadCount(count) { let span = this.button.querySelector('span'); this.button.classList.remove('has'); [span.textContent, span.ariaLabel] = ['', 'Notifications']; // Handle undefined, null, or invalid count if (!count || isNaN(count)) { // Reset to just the bell icon return; } // Convert to number to be safe count = parseInt(count, 10); if (count > 0) { this.button.classList.add('has'); [span.textContent, span.ariaLabel] = [count, count+` unread notification${(count>1)?'s' : ''}`]; } } async markAsRead(notificationId) { try { const notificationElement = this.submenu.querySelector(`[data-id="${notificationId}"]`); if (!notificationElement) return; notificationElement.classList.add('slide-out'); const response = await fetch( `${jvbSettings.api}notifications`, { method: 'POST', headers: { 'X-WP-Nonce': window.auth.getNonce(), 'action_nonce': window.auth.getNonce('dash'), }, body: { notification: notificationId, user: window.auth.getUser(), } } ); if (!response.ok) throw new Error(notificationSettings.strings.error); const data = await response.json(); if (data.success) { setTimeout(() => { notificationElement.remove(); const remainingNotifications = this.submenu.querySelectorAll('.notification-preview').length; if (remainingNotifications === 0) { const viewAllItem = this.submenu.querySelector('#view-all'); const emptyState = document.createElement('li'); emptyState.className = 'empty-state fade-in'; emptyState.textContent = notificationSettings.strings.noNotifications; this.submenu.insertBefore(emptyState, viewAllItem); requestAnimationFrame(() => { emptyState.classList.remove('fade-in'); }); } }, 300); this.updateUnreadCount(data.total); } } catch (error) { console.error('Error marking notification as read:', error); } } initializePolling() { // Start polling for new notifications this.pollTimer = setInterval(() => { this.checkNotifications(); }, this.options.pollingInterval); // Handle visibility changes document.addEventListener('visibilitychange', () => { if (!document.hidden) { this.checkNotifications(); } }); } async checkNotifications() { try { const params = new URLSearchParams({ user: window.auth.getUser(), status: 'unread', }); const response = await fetch(`${jvbSettings.api}notifications?${params.toString()}`, { headers: { 'X-WP-Nonce': window.auth.getNonce(), 'action_nonce': window.auth.getNonce('dash'), 'If-Modified-Since': this.lastCheck, } }); if (!response.ok) return; const data = await response.json(); if (data.has_new) { await this.loadNotifications(true); } } catch (error) { console.error('Check notifications error:', error); } } destroy() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', async function(){ window.auth.subscribe((event) => { if (event === 'auth-loaded') { window.jvbNotifications = new NotificationManager({ position: 'bottom-right', maxVisibleNotifications: 5, displayDuration: 5000 }); } }); }); function handleNotificationAction(button) { const action = button.dataset.action; // Handle other action types as needed switch (action) { case 'approve': case 'reject': // Handle artist approval actions break; } } function addNotification(message, type = 'info') { window.jvbNotifications.showToast( message, type ); } window.addNotification = addNotification;