forked from UKSOURCE/cms.hailearning.edu.vn
add api headermenu and crud management
This commit is contained in:
BIN
public/images/Logo_round.jpg
Normal file
BIN
public/images/Logo_round.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
BIN
public/img/Logo_round.jpg
Normal file
BIN
public/img/Logo_round.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
31
public/js/custom-modal.js
Normal file
31
public/js/custom-modal.js
Normal 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);
|
||||
27
public/js/flash-handler.js
Normal file
27
public/js/flash-handler.js
Normal 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
56
public/js/main.js
Normal 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
74
public/js/toast.js
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user