//============================================================================= // #region ACTIONS //============================================================================= const NotificationActionTypes = { ADD_NOTIFICATION: 'ADD_NOTIFICATION', REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION', CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS', UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION' }; const notificationActions = { addNotification: (notification) => ({ type: NotificationActionTypes.ADD_NOTIFICATION, payload: { id: Date.now() + Math.random(), timestamp: Date.now(), type: 'info', title: 'Notification', message: 'Default message', duration: 0, status: 'showing', ...notification } }), removeNotification: (id) => ({ type: NotificationActionTypes.REMOVE_NOTIFICATION, payload: { id } }), clearNotifications: () => ({ type: NotificationActionTypes.CLEAR_NOTIFICATIONS }), updateNotification: (id, updates) => ({ type: NotificationActionTypes.UPDATE_NOTIFICATION, payload: { id, updates } }) }; //============================================================================= // #region REDUCER //============================================================================= const notificationInitialState = { notifications: [], maxNotifications: 5 }; function notificationReducer(state = notificationInitialState, action = {}) { switch (action.type) { case NotificationActionTypes.ADD_NOTIFICATION: { if (!action.payload) return state; let newNotifications = [...state.notifications]; if (newNotifications.length >= state.maxNotifications) { newNotifications = newNotifications.slice(1); } return { ...state, notifications: [...newNotifications, action.payload] }; } case NotificationActionTypes.REMOVE_NOTIFICATION: { if (!action.payload || !action.payload.id) return state; return { ...state, notifications: state.notifications.filter(n => n.id !== action.payload.id) }; } case NotificationActionTypes.CLEAR_NOTIFICATIONS: return { ...state, notifications: [] }; case NotificationActionTypes.UPDATE_NOTIFICATION: { if (!action.payload || !action.payload.id || !action.payload.updates) return state; return { ...state, notifications: state.notifications.map(n => n.id === action.payload.id ? { ...n, ...action.payload.updates } : n ) }; } default: return state; } } //============================================================================= // #region STORE //============================================================================= class Store { constructor(reducer, initialState) { this.reducer = reducer; this.state = initialState; this.listeners = []; } getState() { return this.state; } dispatch(action) { this.state = this.reducer(this.state, action); this.listeners.forEach(listener => listener(this.state)); return action; } subscribe(listener) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter(l => l !== listener); }; } } const notificationStore = new Store(notificationReducer, notificationInitialState); //============================================================================= // #region SELECTORS //============================================================================= const notificationSelectors = { getNotifications: (state) => state.notifications, getMaxNotifications: (state) => state.maxNotifications }; //============================================================================= // #region UI COMPONENT //============================================================================= class NotificationUI { constructor(store) { this.store = store; this.unsubscribe = null; this.container = document.getElementById('notification-container'); this.renderedNotifications = new Map(); } init() { if (!this.container) { console.error('Notification container not found'); return; } this.unsubscribe = this.store.subscribe((state) => this.render(state)); this.render(this.store.getState()); } destroy() { if (this.unsubscribe) this.unsubscribe(); this.renderedNotifications.forEach(el => { if (el.parentNode) el.parentNode.removeChild(el); }); this.renderedNotifications.clear(); } render(state) { const notifications = notificationSelectors.getNotifications(state); // Remove notifications no longer present const currentIds = new Set(notifications.map(n => n.id)); for (const [id, el] of this.renderedNotifications.entries()) { if (!currentIds.has(id)) { if (el.parentNode) el.parentNode.removeChild(el); this.renderedNotifications.delete(id); } } // Add or update notifications notifications.forEach(notification => { if (!notification || !notification.id) return; if (!this.renderedNotifications.has(notification.id)) { this.createNotificationElement(notification); } else { this.updateNotificationElement(notification); } }); } createNotificationElement(notification) { const el = document.createElement('div'); el.className = `notification ${notification.type || 'info'}`; el.dataset.id = notification.id; el.innerHTML = `