fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
253
resources/js/modules/admin/ui-enhancements.js
Normal file
253
resources/js/modules/admin/ui-enhancements.js
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user