forked from UKSOURCE/cms.hailearning.edu.vn
1215 lines
44 KiB
Plaintext
1215 lines
44 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">
|
||
<form
|
||
action="/admin/header/update"
|
||
method="POST"
|
||
class="content-with-fixed-buttons"
|
||
id="headerForm"
|
||
>
|
||
<!-- Hidden inputs for JSON data -->
|
||
<input type="hidden" name="topbarJson" id="topbarJson" />
|
||
<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 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"
|
||
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"
|
||
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 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Social Links -->
|
||
<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-share-alt me-2"></i>Social Media Links
|
||
<small class="text-muted ms-2">(Drag to reorder)</small>
|
||
</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="socialLinksContainer" class="social-links-sortable">
|
||
<!-- Social links will be populated here -->
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline-primary btn-sm"
|
||
id="addSocialLink"
|
||
>
|
||
<i class="fas fa-plus me-1"></i>Add Social Link
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Logo Tab -->
|
||
<div class="tab-pane fade" 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" id="menu" role="tabpanel">
|
||
<div class="row g-4">
|
||
<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-sitemap me-2"></i>Menu Structure
|
||
</h6>
|
||
<div>
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline-success btn-sm me-2"
|
||
id="saveMenuChanges"
|
||
style="display: none"
|
||
>
|
||
<i class="fas fa-save me-1"></i>Save Changes
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline-primary btn-sm"
|
||
id="refreshMenuTree"
|
||
>
|
||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-info">
|
||
<i class="fas fa-info-circle me-2"></i>
|
||
Click "Save Changes" to apply menu structure updates.
|
||
</div>
|
||
<div id="menuTreeContainer">
|
||
<div class="text-center">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
<p class="mt-2">Loading menu structure...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Move buttons to fixed bottom -->
|
||
<div class="fixed-bottom-buttons">
|
||
<button type="reset" class="btn btn-secondary">
|
||
<i class="fas fa-undo"></i>
|
||
<span>Reset</span>
|
||
</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-save"></i>
|
||
<span>Save Changes</span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
let socialLinkIndex = 0;
|
||
|
||
// Initialize social links from data
|
||
initializeSocialLinks();
|
||
updateHiddenInputs();
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const activeTabObj = urlParams.get('activeTab');
|
||
|
||
if (activeTabObj) {
|
||
const tabTrigger = document.querySelector(`a[href="#${activeTabObj}"]`);
|
||
if (tabTrigger) {
|
||
new bootstrap.Tab(tabTrigger).show();
|
||
document.getElementById('activeTabInput').value = activeTabObj;
|
||
}
|
||
}
|
||
|
||
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;
|
||
});
|
||
});
|
||
|
||
document.querySelector('a[href="#menu"]').addEventListener('shown.bs.tab', function () {
|
||
loadMenuTree();
|
||
});
|
||
|
||
document.getElementById('refreshMenuTree').addEventListener('click', function () {
|
||
loadMenuTree();
|
||
});
|
||
|
||
document.getElementById('saveMenuChanges').addEventListener('click', function () {
|
||
saveMenuChanges();
|
||
});
|
||
|
||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||
button.addEventListener('click', function () {
|
||
const targetInput = this.dataset.targetInput;
|
||
const imageType = this.dataset.imageType;
|
||
openImageUploader(targetInput, imageType);
|
||
});
|
||
});
|
||
|
||
document.getElementById('headerForm').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
updateHiddenInputs();
|
||
|
||
const submitBtn = this.querySelector('button[type="submit"]');
|
||
const originalText = submitBtn.innerHTML;
|
||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Đang lưu...';
|
||
submitBtn.disabled = true;
|
||
|
||
// Collect form data as JSON
|
||
const formData = {
|
||
topbarJson: document.getElementById('topbarJson').value,
|
||
logo: document.getElementById('logoInput').value,
|
||
menuUpdates: document.getElementById('menuUpdates').value,
|
||
activeTab: document.getElementById('activeTabInput').value
|
||
};
|
||
|
||
console.log('=== FORM SUBMISSION ===');
|
||
console.log('Form data being sent:', formData);
|
||
console.log('topbarJson parsed:', JSON.parse(formData.topbarJson || '{}'));
|
||
|
||
// Send via AJAX with JSON
|
||
fetch('/admin/header/update', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(formData)
|
||
})
|
||
.then(response => {
|
||
console.log('Response status:', response.status);
|
||
return response.json();
|
||
})
|
||
.then(result => {
|
||
console.log('Response result:', result);
|
||
if (result.success) {
|
||
showNotification('Changes saved successfully', 'success');
|
||
submitBtn.innerHTML = originalText;
|
||
submitBtn.disabled = false;
|
||
} else {
|
||
showNotification('Error: ' + (result.message || 'Unable to save changes'), 'error');
|
||
submitBtn.innerHTML = originalText;
|
||
submitBtn.disabled = false;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('Lỗi: ' + error.message, 'error');
|
||
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 || '';
|
||
|
||
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');
|
||
const socialLinks = <%- JSON.stringify(data.topbar.socialLinks || []) %>;
|
||
|
||
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.draggable = true;
|
||
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: opacity 0.2s ease, background-color 0.2s ease;
|
||
}
|
||
|
||
.social-link-item[draggable="true"] {
|
||
cursor: grab;
|
||
}
|
||
|
||
.social-link-item[draggable="true"]:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.drag-handle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.drag-handle:hover {
|
||
background-color: #f0f0f0;
|
||
color: #0d6efd;
|
||
}
|
||
|
||
.social-link-item.dragging {
|
||
opacity: 0.5;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.social-link-item.drag-over {
|
||
border-top: 3px solid #0d6efd;
|
||
padding-top: 10px;
|
||
}
|
||
`;
|
||
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) {
|
||
// Convert platform to lowercase
|
||
platform = platform.toLowerCase().trim();
|
||
|
||
console.log("Deleting social link:", platform);
|
||
|
||
fetch(`/api/social-links/${platform}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => {
|
||
console.log("Delete response status:", response.status);
|
||
return response.json();
|
||
})
|
||
.then(result => {
|
||
console.log("Delete API response:", result);
|
||
if (result.success) {
|
||
cardElement.remove();
|
||
updateHiddenInputs();
|
||
showNotification('Xóa liên kết mạng xã hội thành công', 'success');
|
||
} else {
|
||
showNotification(result.message || 'Không thể xóa liên kết mạng xã hội', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('Lỗi xóa liên kết mạng xã hội: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
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) {
|
||
console.log("Adding social link:", { platform, url, icon });
|
||
|
||
fetch('/api/social-links', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
platform,
|
||
url,
|
||
icon
|
||
})
|
||
})
|
||
.then(response => {
|
||
console.log("Response status:", response.status);
|
||
return response.json();
|
||
})
|
||
.then(result => {
|
||
console.log("API response:", result);
|
||
if (result.success) {
|
||
modal.hide();
|
||
modalElement.remove();
|
||
addSocialLinkRow(platform, url, icon, Date.now());
|
||
updateHiddenInputs();
|
||
showNotification('Thêm liên kết mạng xã hội thành công', 'success');
|
||
} else {
|
||
showNotification(result.message || 'Không thể thêm liên kết mạng xã hội', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('Lỗi thêm liên kết mạng xã hội: ' + error.message, 'error');
|
||
});
|
||
}
|
||
|
||
function updateSocialLinkViaAPIModal(oldPlatform, newPlatform, url, icon, modal, modalElement) {
|
||
// Convert to lowercase
|
||
oldPlatform = oldPlatform.toLowerCase().trim();
|
||
newPlatform = newPlatform.toLowerCase().trim();
|
||
url = url.trim();
|
||
icon = icon ? icon.trim() : null;
|
||
|
||
console.log("Updating social link:", { oldPlatform, newPlatform, url, icon });
|
||
|
||
// If platform changed, need to delete old and create new
|
||
if (oldPlatform !== newPlatform) {
|
||
console.log("Platform changed, deleting old and creating new");
|
||
|
||
// Check if new platform already exists
|
||
const existingPlatforms = Array.from(document.querySelectorAll('.social-platform')).map(el => el.value.toLowerCase());
|
||
if (existingPlatforms.includes(newPlatform)) {
|
||
showNotification(`Nền tảng ${newPlatform} đã tồn tại`, 'error');
|
||
return;
|
||
}
|
||
|
||
// First delete old platform
|
||
fetch(`/api/social-links/${oldPlatform}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
console.log("Old platform deleted, creating new");
|
||
// Then create new platform
|
||
return fetch('/api/social-links', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
platform: newPlatform,
|
||
url,
|
||
icon
|
||
})
|
||
});
|
||
} else {
|
||
throw new Error(result.message);
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
console.log("Update API response:", result);
|
||
if (result.success) {
|
||
modal.hide();
|
||
modalElement.remove();
|
||
|
||
// Update the input fields
|
||
const urlInputs = document.querySelectorAll(`.social-url`);
|
||
const platformInputs = document.querySelectorAll(`.social-platform`);
|
||
|
||
for (let i = 0; i < platformInputs.length; i++) {
|
||
if (platformInputs[i].value.toLowerCase() === oldPlatform) {
|
||
platformInputs[i].value = newPlatform;
|
||
urlInputs[i].value = url;
|
||
const iconInput = document.querySelector(`.social-icon[data-index="${platformInputs[i].dataset.index}"]`);
|
||
if (iconInput) {
|
||
iconInput.value = icon;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
updateHiddenInputs();
|
||
showNotification('Cập nhật liên kết mạng xã hội thành công', 'success');
|
||
} else {
|
||
showNotification(result.message || 'Không thể cập nhật liên kết mạng xã hội', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('Lỗi cập nhật liên kết mạng xã hội: ' + error.message, 'error');
|
||
});
|
||
} else {
|
||
// Platform not changed, just update URL and icon
|
||
console.log("Platform not changed, updating URL and icon only");
|
||
|
||
fetch(`/api/social-links/${oldPlatform}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
url,
|
||
icon
|
||
})
|
||
})
|
||
.then(response => {
|
||
console.log("Update response status:", response.status);
|
||
return response.json();
|
||
})
|
||
.then(result => {
|
||
console.log("Update API response:", result);
|
||
if (result.success) {
|
||
modal.hide();
|
||
modalElement.remove();
|
||
|
||
// Update the input fields
|
||
const urlInputs = document.querySelectorAll(`.social-url`);
|
||
const platformInputs = document.querySelectorAll(`.social-platform`);
|
||
|
||
for (let i = 0; i < platformInputs.length; i++) {
|
||
if (platformInputs[i].value.toLowerCase() === oldPlatform) {
|
||
urlInputs[i].value = url;
|
||
const iconInput = document.querySelector(`.social-icon[data-index="${platformInputs[i].dataset.index}"]`);
|
||
if (iconInput) {
|
||
iconInput.value = icon;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
updateHiddenInputs();
|
||
showNotification('Cập nhật liên kết mạng xã hội thành công', 'success');
|
||
} else {
|
||
showNotification(result.message || 'Không thể cập nhật liên kết mạng xã hội', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showNotification('Lỗi cập nhật liên kết mạng xã hội: ' + error.message, 'error');
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
container.innerHTML = '<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>Menu structure management coming soon.</div>';
|
||
}
|
||
|
||
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);
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// DRAG & DROP REORDERING FUNCTIONALITY
|
||
// ============================================
|
||
let draggedElement = null;
|
||
|
||
// Handle drag start - user begins dragging a social link item
|
||
document.addEventListener('dragstart', function (e) {
|
||
if (e.target.closest('.social-link-item')) {
|
||
draggedElement = e.target.closest('.social-link-item');
|
||
draggedElement.style.opacity = '0.5';
|
||
draggedElement.style.cursor = 'grabbing';
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
}
|
||
});
|
||
|
||
// Handle drag over - provide visual feedback as item is dragged over other items
|
||
document.addEventListener('dragover', function (e) {
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = 'move';
|
||
|
||
const socialItem = e.target.closest('.social-link-item');
|
||
if (socialItem && socialItem !== draggedElement) {
|
||
// Add visual feedback to show drop zone
|
||
socialItem.style.borderTop = '3px solid #0d6efd';
|
||
socialItem.style.paddingTop = '10px';
|
||
}
|
||
});
|
||
|
||
// Handle drag leave - remove visual feedback when leaving an item
|
||
document.addEventListener('dragleave', function (e) {
|
||
const socialItem = e.target.closest('.social-link-item');
|
||
if (socialItem) {
|
||
socialItem.style.borderTop = '';
|
||
socialItem.style.paddingTop = '';
|
||
}
|
||
});
|
||
|
||
// Handle drop - reorder items and persist to backend
|
||
document.addEventListener('drop', function (e) {
|
||
e.preventDefault();
|
||
|
||
const socialItem = e.target.closest('.social-link-item');
|
||
if (socialItem && socialItem !== draggedElement) {
|
||
// Reorder DOM elements
|
||
const container = document.getElementById('socialLinksContainer');
|
||
const allItems = Array.from(container.querySelectorAll('.social-link-item'));
|
||
const draggedIndex = allItems.indexOf(draggedElement);
|
||
const targetIndex = allItems.indexOf(socialItem);
|
||
|
||
if (draggedIndex < targetIndex) {
|
||
socialItem.parentNode.insertBefore(draggedElement, socialItem.nextSibling);
|
||
} else {
|
||
socialItem.parentNode.insertBefore(draggedElement, socialItem);
|
||
}
|
||
|
||
// Clear visual feedback
|
||
socialItem.style.borderTop = '';
|
||
socialItem.style.paddingTop = '';
|
||
|
||
// Persist the new order to backend
|
||
persistSocialLinksOrder();
|
||
}
|
||
});
|
||
|
||
// Handle drag end - cleanup and reset styles
|
||
document.addEventListener('dragend', function (e) {
|
||
if (draggedElement) {
|
||
draggedElement.style.opacity = '1';
|
||
draggedElement.style.cursor = 'grab';
|
||
draggedElement = null;
|
||
}
|
||
|
||
// Clear all visual feedback from all items
|
||
document.querySelectorAll('.social-link-item').forEach(item => {
|
||
item.style.borderTop = '';
|
||
item.style.paddingTop = '';
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Persist the reordered social links to the backend
|
||
* Collects current order from DOM and sends to API
|
||
* Updates the order field for each social link based on new position
|
||
*/
|
||
function persistSocialLinksOrder() {
|
||
const container = document.getElementById('socialLinksContainer');
|
||
const items = container.querySelectorAll('.social-link-item');
|
||
|
||
// Build order array with platform and new order position
|
||
const socialLinks = Array.from(items).map((item, index) => {
|
||
const platform = item.dataset.platform;
|
||
const urlInput = item.querySelector('.social-url');
|
||
const iconInput = item.querySelector('.social-icon');
|
||
|
||
return {
|
||
platform: platform,
|
||
url: urlInput.value,
|
||
icon: iconInput.value,
|
||
order: index + 1 // Order starts from 1 (top to bottom)
|
||
};
|
||
});
|
||
|
||
console.log('Persisting social links order:', socialLinks);
|
||
|
||
// Send to backend via bulk-update endpoint
|
||
fetch('/api/social-links/bulk-update', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ socialLinks })
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
console.log('Social links order updated successfully');
|
||
showNotification('Cập nhật thứ tự thành công', 'success');
|
||
updateHiddenInputs();
|
||
} else {
|
||
console.error('Failed to update order:', result.message);
|
||
showNotification('Không thể cập nhật thứ tự: ' + result.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error persisting social links order:', error);
|
||
showNotification('Lỗi cập nhật thứ tự: ' + error.message, 'error');
|
||
});
|
||
}
|
||
});
|
||
</script>
|