/** * UI Enhancements Module * * Provides loading states, optimistic updates, and better error handling */ export class UIEnhancements { constructor() { this.init(); } init() { this.setupLoadingStates(); this.setupOptimisticUpdates(); this.setupErrorHandling(); this.setupFormEnhancements(); } /** * Setup loading states for async operations */ setupLoadingStates() { // Add loading state to buttons document.addEventListener('click', (e) => { const button = e.target.closest('button[data-loading]'); if (button && !button.disabled) { const originalText = button.textContent; button.dataset.originalText = originalText; button.disabled = true; button.classList.add('loading'); // Add spinner const spinner = document.createElement('span'); spinner.className = 'button-spinner'; spinner.innerHTML = '⏳'; button.prepend(spinner); button.textContent = button.dataset.loadingText || 'Loading...'; // Auto-reset after 10 seconds (safety) setTimeout(() => { this.resetButton(button); }, 10000); } }); } resetButton(button) { if (button.dataset.originalText) { button.textContent = button.dataset.originalText; delete button.dataset.originalText; } button.disabled = false; button.classList.remove('loading'); const spinner = button.querySelector('.button-spinner'); if (spinner) { spinner.remove(); } } /** * Setup optimistic updates for better UX */ setupOptimisticUpdates() { // Optimistic update helper window.optimisticUpdate = (element, updateFn, rollbackFn) => { const originalState = this.captureState(element); try { updateFn(element); return () => { // Rollback function this.restoreState(element, originalState); if (rollbackFn) rollbackFn(); }; } catch (error) { console.error('Optimistic update failed:', error); return null; } }; // Apply optimistic updates to table rows document.addEventListener('click', (e) => { const row = e.target.closest('tr[data-id]'); if (!row) return; const action = e.target.closest('[data-optimistic]'); if (!action) return; const actionType = action.dataset.optimistic; const rollback = this.optimisticTableUpdate(row, actionType); // Store rollback function action.dataset.rollback = 'true'; action.addEventListener('error', () => { if (rollback) rollback(); }, { once: true }); }); } optimisticTableUpdate(row, actionType) { const originalClass = row.className; const originalOpacity = row.style.opacity; switch (actionType) { case 'delete': row.style.opacity = '0.5'; row.style.textDecoration = 'line-through'; row.classList.add('optimistic-delete'); return () => { row.className = originalClass; row.style.opacity = originalOpacity; row.style.textDecoration = ''; row.classList.remove('optimistic-delete'); }; case 'update': row.classList.add('optimistic-update'); return () => { row.classList.remove('optimistic-update'); }; default: return null; } } captureState(element) { return { className: element.className, style: element.style.cssText, innerHTML: element.innerHTML, }; } restoreState(element, state) { element.className = state.className; element.style.cssText = state.style; element.innerHTML = state.innerHTML; } /** * Setup better error handling */ setupErrorHandling() { // Global error handler window.addEventListener('error', (e) => { this.showErrorNotification('An unexpected error occurred', e.error); }); // Unhandled promise rejection handler window.addEventListener('unhandledrejection', (e) => { this.showErrorNotification('Request failed', e.reason); }); // Enhanced fetch wrapper with error handling window.safeFetch = async (url, options = {}) => { try { const response = await fetch(url, options); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { this.showErrorNotification('Request failed', error.message); throw error; } }; } showErrorNotification(title, message) { // Use existing toast system if available if (window.Toast) { window.Toast.error(`${title}: ${message}`); } else { // Fallback: console error console.error(title, message); // Show simple alert as last resort const errorDiv = document.createElement('div'); errorDiv.className = 'error-notification'; errorDiv.innerHTML = ` ${title}

${message}

`; document.body.appendChild(errorDiv); setTimeout(() => { if (errorDiv.parentNode) { errorDiv.remove(); } }, 5000); } } /** * Setup form enhancements */ setupFormEnhancements() { // Add loading state to forms document.addEventListener('submit', (e) => { const form = e.target; if (form.tagName !== 'FORM') return; const submitButton = form.querySelector('button[type="submit"], input[type="submit"]'); if (submitButton) { submitButton.disabled = true; submitButton.classList.add('loading'); // Add loading text const originalText = submitButton.value || submitButton.textContent; submitButton.dataset.originalText = originalText; submitButton.value = submitButton.value ? 'Submitting...' : submitButton.value; submitButton.textContent = submitButton.textContent ? 'Submitting...' : submitButton.textContent; } // Reset on error form.addEventListener('error', () => { if (submitButton) { submitButton.disabled = false; submitButton.classList.remove('loading'); submitButton.value = submitButton.dataset.originalText || submitButton.value; submitButton.textContent = submitButton.dataset.originalText || submitButton.textContent; } }, { once: true }); }); // Add retry functionality for failed requests window.retryRequest = async (requestFn, maxRetries = 3, delay = 1000) => { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await requestFn(); } catch (error) { lastError = error; if (i < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); } } } throw lastError; }; } } // Auto-initialize document.addEventListener('DOMContentLoaded', () => { new UIEnhancements(); });