add api headermenu and crud management

This commit is contained in:
2026-02-05 00:01:58 +07:00
parent befe6b30aa
commit f25f6b9156
29 changed files with 2058 additions and 634 deletions

31
public/js/custom-modal.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Custom Modal Utilities
*/
// Global function to clean up any stuck modal backdrops
window.forceCleanupModals = function() {
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
const backdrops = document.querySelectorAll('.modal-backdrop, .overlay, .loading');
if (backdrops.length > 0) {
console.warn('Cleanup: Removing backdrops:', backdrops.length);
backdrops.forEach(el => el.remove());
}
};
// Auto-cleanup on hide
document.addEventListener('hidden.bs.modal', () => {
setTimeout(forceCleanupModals, 100);
});
// Watchdog
setInterval(() => {
if (document.querySelectorAll('.modal.show').length === 0) {
if (document.querySelectorAll('.modal-backdrop').length > 0) {
forceCleanupModals();
}
}
}, 2000);
window.addEventListener('load', forceCleanupModals);

View File

@@ -0,0 +1,27 @@
/**
* Flash Message Handler
* Displays server-side flash messages using showToast
*/
document.addEventListener('DOMContentLoaded', () => {
const flashDataEl = document.getElementById('flash-messages-data');
if (flashDataEl) {
try {
const messages = JSON.parse(flashDataEl.textContent);
if (messages.success_msg) {
showToast('Success', messages.success_msg, 'success');
}
if (messages.error_msg) {
showToast('Error', messages.error_msg, 'error');
}
if (messages.error) {
showToast('Error', messages.error, 'error');
}
} catch (e) {
console.error('Error parsing flash messages data:', e);
}
}
});

56
public/js/main.js Normal file
View File

@@ -0,0 +1,56 @@
/**
* Global Admin Scripts
*/
console.log('CMS Admin Main JS loaded');
// Helper to handle AJAX form submissions with confirmation or loading state
window.handleFormAjax = async (formEl, options = {}) => {
const {
confirmMessage = null,
onSuccess = null,
onError = null,
loaderBtn = null
} = options;
if (confirmMessage && !confirm(confirmMessage)) {
return false;
}
const formData = new FormData(formEl);
const data = Object.fromEntries(formData.entries());
const originalBtnHtml = loaderBtn ? loaderBtn.innerHTML : null;
try {
if (loaderBtn) {
loaderBtn.disabled = true;
loaderBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Processing...';
}
const response = await fetch(formEl.action, {
method: formEl.method || 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
if (onSuccess) onSuccess(result);
else showToast('Success', result.message || 'Operation successful', 'success');
} else {
if (onError) onError(result);
else showToast('Error', result.message || 'Operation failed', 'error');
}
} catch (error) {
console.error('AJAX Error:', error);
showToast('Error', 'A network error occurred', 'error');
} finally {
if (loaderBtn) {
loaderBtn.disabled = false;
loaderBtn.innerHTML = originalBtnHtml;
}
}
};

74
public/js/toast.js Normal file
View File

@@ -0,0 +1,74 @@
/**
* Toast Notification System
* Requires Bootstrap 5 JS
*/
function showToast(title, message, type = 'info') {
// Create container if not exists
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '1100';
document.body.appendChild(toastContainer);
}
// Inject CSS for transition if not already present
if (!document.getElementById('toast-animation-styles')) {
const style = document.createElement('style');
style.id = 'toast-animation-styles';
style.innerHTML = `
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast-slide-in {
animation: slideInRight 0.3s ease-out forwards;
}
`;
document.head.appendChild(style);
}
// Map type to Bootstrap color class
const bgClass = type === 'success' ? 'bg-success' : (type === 'error' || type === 'danger' ? 'bg-danger' : 'bg-info');
// Create toast element
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-white ${bgClass} border-0 mb-2 toast-slide-in`;
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<div class="d-flex align-items-center">
<i class="fas ${type === 'success' ? 'fa-check-circle' : (type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle')} me-2"></i>
<div>
<strong class="me-auto">${title}</strong><br>
${message}
</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toastEl);
// Initialize and show
const bsToast = new bootstrap.Toast(toastEl, { autohide: true, delay: 5000 });
bsToast.show();
// Remove from DOM when hidden
toastEl.addEventListener('hidden.bs.toast', () => {
toastEl.remove();
});
}
// Map showNotification to showToast for backward compatibility
window.showNotification = function(message, type) {
const title = type === 'success' ? 'Success' : (type === 'error' ? 'Error' : 'Notification');
showToast(title, message, type);
};