From 60a5cbb57660a6eb95d736992acc11e0cf7caff2 Mon Sep 17 00:00:00 2001 From: VinnyNC Date: Mon, 29 Sep 2025 23:24:56 -0400 Subject: [PATCH] Complete 4.3.1: Notification System Redesign - Enhanced toast notification system with variants, queue management, and accessibility improvements --- UI_UPDATE.MD | 17 +- assets/js/notifications.js | 420 +++++++++++++++++++++++++++++++++++++ index.php | 8 +- static/css/components.css | 308 ++++++++++++++++++++++++++- 4 files changed, 746 insertions(+), 7 deletions(-) create mode 100644 assets/js/notifications.js diff --git a/UI_UPDATE.MD b/UI_UPDATE.MD index efb55ca..57af954 100644 --- a/UI_UPDATE.MD +++ b/UI_UPDATE.MD @@ -480,10 +480,19 @@ All utilities follow the established pattern of using CSS custom properties from ### Sub-task 4.3: Enhanced User Feedback Systems -#### 4.3.1: Notification Redesign -- [ ] Redesign toast notification system -- [ ] Implement notification variants -- [ ] Add notification queue management +#### 4.3.1: Notification Redesign - COMPLETED 9/29/2025 +- [x] Redesign toast notification system +- [x] Implement notification variants +- [x] Add notification queue management + +**Notes:** Comprehensive notification system redesign completed with: +- **Enhanced Notification System**: Replaced simple toast with advanced notification system supporting multiple variants (success, error, warning, info), custom actions, progress bars, and smooth animations +- **Queue Management**: Implemented intelligent queue system that handles up to 5 simultaneous notifications with automatic stacking and dismissal +- **Variants and Styling**: Four notification variants with distinct colors, icons, and animations (.notification--success, .notification--error, .notification--warning, .notification--info) +- **Advanced Features**: Hover-to-pause, keyboard navigation (Escape key), ARIA accessibility, progress indicators for auto-dismissing notifications, and smooth slide-in/out animations +- **Legacy Support**: Maintains backward compatibility with existing showToast() calls while redirecting to the new system +- **Mobile Responsive**: Full responsive design with mobile-specific positioning and touch-friendly interactions +- **Api Integration**: New Notifications API (show(), success(), error(), warning(), info()) with configuration options #### 4.3.2: Loading States - [ ] Implement loading indicators diff --git a/assets/js/notifications.js b/assets/js/notifications.js new file mode 100644 index 0000000..da1dae4 --- /dev/null +++ b/assets/js/notifications.js @@ -0,0 +1,420 @@ +// Notification System Module +// Advanced notifications with variants, queue management, and accessibility +(function() { + 'use strict'; + + AppModules.require('ui-controls'); + AppModules.register('notifications'); + + // Notification configuration + const NotificationConfig = { + maxNotifications: 5, // Maximum notifications shown simultaneously + defaultDuration: 4000, // Default auto-dismiss duration (ms) + duration: { + success: 3000, + error: 7000, + warning: 5000, + info: 4000 + }, + position: 'top-right', // top-right, top-left, bottom-right, bottom-left, center + positionClasses: { + 'top-right': '', + 'top-left': 'notifications-container--left', + 'bottom-right': 'notifications-container--bottom', + 'bottom-left': 'notifications-container--bottom notifications-container--left', + 'center': 'notifications-container--center' + } + }; + + // Notification queue and state + let notificationQueue = []; + let activeNotifications = []; + let notificationCounter = 0; + + // DOM elements + let notificationsContainer = null; + + // Notification class + class Notification { + constructor(options = {}) { + this.id = `notification-${++notificationCounter}`; + this.title = options.title || ''; + this.message = options.message || ''; + this.type = options.type || 'info'; // success, error, warning, info, default + this.duration = options.duration === 0 ? 0 : (options.duration || NotificationConfig.duration[this.type] || NotificationConfig.defaultDuration); + this.actions = options.actions || []; + this.position = options.position || NotificationConfig.position; + this.persistent = options.persistent || false; + this.icon = options.icon !== undefined ? options.icon : true; + this.timestamp = Date.now(); + this.element = null; + } + + createElement() { + const notification = document.createElement('div'); + notification.id = this.id; + notification.className = `notification notification--${this.type}`; + notification.setAttribute('role', 'alert'); + notification.setAttribute('aria-live', 'assertive'); + notification.setAttribute('aria-atomic', 'true'); + + let html = ''; + + // Icon + if (this.icon) { + html += ''; + } + + // Content + html += '
'; + if (this.title) { + html += `
${escapeHtml(this.title)}
`; + } + html += `
${escapeHtml(this.message)}
`; + html += '
'; + + // Actions + if (this.actions.length > 0) { + html += '
'; + this.actions.forEach(action => { + const actionId = `action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}`; + html += ``; + }); + html += '
'; + } + + // Progress bar for auto-dismissing notifications + if (this.duration > 0) { + html += '
'; + } + + notification.innerHTML = html; + + // Add close button if no custom actions + if (this.actions.length === 0) { + const closeBtn = document.createElement('button'); + closeBtn.className = 'notification__action'; + closeBtn.setAttribute('aria-label', 'Close notification'); + closeBtn.innerHTML = '×'; + closeBtn.addEventListener('click', () => this.dismiss()); + notification.appendChild(closeBtn); + } + + // Set progress bar animation duration + if (this.duration > 0) { + const progressBar = notification.querySelector('.notification__progress-bar'); + if (progressBar) { + progressBar.style.animationDuration = `${this.duration}ms`; + } + } + + // Bind action handlers + this.actions.forEach((action, index) => { + const actionBtn = notification.querySelector(`[data-action-id="action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}"]`); + if (actionBtn && action.handler) { + actionBtn.addEventListener('click', () => { + action.handler(this); + }); + } + }); + + this.element = notification; + return notification; + } + + show() { + if (!notificationsContainer) return; + + // Create element if not exists + if (!this.element) { + this.createElement(); + } + + // Add position class + const positionClass = NotificationConfig.positionClasses[this.position] || ''; + notificationsContainer.className = `notifications-container ${positionClass}`; + + // Add to container + notificationsContainer.appendChild(this.element); + + // Set up auto-dismiss if duration is set + if (this.duration > 0) { + this.timeoutId = setTimeout(() => { + this.dismiss(); + }, this.duration); + } + + // Pause timeout on hover + this.element.addEventListener('mouseenter', () => { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + }); + + this.element.addEventListener('mouseleave', () => { + if (this.duration > 0 && !this.dismissed) { + this.timeoutId = setTimeout(() => { + this.dismiss(); + }, this.duration); + } + }); + + return this; + } + + dismiss() { + if (this.dismissed) return; + this.dismissed = true; + + // Clear timeout + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + + // Add exit animation + if (this.element) { + this.element.classList.add('exiting'); + + // Remove after animation + setTimeout(() => { + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + removeFromActive(this); + processQueue(); + }, 300); + } + } + + update(options = {}) { + if (options.message !== undefined) { + this.message = options.message; + const messageEl = this.element.querySelector('.notification__message'); + if (messageEl) { + messageEl.textContent = this.message; + } + } + + if (options.title !== undefined) { + this.title = options.title; + const titleEl = this.element.querySelector('.notification__title'); + if (titleEl) { + titleEl.textContent = this.title; + } + } + } + } + + // Queue management functions + function addToQueue(notification) { + notificationQueue.push(notification); + + // Process queue if not at max capacity + if (activeNotifications.length < NotificationConfig.maxNotifications) { + processQueue(); + } + } + + function removeFromActive(notification) { + const index = activeNotifications.indexOf(notification); + if (index > -1) { + activeNotifications.splice(index, 1); + } + } + + function processQueue() { + // Remove any notifications that have been dismissed + activeNotifications = activeNotifications.filter(n => !n.dismissed); + + // Show queued notifications if space available + while (activeNotifications.length < NotificationConfig.maxNotifications && notificationQueue.length > 0) { + const notification = notificationQueue.shift(); + activeNotifications.push(notification); + notification.show(); + } + } + + // Public API functions + function show(options = {}) { + if (typeof options === 'string') { + options = { message: options }; + } + + const notification = new Notification(options); + addToQueue(notification); + return notification; + } + + function success(message, options = {}) { + return show(Object.assign({ + message, + type: 'success' + }, options)); + } + + function error(message, options = {}) { + return show(Object.assign({ + message, + type: 'error' + }, options)); + } + + function warning(message, options = {}) { + return show(Object.assign({ + message, + type: 'warning' + }, options)); + } + + function info(message, options = {}) { + return show(Object.assign({ + message, + type: 'info' + }, options)); + } + + function dismissAll() { + // Clear queue + notificationQueue = []; + + // Dismiss all active notifications + activeNotifications.forEach(notification => { + notification.dismiss(); + }); + activeNotifications = []; + } + + function clearQueue() { + notificationQueue = []; + } + + function setConfig(newConfig = {}) { + Object.assign(NotificationConfig, newConfig); + + // Update container class if position changed + if (notificationsContainer && newConfig.position) { + const positionClass = NotificationConfig.positionClasses[newConfig.position] || ''; + notificationsContainer.className = `notifications-container ${positionClass}`; + } + } + + // Utility functions + function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return (text || '').replace(/[&<>"']/g, m => map[m]); + } + + // Initialize the notification system + function initialize() { + // Get container element + notificationsContainer = document.getElementById('notificationsContainer'); + + if (!notificationsContainer) { + console.warn('Notification container element not found. Notifications will not be displayed.'); + return; + } + + // Set initial position class + const positionClass = NotificationConfig.positionClasses[NotificationConfig.position] || ''; + notificationsContainer.className = `notifications-container ${positionClass}`; + + // Add keyboard navigation support + document.addEventListener('keydown', handleKeyboardNavigation); + + // Add container click delegation + notificationsContainer.addEventListener('click', handleContainerClick); + + AppLogger.log('Notification system initialized'); + } + + // Keyboard navigation handler + function handleKeyboardNavigation(event) { + // Close notification on Escape + if (event.key === 'Escape') { + const activeNotification = activeNotifications[activeNotifications.length - 1]; + if (activeNotification) { + activeNotification.dismiss(); + event.preventDefault(); + } + } + } + + // Container click handler for delegation + function handleContainerClick(event) { + // Handle close button clicks + if (event.target.classList.contains('notification__action') && + event.target.textContent === '×') { + const notificationEl = event.target.closest('.notification'); + if (notificationEl) { + const notificationId = notificationEl.id; + const notification = activeNotifications.find(n => n.id === notificationId); + if (notification) { + notification.dismiss(); + } + } + } + } + + // Legacy toast support - redirect to new system + function legacyShowToast(message, duration) { + const toastElement = document.getElementById('toast'); + if (toastElement) { + toastElement.style.display = 'none'; + } + + // Show using new system instead + show({ + message, + duration: duration === 0 ? 0 : duration, + type: 'info', + position: 'bottom' + }); + } + + function legacyHideToast() { + // Legacy function - kept for compatibility, but new system is auto-managing + AppLogger.warn('hideToast() called - this is deprecated. Notifications auto-manage themselves now.'); + } + + // Update ui-controls module to use new notification system + function updateUIControls() { + if (window.UIControls && window.UIControls.showToast) { + window.UIControls.showToast = legacyShowToast; + window.UIControls.hideToast = legacyHideToast; + } + } + + // Public API + window.Notifications = { + show: show, + success: success, + error: error, + warning: warning, + info: info, + dismissAll: dismissAll, + clearQueue: clearQueue, + setConfig: setConfig, + config: NotificationConfig + }; + + // Initialize on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialize); + } else { + initialize(); + } + + // Update UI controls after a short delay to ensure they load + setTimeout(updateUIControls, 100); + + AppLogger.log('Notification system module loaded'); + +})(); diff --git a/index.php b/index.php index df0c5f8..2a2da7b 100644 --- a/index.php +++ b/index.php @@ -495,8 +495,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { - -
+ +
+ + +