forked from UKSOURCE/cms.hailearning.edu.vn
1019 lines
57 KiB
Plaintext
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> |