Files
2026-04-22 15:22:22 +07:00

691 lines
27 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark)">
Header Management
</h1>
<p class="text-muted mb-0">Edit header content and menu structure</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="content-with-fixed-buttons">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="logo" id="logoInput" />
<input type="hidden" name="activeTab" id="activeTabInput" value="logo" />
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a
class="nav-link <%= activeTab === 'logo' ? 'active' : '' %>"
data-bs-toggle="tab"
href="#logo"
role="tab"
>
<i class="fas fa-image me-2"></i>Logo
</a>
</li>
<li class="nav-item">
<a
class="nav-link <%= activeTab === 'buttons' ? 'active' : '' %>"
data-bs-toggle="tab"
href="#buttons"
role="tab"
>
<i class="fas fa-mouse-pointer me-2"></i>Buttons
</a>
</li>
<li class="nav-item">
<a
class="nav-link <%= activeTab === 'menu' ? 'active' : '' %>"
data-bs-toggle="tab"
href="#menu"
role="tab"
>
<i class="fas fa-sitemap me-2"></i>Menu Structure
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Logo Tab -->
<div class="tab-pane fade <%= activeTab === 'logo' ? 'show active' : '' %>" id="logo" role="tabpanel">
<div class="row g-4">
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-image me-2"></i>Logo Configuration
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Logo Image</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="logoImage"
value="<%= data.logo %>"
placeholder="/path/to/logo.png"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="logoImage"
data-image-type="header"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 200x60px</small>
</div>
<div class="col-md-6" id="logoPreviewContainer">
<% if (data.logo) { %>
<img
src="<%= data.logo %>"
class="img-thumbnail"
style="max-height: 100px; max-width: 300px; object-fit: contain; background: #b8b76a;"
alt="Logo preview"
/>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Buttons Tab -->
<div class="tab-pane fade <%= activeTab === 'buttons' ? 'show active' : '' %>" id="buttons" role="tabpanel">
<div class="row g-4">
<!-- Sign In Button -->
<div class="col-md-6">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-sign-in-alt me-2"></i>Sign In Button</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="signInLabel"
value="<%= data.signInButton.label %>" placeholder="Sign In" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" id="signInHref"
value="<%= data.signInButton.href %>" placeholder="/signin" />
</div>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-6">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-mouse-pointer me-2"></i>CTA Button</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="ctaLabel"
value="<%= data.ctaButton.label %>" placeholder="Request Info" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" id="ctaHref"
value="<%= data.ctaButton.href %>" placeholder="/request" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Menu Structure Tab -->
<div class="tab-pane fade <%= activeTab === 'menu' ? 'show active' : '' %>" id="menu" role="tabpanel">
<%- include('menu') %>
</div>
</div>
</div>
<!-- Fixed actions for ALL tabs -->
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
<button type="button" class="btn btn-outline-secondary px-4" id="headerResetBtn">
<i class="fas fa-undo me-1"></i>Reset
</button>
<button type="button" id="saveHeaderBtn" class="btn btn-outline-primary px-4">
<i class="fas fa-save me-1"></i>Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Safely remove any lingering modal backdrops on page load/navigation
function cleanupModals() {
// Basic reset
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
document.body.style.pointerEvents = 'auto';
// Remove all backdrop/overlay elements
const selector = '.modal-backdrop, .overlay, .loading';
document.querySelectorAll(selector).forEach(el => {
try {
el.remove();
} catch (err) {
console.warn('Error removing overlay:', err);
}
});
// Cleanup dynamically created modals that are not shown
document.querySelectorAll('.modal.fade:not(#modalAddMenu)').forEach(m => {
if (!m.classList.contains('show')) {
m.remove();
}
});
console.log('DOM Cleaned Up: Backdrops removed, body interaction restored.');
}
// Ensure all modals are in body root (prevents stacking context issues)
function relocateModals() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
if (modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
});
}
window.cleanupModals = cleanupModals;
// Initial cleanup and relocation
relocateModals();
cleanupModals();
const urlParams = new URLSearchParams(window.location.search);
const activeTabObj = urlParams.get('activeTab') || urlParams.get('tab');
if (activeTabObj) {
const tabTrigger = document.querySelector(`a[href="#${activeTabObj}"]`);
if (tabTrigger) {
new bootstrap.Tab(tabTrigger).show();
document.getElementById('activeTabInput').value = activeTabObj;
}
}
// Listen for tab changes
document.querySelectorAll('a[data-bs-toggle="tab"]').forEach(tab => {
tab.addEventListener('shown.bs.tab', function (event) {
const targetId = event.target.getAttribute('href').substring(1);
document.getElementById('activeTabInput').value = targetId;
// Update URL without reload to preserve tab state
const url = new URL(window.location);
url.searchParams.set('tab', targetId);
window.history.replaceState({}, '', url);
// Only load Menu Tree if clicking on the menu tab
if (targetId === 'menu') {
loadMenuTree();
}
});
});
// Detect changes to highlight Save button
function markChanged() {
const saveBtn = document.getElementById('saveHeaderBtn');
if (saveBtn) {
saveBtn.classList.remove('btn-outline-primary');
saveBtn.classList.add('btn-primary');
}
}
// Attach listeners to all inputs for change detection
const headerInputs = document.querySelectorAll('#logo input, #buttons input');
headerInputs.forEach(input => {
input.addEventListener('input', markChanged);
input.addEventListener('change', markChanged);
});
// Reset button logic
const headerResetBtn = document.getElementById('headerResetBtn');
if (headerResetBtn) {
headerResetBtn.addEventListener('click', function() {
if (confirm('Are you sure you want to discard all unsaved changes and reset to current saved data?')) {
window.location.reload(); // Simplest and most reliable reset
}
});
}
// Exposed markChanged for other components (like menu)
window.markHeaderChanged = markChanged;
const refreshMenuTreeBtn = document.getElementById('refreshMenuTree');
if (refreshMenuTreeBtn) {
refreshMenuTreeBtn.addEventListener('click', function () {
loadMenuTree();
});
}
// saveMenuChanges has its own onclick in menu.ejs, no need for redundant listener here
// But we'll keep a log to see if it's called
console.log('=== TRACE: Global click listeners initialized ===');
// Cleanup modals when any modal is hidden
document.addEventListener('hidden.bs.modal', function() {
cleanupModals();
});
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
const saveHeaderBtn = document.getElementById('saveHeaderBtn');
if (saveHeaderBtn) {
saveHeaderBtn.addEventListener('click', async function (e) {
console.log('=== TRACE: saveHeaderBtn Clicked (Unified) ===');
const submitBtn = this;
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving everything...';
submitBtn.disabled = true;
try {
// 1. Collect and Save Header Data (Logo + Buttons)
const headerData = {
logo: document.getElementById('logoImage').value,
signInButton: {
label: document.getElementById('signInLabel').value,
href: document.getElementById('signInHref').value,
},
ctaButton: {
label: document.getElementById('ctaLabel').value,
href: document.getElementById('ctaHref').value,
},
};
const headerResponse = await fetch('/admin/header/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(headerData)
});
const headerResult = await headerResponse.json();
// 2. Save Menu Structure if we have the management script loaded
let menuResult = { success: true };
if (typeof window.saveMenuChanges === 'function') {
menuResult = await window.saveMenuChanges(false); // Save without redundant notification
}
if (headerResult.success && menuResult.success) {
showNotification('All changes saved successfully', 'success');
submitBtn.classList.remove('btn-primary');
submitBtn.classList.add('btn-outline-primary');
// Reload to refresh data, preserve current tab
const currentTab = document.getElementById('activeTabInput').value;
setTimeout(() => {
window.location.href = window.location.pathname + '?tab=' + currentTab;
}, 1000);
} else {
const errorMsg = (!headerResult.success ? headerResult.message : '') || (!menuResult.success ? menuResult.message : '') || 'Unable to save some changes';
showNotification('Error: ' + errorMsg, 'error');
}
} catch (error) {
console.error('=== TRACE: Unified Save ERROR ===', error);
showNotification('Error: ' + error.message, 'error');
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
});
}
/**
* Show toast notification at top of page
* Auto-hides after 3 seconds
*/
function showNotification(message, type = 'info') {
// Create toast container if it doesn't exist
let toastContainer = document.getElementById('toastContainer');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toastContainer';
toastContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
`;
document.body.appendChild(toastContainer);
}
// Create toast element
const toast = document.createElement('div');
const bgColor = type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#17a2b8';
const icon = type === 'success' ? '✓' : type === 'error' ? '✕' : '';
toast.style.cssText = `
background-color: ${bgColor};
color: white;
padding: 12px 16px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
animation: slideIn 0.3s ease-out;
pointer-events: auto;
cursor: pointer;
`;
toast.innerHTML = `
<span style="font-weight: bold; font-size: 16px;">${icon}</span>
<span>${message}</span>
`;
toastContainer.appendChild(toast);
// Auto-hide after 3 seconds
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
}, 3000);
// Click to dismiss
toast.addEventListener('click', () => {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
});
}
// Add CSS animations for toast
const toastStyles = document.createElement('style');
toastStyles.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(toastStyles);
function loadMenuTree() {
const container = document.getElementById('menuTreeContainer');
if (!container) return; // Safely return if element doesn't exist
// If container is empty (or only has the spinner), we can show a message or fetch data
// But since we use EJS for server-side rendering, we usually don't want to overwrite it
console.log("Menu tab activated");
}
function openImageUploader(targetInput, imageType) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
alert('Please select a valid image file');
document.body.removeChild(fileInput);
return;
}
try {
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn.innerHTML;
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
const formData = new FormData();
formData.append('image', file);
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: Upload failed`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
const input = document.getElementById(targetInput);
if (!input) {
throw new Error(`Input field #${targetInput} not found`);
}
input.value = result.path;
updateImagePreview(targetInput, result.url);
markChanged();
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed: ' + error.message);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
} finally {
if (document.body.contains(fileInput)) {
document.body.removeChild(fileInput);
}
}
};
fileInput.click();
}
function updateImagePreview(inputId, imageUrl) {
const previewContainer = document.getElementById('logoPreviewContainer');
if (!previewContainer) {
return;
}
let img = previewContainer.querySelector('img');
if (img) {
img.src = imageUrl;
} else {
img = document.createElement('img');
img.src = imageUrl;
img.className = 'img-thumbnail';
img.style.maxHeight = '100px';
img.style.maxWidth = '300px';
img.style.objectFit = 'contain';
img.style.backgroundColor = '#b8b76a';
img.alt = 'Logo preview';
previewContainer.appendChild(img);
}
}
});
</script>
<!-- Modal Add/Edit Menu (Moved OUTSIDE tabs to prevent z-index/freeze issues) -->
<div class="modal fade" id="modalAddMenu" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="menuForm" action="/admin/header/menu/create" method="POST" class="w-100">
<input type="hidden" name="id" id="menuId">
<input type="hidden" name="parentId" id="parentId">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-light border-bottom-0 py-3">
<h5 class="modal-title fw-bold" id="modalTitle">Add Menu Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label fw-medium">Menu Title</label>
<input type="text" name="title" id="formTitle" class="form-control form-control-lg fs-6" placeholder="e.g. Home, Services, About" required>
<small class="text-muted">Display text for the menu item.</small>
</div>
<div class="mb-3">
<label class="form-label fw-medium">Navigation URL</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="fas fa-link"></i></span>
<input type="text" name="url" id="formUrl" class="form-control" required placeholder="/services or https://...">
</div>
<small class="text-muted">Use relative paths for internal links.</small>
</div>
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label fw-medium">Display Order</label>
<input type="number" name="order" id="formOrder" class="form-control" value="0">
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-medium">Status</label>
<select name="status" id="formStatus" class="form-select">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
<div class="mb-0">
<label class="form-label fw-medium">Link Type</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="type" id="typeInternal" value="internal" checked>
<label class="form-check-label" for="typeInternal">Internal</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="type" id="typeExternal" value="external">
<label class="form-check-label" for="typeExternal">External</label>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-light border-top-0 py-3">
<button type="button" class="btn btn-white border px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4" id="btnSaveMenu">
<i class="fas fa-save me-1"></i>Save Changes
</button>
</div>
</div>
</form>
</div>
</div>
<script>
// AJAX handler for menuForm
const menuForm = document.getElementById('menuForm');
if (menuForm) {
menuForm.addEventListener('submit', async function(e) {
e.preventDefault();
console.log('=== TRACE: menuForm AJAX Submission Start ===');
const submitBtn = document.getElementById('btnSaveMenu');
const originalText = submitBtn ? submitBtn.innerHTML : 'Save';
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
}
try {
const formData = new FormData(this);
const data = {};
formData.forEach((value, key) => data[key] = value);
console.log('Sending data:', data);
const response = await axios({
method: 'POST',
url: this.action,
data: data
});
console.log('Response:', response.data);
if (response.data.success || response.status === 200) {
showNotification('Menu item saved successfully', 'success');
// Hide modal
const modalElement = document.getElementById('modalAddMenu');
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
modal.hide();
// Mark as changed so user needs to click Save Changes
if (typeof window.markHeaderChanged === 'function') {
window.markHeaderChanged();
}
// Reload page to show updated menu structure, preserve current tab
const currentTab = document.getElementById('activeTabInput').value;
setTimeout(() => {
window.location.href = window.location.pathname + '?tab=' + currentTab;
}, 1000);
} else {
showNotification(response.data.message || 'Unable to save menu', 'error');
}
} catch (error) {
console.error('AJAX Error:', error);
showNotification('Server connection error: ' + (error.response?.data?.message || error.message), 'error');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
}
});
}
// TRACE: Click listener for btnSaveMenu
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'btnSaveMenu') {
console.log('=== TRACE: btnSaveMenu CLICKED ===');
}
});
</script>