Some checks failed
Deploy Application / deploy (push) Has been cancelled
254 lines
8.4 KiB
JavaScript
254 lines
8.4 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<strong>${title}</strong>
|
|
<p>${message}</p>
|
|
<button onclick="this.parentElement.remove()">Close</button>
|
|
`;
|
|
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();
|
|
});
|
|
|