forked from UKSOURCE/cms.hailearning.edu.vn
drawermenu: 1. contact info: address,phone,location 2. working hours 3. description 4. social links
1211 lines
48 KiB
Plaintext
1211 lines
48 KiB
Plaintext
<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('Lỗi: ' + 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 => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[char]));
|
||
|
||
const escapedUrl = (url || '').replace(/[&<>"']/g, char => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[char]));
|
||
|
||
const escapedIcon = (icon || '').replace(/[&<>"']/g, char => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[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"> </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"> </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('Vui lòng nhập tên nền tảng');
|
||
return;
|
||
}
|
||
|
||
if (!url) {
|
||
alert('Vui lòng nhập 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} đã tồn tại`);
|
||
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('Vui lòng nhập tên nền tảng');
|
||
return;
|
||
}
|
||
|
||
if (!newUrl) {
|
||
alert('Vui lòng nhập 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>
|