Files
cms.uldp.edu.vn/views/admin/header/index.ejs
2026-04-10 15:55:15 +07:00

1211 lines
48 KiB
Plaintext
Raw 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="topbarJson" id="topbarJson" />
<input type="hidden" name="offcanvasJson" id="offcanvasJson" />
<input type="hidden" name="logo" id="logoInput" />
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar" />
<input type="hidden" name="menuUpdates" id="menuUpdates" />
<!-- 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 === 'topbar' ? 'active' : '' %>"
data-bs-toggle="tab"
href="#topbar"
role="tab"
>
<i class="fas fa-bars me-2"></i>Topbar
</a>
</li>
<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 === '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">
<!-- Topbar Tab -->
<div class="tab-pane fade <%= activeTab === 'topbar' ? 'show active' : '' %>" id="topbar" role="tabpanel">
<div class="row g-4">
<!-- Contact Information -->
<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-phone me-2"></i>Contact Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Phone Number</label>
<input
type="text"
class="form-control"
id="contactPhone"
value="<%= data.topbar.contactInfo.phone %>"
placeholder="+1 234 567 890"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Email Address</label>
<input
type="email"
class="form-control"
id="contactEmail"
value="<%= data.topbar.contactInfo.email || '' %>"
placeholder="info@example.com"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Location</label>
<input
type="text"
class="form-control"
id="contactLocation"
value="<%= data.topbar.contactInfo.location || '' %>"
placeholder="69 Street, 5th Avenue LA, United States"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Working Hours <small class="text-muted fw-normal">(Drawer Menu)</small></label>
<input
type="text"
class="form-control"
id="offcanvasWorkingHours"
value="<%= (data.header && data.header.offcanvas && data.header.offcanvas.contactInfo && data.header.offcanvas.contactInfo.workingHours) ? data.header.offcanvas.contactInfo.workingHours : '' %>"
placeholder="Mon-Friday, 09am - 05pm"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Offcanvas Description <small class="text-muted fw-normal">(Drawer Menu)</small></label>
<textarea
class="form-control"
id="offcanvasDescription"
rows="3"
placeholder="Short description displayed in the offcanvas sidebar..."
><%= (data.header && data.header.offcanvas && data.header.offcanvas.description) ? data.header.offcanvas.description : '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Social Links -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-share-alt me-2"></i>Social Media Links
</h6>
<button
type="button"
class="btn btn-primary btn-sm"
id="addSocialLink"
>
<i class="fas fa-plus me-1"></i>Add Social Link
</button>
</div>
<div class="card-body">
<div id="socialLinksContainer" class="social-links-sortable">
<!-- Social links will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 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>
<!-- 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 () {
let socialLinkIndex = 0;
// Initialize social links from data
initializeSocialLinks();
updateHiddenInputs();
// 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('#topbar input, #logo 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) ===');
updateHiddenInputs();
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 Topbar & Logo
const headerData = {
topbarJson: document.getElementById('topbarJson').value,
offcanvasJson: document.getElementById('offcanvasJson').value,
logo: document.getElementById('logoInput').value,
activeTab: document.getElementById('activeTabInput').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;
}
});
}
function updateHiddenInputs() {
const topbarData = {
contactInfo: {
phone: document.getElementById('contactPhone').value || '',
email: document.getElementById('contactEmail').value || '',
location: document.getElementById('contactLocation').value || ''
},
socialLinks: []
};
// Collect social links
document.querySelectorAll('.social-platform').forEach((input) => {
const platform = input.value.trim();
const urlInput = document.querySelector(`.social-url[data-index="${input.dataset.index}"]`);
const iconInput = document.querySelector(`.social-icon[data-index="${input.dataset.index}"]`);
const url = urlInput ? urlInput.value.trim() : '';
const icon = iconInput ? iconInput.value.trim() : '';
if (platform && url) {
topbarData.socialLinks.push({
platform: platform,
url: url,
icon: icon
});
}
});
document.getElementById('topbarJson').value = JSON.stringify(topbarData);
document.getElementById('logoInput').value = document.getElementById('logoImage').value || '';
// Collect offcanvas data — phone/email/address shared from topbar
const offcanvasData = {
description: document.getElementById('offcanvasDescription').value || '',
contactInfo: {
phone: document.getElementById('contactPhone').value || '',
email: document.getElementById('contactEmail').value || '',
address: document.getElementById('contactLocation').value || '',
workingHours: document.getElementById('offcanvasWorkingHours').value || ''
}
};
document.getElementById('offcanvasJson').value = JSON.stringify(offcanvasData);
try {
const menuUpdates = collectMenuUpdates();
document.getElementById('menuUpdates').value = JSON.stringify(menuUpdates);
} catch (e) {
console.error('Error collecting menu updates:', e);
document.getElementById('menuUpdates').value = '[]';
}
}
/**
* Collect menu updates from the menu tree
* Returns an empty array if no menu tree exists
*/
function collectMenuUpdates() {
// For now, return empty array as menu structure management is coming soon
// This function can be expanded when menu tree functionality is implemented
return [];
}
function initializeSocialLinks() {
const container = document.getElementById('socialLinksContainer');
if (!container) return;
// Extract social links safely from EJS data
let socialLinks = [];
try {
socialLinks = <%- JSON.stringify(data.topbar.socialLinks || []) %>;
} catch (e) {
console.error('Error parsing social links data:', e);
}
if (socialLinks.length === 0) {
// Add default platforms if no social links exist
const platforms = ['linkedin', 'twitter', 'instagram', 'youtube'];
platforms.forEach((platform, index) => {
addSocialLinkRow(platform, '', `fa-brands fa-${platform}`, index);
});
socialLinkIndex = platforms.length;
} else {
// Load existing social links
socialLinks.forEach((social, index) => {
const platform = social.platform || '';
const url = social.url || '';
const icon = social.icon || '';
addSocialLinkRow(platform, url, icon, index);
});
socialLinkIndex = socialLinks.length;
}
}
function addSocialLinkRow(platform, url, icon, index) {
const container = document.getElementById('socialLinksContainer');
const socialLink = document.createElement('div');
socialLink.className = 'card mb-3 border social-link-item';
socialLink.dataset.platform = platform;
// Escape HTML to prevent XSS
const escapedPlatform = (platform || '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
const escapedUrl = (url || '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
const escapedIcon = (icon || '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
socialLink.innerHTML = `
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-2">
<label class="form-label fw-medium">Platform</label>
<input type="text" class="form-control social-platform" value="${escapedPlatform}" data-index="${index}" disabled />
</div>
<div class="col-md-4">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control social-url" value="${escapedUrl}" data-index="${index}" placeholder="https://..." />
</div>
<div class="col-md-3">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control social-icon" value="${escapedIcon}" data-index="${index}" />
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-primary btn-sm edit-social-link mx-3 rounded" style="transform: none" data-index="${index}" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link rounded" data-index="${index}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(socialLink);
}
/* ============================================
DRAG & DROP STYLING
============================================ */
const dragDropStyles = document.createElement('style');
dragDropStyles.textContent = `
.social-link-item {
transition: transform 0.2s ease;
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
cursor: grab !important;
}
.drag-handle:active {
cursor: grabbing !important;
}
.drag-handle:hover {
background-color: #f0f0f0;
color: #0d6efd;
}
/* SortableJS Classes */
.social-ghost {
opacity: 0.4;
border: 2px dashed #0d6efd !important;
background-color: #f8f9fa !important;
}
.social-chosen {
background-color: #eef3ff !important;
box-shadow: 0 5px 15px rgba(0,0,0,0.1) !important;
}
.social-drag {
opacity: 0.9;
}
/* Fix Modal Freeze & Z-Index issues */
body.modal-open {
overflow: hidden !important;
padding-right: 0 !important;
}
`;
document.head.appendChild(dragDropStyles);
document.addEventListener('click', function (e) {
if (e.target.closest('.remove-social-link')) {
e.preventDefault();
const btn = e.target.closest('.remove-social-link');
const index = btn.dataset.index;
const platformInput = document.querySelector(`.social-platform[data-index="${index}"]`);
const platform = platformInput.value;
if (confirm(`Delete ${platform} social link?`)) {
deleteSocialLink(platform, btn.closest('.card'));
}
}
if (e.target.closest('.edit-social-link')) {
e.preventDefault();
const btn = e.target.closest('.edit-social-link');
const index = btn.dataset.index;
const platformInput = document.querySelector(`.social-platform[data-index="${index}"]`);
const urlInput = document.querySelector(`.social-url[data-index="${index}"]`);
const iconInput = document.querySelector(`.social-icon[data-index="${index}"]`);
const platform = platformInput.value;
const url = urlInput.value;
const icon = iconInput.value;
showEditSocialLinkModal(platform, url, icon);
}
if (e.target.closest('#addSocialLink')) {
e.preventDefault();
showAddSocialLinkModal();
}
});
function deleteSocialLink(platform, cardElement) {
if (confirm(`Delete ${platform} social link?`)) {
cardElement.remove();
updateHiddenInputs();
markChanged();
}
}
function showAddSocialLinkModal() {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'addSocialLinkModal';
modal.setAttribute('tabindex', '-1');
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Social Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Platform <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newSocialPlatform" placeholder="e.g., linkedin, twitter, instagram, youtube, facebook" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newSocialUrl" placeholder="https://..." />
</div>
<div class="mb-3">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control" id="newSocialIcon" placeholder="e.g., fa-brands fa-linkedin" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveSocialLink">Save</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
document.getElementById('saveSocialLink').addEventListener('click', function() {
const platform = document.getElementById('newSocialPlatform').value.trim();
const url = document.getElementById('newSocialUrl').value.trim();
const icon = document.getElementById('newSocialIcon').value.trim();
if (!platform) {
alert('Please enter a platform name');
return;
}
if (!url) {
alert('Please enter a URL');
return;
}
// Check if platform already exists
const existingPlatforms = Array.from(document.querySelectorAll('.social-platform')).map(el => el.value);
if (existingPlatforms.includes(platform)) {
alert(`${platform} already exists`);
return;
}
addSocialLinkViaAPI(platform, url, icon || `fa-brands fa-${platform}`, bsModal, modal);
});
bsModal.show();
// Focus on platform input
setTimeout(() => {
document.getElementById('newSocialPlatform').focus();
}, 500);
}
function showEditSocialLinkModal(platform, url, icon) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'editSocialLinkModal';
modal.setAttribute('tabindex', '-1');
const platformDisplay = platform ? platform.charAt(0).toUpperCase() + platform.slice(1) : 'Social';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit ${platformDisplay} Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Platform <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSocialPlatform" value="${platform}" placeholder="e.g., linkedin, twitter, instagram" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSocialUrl" value="${url}" placeholder="https://www.example.com" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control" id="editSocialIcon" value="${icon}" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="updateSocialLink">Update</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
document.getElementById('updateSocialLink').addEventListener('click', function() {
const newPlatform = document.getElementById('editSocialPlatform').value.trim();
const newUrl = document.getElementById('editSocialUrl').value.trim();
const newIcon = document.getElementById('editSocialIcon').value.trim();
if (!newPlatform) {
alert('Please enter a platform name');
return;
}
if (!newUrl) {
alert('Please enter a URL');
return;
}
updateSocialLinkViaAPIModal(platform, newPlatform, newUrl, newIcon, bsModal, modal);
});
bsModal.show();
// Focus on platform input
setTimeout(() => {
document.getElementById('editSocialPlatform').focus();
}, 500);
}
function addSocialLinkViaAPI(platform, url, icon, modal, modalElement) {
addSocialLinkRow(platform, url, icon, Date.now());
updateHiddenInputs();
markChanged();
modal.hide();
modalElement.remove();
}
function updateSocialLinkViaAPIModal(oldPlatform, newPlatform, url, icon, modal, modalElement) {
newPlatform = newPlatform.toLowerCase().trim();
// Update the input fields in the DOM
const platformInputs = document.querySelectorAll(`.social-platform`);
const urlInputs = document.querySelectorAll(`.social-url`);
const iconInputs = document.querySelectorAll(`.social-icon`);
let found = false;
for (let i = 0; i < platformInputs.length; i++) {
if (platformInputs[i].value.toLowerCase() === oldPlatform.toLowerCase()) {
platformInputs[i].value = newPlatform;
urlInputs[i].value = url;
if (iconInputs[i]) iconInputs[i].value = icon;
found = true;
break;
}
}
if (found) {
modal.hide();
modalElement.remove();
updateHiddenInputs();
markChanged();
}
}
/**
* 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);
}
}
// Initialize Sortable for Social Links
const socialLinksContainer = document.getElementById('socialLinksContainer');
const SortableLib = window.Sortable || Sortable;
console.log('=== TRACE: Social Sortable Init ===', {
containerExists: !!socialLinksContainer,
sortableDefined: typeof SortableLib !== 'undefined'
});
if (socialLinksContainer && typeof SortableLib !== 'undefined') {
try {
new SortableLib(socialLinksContainer, {
animation: 150,
handle: '.drag-handle',
ghostClass: 'social-ghost',
chosenClass: 'social-chosen',
dragClass: 'social-drag',
forceFallback: true, // Use transition-based dragging for better compatibility
onStart: function() {
console.log('=== TRACE: Social Drag Started ===');
},
onEnd: function() {
console.log('=== TRACE: Social Drag Ended ===');
updateHiddenInputs();
markChanged();
}
});
console.log('=== TRACE: Sortable initialized for socialLinksContainer ===');
} catch (err) {
console.error('=== TRACE: Sortable Init Error ===', err);
}
} else {
console.warn('SortableJS not loaded or socialLinksContainer not found');
}
});
</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-3">
<label class="form-label fw-medium d-block">Maintenance Mode</label>
<input type="hidden" name="is_maintainance" value="false">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" name="is_maintainance" id="formMaintainance" value="true">
<label class="form-check-label" for="formMaintainance">
Redirect this menu page to the maintenance screen while it is being repaired.
</label>
</div>
<small class="text-muted">Use this when the linked page should be temporarily unavailable to visitors.</small>
</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>