Files
uldp-degree-mangement-system/views/admin/faq/index.ejs
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

1019 lines
57 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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit FAQ sections and questions displayed on FAQ page</p>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="faqForm"
action="/admin/faq/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="sidebarNav" id="sidebarNavJson">
<input type="hidden" name="contactBox" id="contactBoxJson">
<input type="hidden" name="faqSections" id="faqSectionsJson">
<input type="hidden" name="video" id="videoJson">
<!-- 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="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#sidebar" role="tab">
<i class="fas fa-bars me-2"></i>Sidebar Navigation
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#sections" role="tab">
<i class="fas fa-layer-group me-2"></i>FAQ Sections
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#contactBox" role="tab">
<i class="fas fa-address-card me-2"></i>Contact Box
</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#video" role="tab">
<i class="fas fa-video me-2"></i>Video
</a>
</li> -->
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage" value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="faq">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 300px;">
<% if (data.hero?.backgroundImage) { %>
<%
let heroImgSrc = data.hero.backgroundImage;
if (heroImgSrc && !heroImgSrc.startsWith('http://') && !heroImgSrc.startsWith('https://')) {
heroImgSrc = heroImgSrc.startsWith('/') ? heroImgSrc : '/' + heroImgSrc;
}
%>
<img src="<%= heroImgSrc %>" class="img-thumbnail" id="heroPreviewImg"
style="height: 300px; width: 100%; object-fit: cover;"
alt="Background image preview" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
</div>
<!-- Hidden fields for hero settings - keep defaults -->
<input type="hidden" id="heroOverlayColor" value="<%= data.hero?.overlayColor || 'rgba(0, 0, 0, 0)' %>">
<input type="hidden" id="heroSectionClass" value="<%= data.hero?.sectionClass || '' %>">
<input type="hidden" id="heroTitleClass" value="<%= data.hero?.titleClass || '' %>">
<input type="hidden" id="heroBackgroundPosition" value="<%= data.hero?.backgroundPosition || 'top-center' %>">
<input type="hidden" id="heroEnableScrollspy" value="<%= data.hero?.enableScrollspy ? 'true' : 'false' %>">
</div>
</div>
</div>
<!-- Sidebar Navigation Tab -->
<div class="tab-pane fade" id="sidebar" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Sidebar Navigation Items</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addSidebarItem()">
<i class="fas fa-plus"></i> Add Item
</button>
</div>
<div id="sidebarNavContainer">
<% if (data.sidebarNav && data.sidebarNav.length > 0) { %>
<% data.sidebarNav.forEach((item, index) => { %>
<div class="card mb-3 sidebar-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID (Unique)</label>
<input type="text" class="form-control sidebar-id-input"
name="sidebarId_<%= index %>"
value="<%= item.id || '' %>"
placeholder="e.g., general-information"
oninput="updateSidebarId(this)">
</div>
<div class="col-md-6">
<label class="form-label">Label</label>
<input type="text" class="form-control"
name="sidebarLabel_<%= index %>"
value="<%= item.label || '' %>"
placeholder="e.g., General Information">
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeSidebarItem(this)">
<i class="fas fa-trash me-2"></i>Remove Item
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
<div class="alert alert-info mt-3">
<i class="fas fa-info-circle me-2"></i>
<strong>Important:</strong> The ID must match the section ID in FAQ Sections.
Each ID must be unique and use lowercase with hyphens (e.g., "general-information").
</div>
</div>
</div>
</div>
<!-- FAQ Sections Tab -->
<div class="tab-pane fade" id="sections" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">FAQ Sections</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFaqSection()">
<i class="fas fa-plus"></i> Add Section
</button>
</div>
<div id="faqSectionsContainer">
<% if (data.faqSections && data.faqSections.length > 0) { %>
<% data.faqSections.forEach((section, sectionIndex) => { %>
<div class="card mb-4 faq-section-item" data-section-index="<%= sectionIndex %>">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-medium">Section <%= sectionIndex + 1 %>: <%= section.title || 'Untitled' %></h6>
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="removeFaqSection(this)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Section ID</label>
<input type="text" class="form-control section-id-input"
name="sectionId_<%= sectionIndex %>"
value="<%= section.id || '' %>"
placeholder="e.g., general-information"
data-section-index="<%= sectionIndex %>"
oninput="updateSectionId(this)">
</div>
<div class="col-md-6">
<label class="form-label">Section Title</label>
<input type="text" class="form-control section-title-input"
name="sectionTitle_<%= sectionIndex %>"
value="<%= section.title || '' %>"
placeholder="e.g., General Information"
data-section-index="<%= sectionIndex %>"
oninput="updateSectionTitle(this)">
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">FAQ Items in this Section</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addFaqItem(<%= sectionIndex %>)">
<i class="fas fa-plus"></i> Add FAQ
</button>
</div>
<div class="faq-items-container" id="faqItems_<%= sectionIndex %>">
<% if (section.faqs && section.faqs.length > 0) { %>
<% section.faqs.forEach((faq, faqIndex) => { %>
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question-input"
name="faqQuestion_<%= sectionIndex %>_<%= faqIndex %>"
value="<%= faq.title || '' %>"
placeholder="e.g., What are the camp dates?">
</div>
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer-input"
name="faqAnswer_<%= sectionIndex %>_<%= faqIndex %>"
rows="4"
placeholder="Enter detailed answer here..."><%= (faq.description || '').replace(/\n/g, '\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFaqItem(this)">
<i class="fas fa-trash me-2"></i>Remove FAQ
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Contact Box Tab -->
<div class="tab-pane fade" id="contactBox" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Contact Box Settings</h6>
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Title</label>
<input type="text" class="form-control" id="contactBoxTitle"
value="<%= data.contactBox?.title || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Phone Number</label>
<input type="text" class="form-control" id="contactBoxPhone"
value="<%= data.contactBox?.phone?.text || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Email Address</label>
<input type="text" class="form-control" id="contactBoxEmail"
value="<%= data.contactBox?.email?.text || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Phone Icon</label>
<select class="form-select" id="contactBoxPhoneIcon">
<option value="phone" <%= data.contactBox?.phone?.icon === 'phone' ? 'selected' : '' %>>Phone</option>
<option value="mobile-alt" <%= data.contactBox?.phone?.icon === 'mobile-alt' ? 'selected' : '' %>>Mobile</option>
<option value="whatsapp" <%= data.contactBox?.phone?.icon === 'whatsapp' ? 'selected' : '' %>>WhatsApp</option>
<option value="headset" <%= data.contactBox?.phone?.icon === 'headset' ? 'selected' : '' %>>Headset</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Email Icon</label>
<select class="form-select" id="contactBoxEmailIcon">
<option value="envelope" <%= data.contactBox?.email?.icon === 'envelope' ? 'selected' : '' %>>Envelope</option>
<option value="mail-bulk" <%= data.contactBox?.email?.icon === 'mail-bulk' ? 'selected' : '' %>>Mail Bulk</option>
<option value="paper-plane" <%= data.contactBox?.email?.icon === 'paper-plane' ? 'selected' : '' %>>Paper Plane</option>
<option value="at" <%= data.contactBox?.email?.icon === 'at' ? 'selected' : '' %>>@ Symbol</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Video Tab -->
<div class="tab-pane fade" id="video" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Video Settings</h6>
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">YouTube Video URL</label>
<div class="input-group">
<input type="text" class="form-control" id="videoUrl"
value="<%= data.video?.url || '' %>"
placeholder="https://www.youtube.com/embed/VIDEO_ID">
<button type="button" class="btn btn-outline-secondary" onclick="parseYouTubeUrl()">
<i class="fas fa-sync-alt me-1"></i>Parse
</button>
</div>
<small class="text-muted">
Enter full YouTube embed URL or video ID.
Example: https://www.youtube.com/embed/3NtE5wSwYTo
</small>
</div>
<div class="col-md-12">
<label class="form-label">Video Title</label>
<input type="text" class="form-control" id="videoTitle"
value="<%= data.video?.title || '' %>"
placeholder="e.g., Anti Homesickness Adviser">
</div>
<div class="col-md-12">
<div id="videoPreview" class="mt-3">
<% if (data.video?.url) { %>
<div class="ratio ratio-16x9">
<iframe src="<%= data.video.url %>"
title="<%= data.video.title || 'FAQ Video' %>"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
<% if (data.video.title) { %>
<div class="mt-2 text-center">
<small class="text-muted">🎬 <%= data.video.title %></small>
</div>
<% } %>
<% } else { %>
<div class="border rounded p-5 text-center text-muted" style="height: 200px; display: flex; align-items: center; justify-content: center;">
Enter YouTube URL above to see video preview
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script>
let originalFormData = null;
let sidebarIdCounter = 0;
let sectionIdCounter = 0;
document.addEventListener('DOMContentLoaded', function () {
originalFormData = <%- JSON.stringify(data) %>;
updateAllJsonInputs(originalFormData);
initializeFormHandlers();
// Initialize counters
sidebarIdCounter = originalFormData.sidebarNav ? originalFormData.sidebarNav.length : 0;
sectionIdCounter = originalFormData.faqSections ? originalFormData.faqSections.length : 0;
});
function initializeFormHandlers() {
const form = document.getElementById('faqForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
// Initialize video URL input handler
const videoUrlInput = document.getElementById('videoUrl');
if (videoUrlInput) {
let videoTimeout;
videoUrlInput.addEventListener('input', function() {
clearTimeout(videoTimeout);
videoTimeout = setTimeout(() => {
updateVideoPreview();
}, 800);
});
}
}
function updateVideoPreview() {
const videoUrl = document.getElementById('videoUrl').value.trim();
const videoTitle = document.getElementById('videoTitle').value.trim();
const preview = document.getElementById('videoPreview');
if (!preview) return;
if (!videoUrl) {
preview.innerHTML = `
<div class="border rounded p-5 text-center text-muted" style="height: 200px; display: flex; align-items: center; justify-content: center;">
Enter YouTube URL above to see video preview
</div>
`;
return;
}
let embedUrl = videoUrl;
// Convert various YouTube URL formats to embed URL
if (videoUrl.includes('youtube.com/watch?v=')) {
const videoId = videoUrl.split('v=')[1]?.split('&')[0];
embedUrl = `https://www.youtube.com/embed/${videoId}`;
} else if (videoUrl.includes('youtu.be/')) {
const videoId = videoUrl.split('youtu.be/')[1]?.split('?')[0];
embedUrl = `https://www.youtube.com/embed/${videoId}`;
} else if (!videoUrl.includes('youtube.com/embed/')) {
// Assume it's a video ID if it's short and doesn't contain special characters
if (videoUrl.length <= 20 && /^[a-zA-Z0-9_-]+$/.test(videoUrl)) {
embedUrl = `https://www.youtube.com/embed/${videoUrl}`;
}
}
preview.innerHTML = `
<div class="ratio ratio-16x9">
<iframe src="${embedUrl}"
title="${videoTitle || 'FAQ Video'}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
${videoTitle ? `<div class="mt-2 text-center"><small class="text-muted">🎬 ${videoTitle}</small></div>` : ''}
`;
}
function parseYouTubeUrl() {
const urlInput = document.getElementById('videoUrl');
let url = urlInput.value.trim();
if (!url) return;
// Extract video ID from various YouTube URL formats
let videoId = '';
if (url.includes('youtube.com/watch?v=')) {
videoId = url.split('v=')[1]?.split('&')[0];
} else if (url.includes('youtu.be/')) {
videoId = url.split('youtu.be/')[1]?.split('?')[0];
} else if (url.includes('youtube.com/embed/')) {
videoId = url.split('embed/')[1]?.split('?')[0];
} else if (url.length <= 20 && /^[a-zA-Z0-9_-]+$/.test(url)) {
// Already a video ID
videoId = url;
}
if (videoId) {
// Clean up the video ID (remove any invalid characters)
videoId = videoId.replace(/[^a-zA-Z0-9_-]/g, '');
urlInput.value = `https://www.youtube.com/embed/${videoId}`;
updateVideoPreview();
showToast('Success', 'YouTube URL parsed successfully', 'success');
} else {
showToast('Error', 'Could not parse YouTube URL. Please check the format.', 'error');
}
}
// Thêm/update các hàm sau trong script của admin/faq/index.ejs:
// Hàm mở image uploader
// Use a hidden file input so the native file picker opens without any modal/backdrop.
function openImageUploader(targetInput, imageType) {
let uploadInput = document.getElementById('imageUploadInput');
if (!uploadInput) {
uploadInput = document.createElement('input');
uploadInput.type = 'file';
uploadInput.accept = 'image/*';
uploadInput.id = 'imageUploadInput';
uploadInput.style.display = 'none';
document.body.appendChild(uploadInput);
}
// Clear previous value/handlers to ensure onchange always fires
uploadInput.value = '';
uploadInput.onchange = async function(e) {
const file = e.target.files[0];
if (!file) return;
try {
// Optional preview: show selected file in the hero preview area (if present)
const reader = new FileReader();
reader.onload = function(ev) {
const previewDiv = document.getElementById('heroImagePreview');
if (previewDiv) {
let previewImg = document.getElementById('heroPreviewImg');
if (!previewImg) {
previewDiv.innerHTML = `
<img src="${ev.target.result}" class="img-thumbnail" id="heroPreviewImg"
style="height: 300px; width: 100%; object-fit: cover;"
alt="Background image preview">
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewImg.src = ev.target.result;
previewImg.style.display = 'block';
if (previewImg.nextElementSibling) previewImg.nextElementSibling.style.display = 'none';
}
}
};
reader.readAsDataURL(file);
await uploadImage(file, imageType, targetInput);
} catch (err) {
console.error('Upload error:', err);
showToast('Error', 'Failed to upload image: ' + err.message, 'error');
} finally {
// Reset input so selecting the same file again will trigger onchange
uploadInput.value = '';
}
};
// Trigger native file picker without any modal/backdrop
uploadInput.click();
}
// Hàm load images đã upload
async function loadExistingImages(imageType) {
try {
const response = await fetch(`/admin/upload/list?imageType=${imageType}`);
if (!response.ok) return;
const data = await response.json();
const imagesGrid = document.getElementById('uploadedImagesGrid');
if (!data.images || data.images.length === 0) {
imagesGrid.innerHTML = '<div class="col-12 text-center text-muted">No images uploaded yet</div>';
return;
}
imagesGrid.innerHTML = data.images.map(img => `
<div class="col-md-4 col-sm-6 mb-3">
<div class="image-thumbnail" style="cursor: pointer;" data-path="${img.path}" data-url="${img.url}">
<img src="${img.url}" class="img-fluid rounded" style="height: 120px; width: 100%; object-fit: cover;">
<div class="text-center mt-1 small text-truncate">${img.name}</div>
</div>
</div>
`).join('');
// Add click handlers
document.querySelectorAll('.image-thumbnail').forEach(thumb => {
thumb.addEventListener('click', function() {
selectExistingImage(this.dataset.path, this.dataset.url);
const modal = bootstrap.Modal.getInstance(document.getElementById('imageUploadModal'));
modal.hide();
});
});
} catch (error) {
console.error('Error loading images:', error);
document.getElementById('uploadedImagesGrid').innerHTML =
'<div class="col-12 text-center text-danger">Error loading images</div>';
}
}
// Hàm select existing image
function selectExistingImage(path, url) {
const activeTab = document.querySelector('.tab-pane.active');
let targetInput = null;
if (activeTab.id === 'hero') {
targetInput = document.getElementById('heroBackgroundImage');
if (targetInput) {
targetInput.value = path;
// Update preview
const previewDiv = document.getElementById('heroImagePreview');
let previewImg = document.getElementById('heroPreviewImg');
if (!previewImg) {
previewDiv.innerHTML = `
<img src="${url}" class="img-thumbnail" id="heroPreviewImg"
style="height: 300px; width: 100%; object-fit: cover;"
alt="Background image preview">
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewImg.src = url;
previewImg.style.display = 'block';
if (previewImg.nextElementSibling) {
previewImg.nextElementSibling.style.display = 'none';
}
}
}
}
}
// Hàm upload image mới
async function uploadImage(file, imageType, targetInput) {
try {
const formData = new FormData();
formData.append('image', file);
formData.append('imageType', imageType);
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(imageType)}`, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Upload failed');
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
// Update input and preview
const input = document.getElementById(targetInput);
if (input) {
input.value = result.path;
updateImagePreview(result.path, targetInput);
}
showToast('Success', 'Image uploaded successfully', 'success');
} catch (error) {
console.error('Upload error:', error);
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
}
}
// Hàm update preview
function updateImagePreview(imagePath, targetInput) {
if (!imagePath) return;
// Construct full URL
const baseUrl = window.location.origin;
let imageUrl = imagePath;
if (!imagePath.startsWith('http://') && !imagePath.startsWith('https://')) {
imageUrl = imagePath.startsWith('/') ? baseUrl + imagePath : baseUrl + '/' + imagePath;
}
if (targetInput === 'heroBackgroundImage') {
const previewDiv = document.getElementById('heroImagePreview');
if (!previewDiv) return;
let previewImg = document.getElementById('heroPreviewImg');
if (previewImg) {
previewImg.src = imageUrl;
previewImg.style.display = 'block';
const fallbackDiv = previewImg.nextElementSibling;
if (fallbackDiv) fallbackDiv.style.display = 'none';
} else {
previewDiv.innerHTML = `
<img src="${imageUrl}" class="img-thumbnail" id="heroPreviewImg"
style="height: 300px; width: 100%; object-fit: cover;"
alt="Background image preview">
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
updateAllJsonInputs(originalFormData);
location.reload();
}
}
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}:</strong> ${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
}
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, {
animation: true,
autohide: true,
delay: 3000
});
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
function updateAllJsonInputs(data) {
document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
document.getElementById('sidebarNavJson').value = JSON.stringify(data.sidebarNav || []);
document.getElementById('contactBoxJson').value = JSON.stringify(data.contactBox || {});
document.getElementById('faqSectionsJson').value = JSON.stringify(data.faqSections || []);
document.getElementById('videoJson').value = JSON.stringify(data.video || {});
}
// Sidebar Navigation Functions
function addSidebarItem() {
const container = document.getElementById('sidebarNavContainer');
const index = sidebarIdCounter++;
const html = `
<div class="card mb-3 sidebar-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID (Unique)</label>
<input type="text" class="form-control sidebar-id-input"
name="sidebarId_${index}"
value=""
placeholder="e.g., general-information"
oninput="updateSidebarId(this)">
</div>
<div class="col-md-6">
<label class="form-label">Label</label>
<input type="text" class="form-control"
name="sidebarLabel_${index}"
value=""
placeholder="e.g., General Information">
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeSidebarItem(this)">
<i class="fas fa-trash me-2"></i>Remove Item
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeSidebarItem(button) {
button.closest('.sidebar-item').remove();
}
function updateSidebarId(input) {
// Auto-format ID: lowercase, replace spaces with hyphens, remove special chars
let value = input.value.trim();
value = value.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
input.value = value;
}
// FAQ Sections Functions
function addFaqSection() {
const container = document.getElementById('faqSectionsContainer');
const sectionIndex = sectionIdCounter++;
const html = `
<div class="card mb-4 faq-section-item" data-section-index="${sectionIndex}">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-medium">Section ${sectionIndex + 1}: Untitled</h6>
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="removeFaqSection(this)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Section ID</label>
<input type="text" class="form-control section-id-input"
name="sectionId_${sectionIndex}"
value=""
placeholder="e.g., general-information"
data-section-index="${sectionIndex}"
oninput="updateSectionId(this)">
</div>
<div class="col-md-6">
<label class="form-label">Section Title</label>
<input type="text" class="form-control section-title-input"
name="sectionTitle_${sectionIndex}"
value=""
placeholder="e.g., General Information"
data-section-index="${sectionIndex}"
oninput="updateSectionTitle(this)">
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">FAQ Items in this Section</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addFaqItem(${sectionIndex})">
<i class="fas fa-plus"></i> Add FAQ
</button>
</div>
<div class="faq-items-container" id="faqItems_${sectionIndex}">
<!-- FAQ items will be added here -->
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFaqSection(button) {
if (confirm('Are you sure you want to remove this section and all its FAQs?')) {
button.closest('.faq-section-item').remove();
}
}
function updateSectionId(input) {
// Auto-format ID: lowercase, replace spaces with hyphens, remove special chars
let value = input.value.trim();
value = value.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
input.value = value;
}
function updateSectionTitle(input) {
const sectionIndex = input.dataset.sectionIndex;
const sectionItem = document.querySelector(`.faq-section-item[data-section-index="${sectionIndex}"]`);
const headerTitle = sectionItem?.querySelector('.card-header h6');
if (headerTitle) {
const title = input.value.trim() || 'Untitled';
headerTitle.textContent = `Section ${parseInt(sectionIndex) + 1}: ${title}`;
}
}
function addFaqItem(sectionIndex) {
const container = document.getElementById(`faqItems_${sectionIndex}`);
if (!container) return;
const faqIndex = container.children.length;
const html = `
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question-input"
name="faqQuestion_${sectionIndex}_${faqIndex}"
value=""
placeholder="e.g., What are the camp dates?">
</div>
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer-input"
name="faqAnswer_${sectionIndex}_${faqIndex}"
rows="4"
placeholder="Enter detailed answer here..."></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFaqItem(this)">
<i class="fas fa-trash me-2"></i>Remove FAQ
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFaqItem(button) {
if (confirm('Are you sure you want to remove this FAQ?')) {
button.closest('.faq-item').remove();
}
}
function updateJsonData() {
try {
// Hero
const heroData = {
title: (document.getElementById('heroTitle') || {}).value?.trim() || '',
backgroundImage: (document.getElementById('heroBackgroundImage') || {}).value?.trim() || '',
overlayColor: (document.getElementById('heroOverlayColor') || {}).value?.trim() || 'rgba(0, 0, 0, 0)',
sectionClass: (document.getElementById('heroSectionClass') || {}).value?.trim() || (originalFormData?.hero?.sectionClass || ''),
titleClass: (document.getElementById('heroTitleClass') || {}).value?.trim() || (originalFormData?.hero?.titleClass || ''),
enableScrollspy: (document.getElementById('heroEnableScrollspy') || {}).value === 'true' || (originalFormData?.hero?.enableScrollspy || false),
backgroundPosition: (document.getElementById('heroBackgroundPosition') || {}).value?.trim() || (originalFormData?.hero?.backgroundPosition || 'top-center')
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Sidebar Navigation
const sidebarNavData = Array.from(document.querySelectorAll('.sidebar-item'))
.map((item) => {
const idEl = item.querySelector('[name^="sidebarId_"]');
const labelEl = item.querySelector('[name^="sidebarLabel_"]');
return {
id: (idEl?.value || '').trim(),
label: (labelEl?.value || '').trim()
};
})
.filter(item => item.id !== '' && item.label !== '');
document.getElementById('sidebarNavJson').value = JSON.stringify(sidebarNavData);
// Contact Box
const contactBoxData = {
title: (document.getElementById('contactBoxTitle') || {}).value?.trim() || '',
phone: {
icon: (document.getElementById('contactBoxPhoneIcon') || {}).value?.trim() || 'phone',
text: (document.getElementById('contactBoxPhone') || {}).value?.trim() || ''
},
email: {
icon: (document.getElementById('contactBoxEmailIcon') || {}).value?.trim() || 'envelope',
text: (document.getElementById('contactBoxEmail') || {}).value?.trim() || ''
}
};
document.getElementById('contactBoxJson').value = JSON.stringify(contactBoxData);
// FAQ Sections
const faqSectionsData = Array.from(document.querySelectorAll('.faq-section-item'))
.map((sectionItem) => {
const sectionIndex = sectionItem.dataset.sectionIndex;
// find section id/title inputs
const idEl = sectionItem.querySelector(`[name="sectionId_${sectionIndex}"]`);
const titleEl = sectionItem.querySelector(`[name="sectionTitle_${sectionIndex}"]`);
// Collect FAQ items for this section
const faqItemsContainer = sectionItem.querySelector(`#faqItems_${sectionIndex}`);
const faqItems = faqItemsContainer ?
Array.from(faqItemsContainer.querySelectorAll('.faq-item')).map((faqItem, faqIndex) => {
const questionEl = faqItem.querySelector(`[name="faqQuestion_${sectionIndex}_${faqIndex}"]`);
const answerEl = faqItem.querySelector(`[name="faqAnswer_${sectionIndex}_${faqIndex}"]`);
// Bảo toàn newlines từ textarea
const description = answerEl?.value || '';
const cleanDescription = description
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
return {
title: (questionEl?.value || '').trim(),
description: cleanDescription
};
}).filter(faq => faq.title !== '' && faq.description !== '') : [];
return {
id: (idEl?.value || '').trim(),
title: (titleEl?.value || '').trim(),
faqs: faqItems
};
})
.filter(section => section.id !== '' && section.title !== '');
document.getElementById('faqSectionsJson').value = JSON.stringify(faqSectionsData);
} catch (error) {
console.error('Error updating JSON data:', error);
throw new Error('Failed to process form data');
}
}
</script>