/**
|
* 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(),
|
'X-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 = `
|
<p>${message}</p>
|
<button onclick="window.jvbNotifications.loadNotifications()">
|
Try Again
|
</button>
|
`;
|
|
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(),
|
'X-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(),
|
'X-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;
|