forked from UKSOURCE/cms.hailearning.edu.vn
971 lines
54 KiB
Plaintext
971 lines
54 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 content displayed on the About page</p>
|
|
</div>
|
|
<div>
|
|
<a href="<%= frontendUrl %>/about" class="btn btn-outline-primary" target="_blank">
|
|
<i class="fas fa-external-link-alt me-2"></i>View About Page
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<form method="POST" class="content-with-fixed-buttons" id="aboutUsForm"
|
|
action="/admin/about-us/update">
|
|
<!-- Hidden inputs for JSON data -->
|
|
<input type="hidden" name="heroJson" id="heroJson">
|
|
<input type="hidden" name="introJson" id="introJson">
|
|
<input type="hidden" name="missionJson" id="missionJson">
|
|
<input type="hidden" name="featuresJson" id="featuresJson">
|
|
<input type="hidden" name="newsJson" id="newsJson">
|
|
<input type="hidden" name="activeTab" id="activeTabInput" value="<%= locals.activeTab || 'hero' %>">
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-header bg-white border-bottom">
|
|
<!-- Tab Menu -->
|
|
<ul class="nav nav-tabs card-header-tabs" id="aboutUsTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link <%= (locals.activeTab === 'hero' || !locals.activeTab) ? 'active' : '' %>"
|
|
id="hero-tab" data-bs-toggle="tab" href="#hero" role="tab"
|
|
aria-selected="true"><i class="fas fa-image me-2"></i>Hero</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link <%= locals.activeTab === 'intro' ? 'active' : '' %>" id="intro-tab"
|
|
data-bs-toggle="tab" href="#intro" role="tab"
|
|
aria-selected="false"><i class="fas fa-info-circle me-2"></i>Intro</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link <%= locals.activeTab === 'mission-vision' ? 'active' : '' %>" id="mission-vision-tab"
|
|
data-bs-toggle="tab" href="#mission-vision" role="tab"
|
|
aria-selected="false"><i class="fas fa-bullseye me-2"></i>Mission & Vision</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link <%= locals.activeTab === 'news' ? 'active' : '' %>" id="news-tab"
|
|
data-bs-toggle="tab" href="#news" role="tab"
|
|
aria-selected="false"><i class="fas fa-newspaper me-2"></i>News</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
<!-- Hero Tab -->
|
|
<div class="tab-pane fade <%= (locals.activeTab === 'hero' || !locals.activeTab) ? 'show active' : '' %>" id="hero"
|
|
role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-image me-2"></i>Hero Section</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
|
|
value="<%= data.hero?.title || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Breadcrumb (comma separated)</label>
|
|
<input type="text" class="form-control" id="heroBreadcrumb"
|
|
value="<%= (data.hero?.breadcrumb || []).join(', ') %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Background Image</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="heroBackgroundImage"
|
|
name="heroBackgroundImage" value="<%= data.hero?.backgroundImage || '' %>">
|
|
<button class="btn btn-outline-primary btn-upload-image" type="button"
|
|
data-target-input="heroBackgroundImage" data-image-type="about">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<small class="text-muted d-block mt-1">Recommended: 1920x640px wide banner (3:1). Keep the focal subject centered because the image is cropped responsively.</small>
|
|
<% if (data.hero?.backgroundImage) { %>
|
|
<img src="<%= data.hero.backgroundImage %>"
|
|
class="img-thumbnail uploaded-preview mt-2"
|
|
style="max-height: 200px;">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Intro Tab -->
|
|
<div class="tab-pane fade <%= locals.activeTab === 'intro' ? 'show active' : '' %>" id="intro"
|
|
role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-info-circle me-2"></i>Introduction</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Subheading</label>
|
|
<input type="text" class="form-control" id="introSubheading" name="introSubheading"
|
|
value="<%= data.intro?.subheading || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Heading</label>
|
|
<input type="text" class="form-control" id="introHeading" name="introHeading"
|
|
value="<%= data.intro?.heading || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="introDescription" name="introDescription"
|
|
rows="4"><%= data.intro?.description || '' %></textarea>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Main Image</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="introImage" name="introImage"
|
|
value="<%= data.intro?.image || '' %>">
|
|
<button class="btn btn-outline-primary btn-upload-image" type="button"
|
|
data-target-input="introImage" data-image-type="about">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<small class="text-muted d-block mt-1">Recommended: 1600x900px landscape image (16:9). This section uses a full-width cover frame.</small>
|
|
<% if (data.intro?.image) { %>
|
|
<img src="<%= data.intro.image %>"
|
|
class="img-thumbnail uploaded-preview mt-2"
|
|
style="max-height: 200px;">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mission Tab -->
|
|
<div class="tab-pane fade <%= locals.activeTab === 'mission-vision' ? 'show active' : '' %>" id="mission-vision" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-bullseye me-2"></i>Mission Section</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Subheading</label>
|
|
<input type="text" class="form-control" id="missionSubheading" value="<%= data.mission?.subheading || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Heading</label>
|
|
<input type="text" class="form-control" id="missionHeading" value="<%= data.mission?.heading || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="missionDescription" rows="3"><%= data.mission?.description || '' %></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mt-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label">CTA Button Label</label>
|
|
<input type="text" class="form-control" id="missionCtaLabel" value="<%= data.mission?.ctaButton?.label || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">CTA Button Link</label>
|
|
<input type="text" class="form-control" id="missionCtaHref" value="<%= data.mission?.ctaButton?.href || '' %>">
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="mt-4 mb-3">Images</h6>
|
|
<div class="row g-3">
|
|
<% ['main', 'secondary'].forEach(imgKey => { %>
|
|
<div class="col-md-4">
|
|
<label class="form-label"><%= imgKey.charAt(0).toUpperCase() + imgKey.slice(1) %> Image</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="missionImg_<%= imgKey %>" value="<%= data.mission?.images?.[imgKey] || '' %>">
|
|
<button class="btn btn-outline-primary btn-upload-image btn-sm" type="button" data-target-input="missionImg_<%= imgKey %>" data-image-type="about">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
</div>
|
|
<% if (imgKey === 'main') { %>
|
|
<small class="text-muted d-block mt-1">Recommended: 1200x1500px portrait image (4:5). This is the primary mission visual.</small>
|
|
<% } else { %>
|
|
<small class="text-muted d-block mt-1">Recommended: 1200x900px image (4:3). This image sits as the smaller overlapping card.</small>
|
|
<% } %>
|
|
</div>
|
|
<% }) %>
|
|
</div>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label mb-0">Items</label>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionItem()">
|
|
<i class="fas fa-plus me-1"></i>Add
|
|
</button>
|
|
</div>
|
|
<small class="text-muted d-block mb-2">Mission item icons are fixed by the frontend design and are not editable in CMS.</small>
|
|
<div id="missionItemsContainer"></div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label mb-0">Features (List)</label>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionFeature()">
|
|
<i class="fas fa-plus me-1"></i>Add
|
|
</button>
|
|
</div>
|
|
<div id="missionFeaturesContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Features Section</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Subheading</label>
|
|
<input type="text" class="form-control" id="featuresSubheading" value="<%= data.features?.subheading || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Heading</label>
|
|
<input type="text" class="form-control" id="featuresHeading" value="<%= data.features?.heading || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="featuresDescription" rows="3"><%= data.features?.description || '' %></textarea>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Background Image</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="featuresBgImage" value="<%= data.features?.backgroundImage || '' %>">
|
|
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featuresBgImage" data-image-type="about">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
</div>
|
|
<small class="text-muted d-block mt-1">Recommended: 1920x1080px background image (16:9). Darker imagery works best because the section adds a dark overlay.</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Side Image</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="featuresImage" value="<%= data.features?.image || '' %>">
|
|
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featuresImage" data-image-type="about">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
</div>
|
|
<small class="text-muted d-block mt-1">Recommended: 1200x1500px portrait image (4:5). The image fills a tall cover frame on desktop.</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">CTA Button Label</label>
|
|
<input type="text" class="form-control" id="featuresCtaLabel" value="<%= data.features?.ctaButton?.label || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">CTA Button Link</label>
|
|
<input type="text" class="form-control" id="featuresCtaHref" value="<%= data.features?.ctaButton?.href || '' %>">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h6 class="mb-0">Feature Items</h6>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addFeatureItem()">
|
|
<i class="fas fa-plus me-1"></i>Add Item
|
|
</button>
|
|
</div>
|
|
<small class="text-muted d-block mb-2">Custom icons render at about 28x28px inside a 60x60 card. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</small>
|
|
<div id="featureItemsContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- News Tab -->
|
|
<div class="tab-pane fade <%= locals.activeTab === 'news' ? 'show active' : '' %>" id="news" role="tabpanel">
|
|
<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-newspaper me-2"></i>News Section (Blog Preview)</h6>
|
|
<span class="badge bg-info text-dark">System will automatically fetch the 3 latest posts if no specific blog is selected.</span>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-medium">Subheading</label>
|
|
<input type="text" class="form-control" id="newsSubheading" value="<%= data.news?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-medium">Heading</label>
|
|
<input type="text" class="form-control" id="newsHeading" value="<%= data.news?.heading || '' %>" placeholder="e.g., Latest Insights & Updates">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-medium">CTA Button Label</label>
|
|
<input type="text" class="form-control" id="newsCtaLabel" value="<%= data.news?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-medium">CTA Button Link</label>
|
|
<input type="text" class="form-control" id="newsCtaHref" value="<%= data.news?.ctaButton?.href || '' %>" placeholder="/blog">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12 mt-4">
|
|
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct from Blog Module)</label>
|
|
<p class="text-muted small mb-3">Select blog posts to display on About page. If none are selected, the system will use the 3 latest posts.</p>
|
|
<div class="row g-3 blog-selector-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
|
|
<% if (allBlogs && allBlogs.length > 0) { %>
|
|
<% allBlogs.forEach(blog => {
|
|
const isSelected = data.news?.selectedBlogIds && data.news.selectedBlogIds.some(id => id.toString() === blog._id.toString());
|
|
%>
|
|
<div class="col-md-4">
|
|
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>" onclick="toggleAboutBlogSelection(this, '<%= blog._id %>')" style="cursor: pointer; transition: all 0.2s;">
|
|
<div class="position-absolute top-0 end-0 m-2">
|
|
<div class="form-check">
|
|
<input class="form-check-input about-blog-checkbox" type="checkbox" value="<%= blog._id %>" <%= isSelected ? 'checked' : '' %> onclick="event.stopPropagation(); handleAboutCheckboxChange(this)">
|
|
</div>
|
|
</div>
|
|
<img src="<%= blog.featuredImage ? (blog.featuredImage.startsWith('http') ? blog.featuredImage : (blog.featuredImage.startsWith('/') ? backendUrl + blog.featuredImage : backendUrl + '/' + blog.featuredImage)) : '/assets/img/placeholder.jpg' %>" class="card-img-top" style="height: 210px; object-fit: cover;">
|
|
<div class="card-body p-2">
|
|
<h6 class="card-title small fw-bold mb-1" title="<%= blog.title %>" style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.6em; line-height: 1.3em;">
|
|
<%= blog.title %>
|
|
</h6>
|
|
<p class="card-text tiny text-muted mb-0">
|
|
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% }) %>
|
|
<% } else { %>
|
|
<div class="col-12 text-center py-4">
|
|
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
|
|
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fixed Bottom Buttons inside Card Footer -->
|
|
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
|
|
<button type="button" class="btn btn-outline-secondary px-4" onclick="resetForm()">
|
|
<i class="fas fa-undo me-2"></i>Reset
|
|
</button>
|
|
<button type="submit" class="btn btn-outline-primary px-4" id="submitBtn">
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script id="serverAboutData" type="application/json">
|
|
<%- JSON.stringify(locals.data || {}) %>
|
|
</script>
|
|
|
|
<script>
|
|
let originalFormData = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Lấy nội dung text từ thẻ script ẩn và parse nó thành Object
|
|
const dataElement = document.getElementById('serverAboutData');
|
|
|
|
try {
|
|
originalFormData = JSON.parse(dataElement.textContent || '{}');
|
|
} catch (error) {
|
|
console.error('Lỗi khi parse dữ liệu từ server:', error);
|
|
originalFormData = {}; // Giá trị mặc định an toàn nếu parse xịt
|
|
}
|
|
|
|
updateAllJsonInputs(originalFormData);
|
|
initializeFormHandlers();
|
|
});
|
|
|
|
function initializeFormHandlers() {
|
|
const form = document.getElementById('aboutUsForm');
|
|
form.addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const originalHtml = submitBtn.innerHTML;
|
|
|
|
try {
|
|
// Collect all data into a single object
|
|
updateJsonData();
|
|
// No more "Saving..." text or disabling button to keep UI "instant"
|
|
// submitBtn.disabled = true;
|
|
// submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
|
|
|
// We'll send the individual section JSONs or construct one big one
|
|
const aboutData = {
|
|
hero: JSON.parse(document.getElementById('heroJson').value),
|
|
intro: JSON.parse(document.getElementById('introJson').value),
|
|
mission: JSON.parse(document.getElementById('missionJson').value),
|
|
features: JSON.parse(document.getElementById('featuresJson').value),
|
|
news: JSON.parse(document.getElementById('newsJson').value)
|
|
};
|
|
|
|
const response = await fetch('/api/about', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ aboutJson: JSON.stringify(aboutData) })
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Update failed');
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showToast('Success', 'About page updated successfully', 'success');
|
|
|
|
// Update the local state with returned data from server
|
|
// This ensures the UI is in sync with what was actually saved
|
|
if (result.data) {
|
|
originalFormData = result.data;
|
|
updateAllJsonInputs(originalFormData);
|
|
}
|
|
} else {
|
|
throw new Error(result.error || 'Failed to update');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
showToast('Error', error.message, 'error');
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener('click', function (e) {
|
|
const uploadBtn = e.target.closest('.btn-upload-image');
|
|
if (uploadBtn) {
|
|
const targetInput = uploadBtn.dataset.targetInput;
|
|
const imageType = uploadBtn.dataset.imageType;
|
|
openImageUploader(targetInput, imageType);
|
|
}
|
|
});
|
|
|
|
// Tab change listener to keep track of active tab
|
|
const tabs = document.querySelectorAll('a[data-bs-toggle="tab"]');
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('shown.bs.tab', function (e) {
|
|
const targetId = e.target.getAttribute('href').replace('#', '');
|
|
const activeTabInput = document.getElementById('activeTabInput');
|
|
if (activeTabInput) {
|
|
activeTabInput.value = targetId;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function openImageUploader(targetInput, imageType) {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = 'image/*';
|
|
fileInput.style.display = 'none';
|
|
document.body.appendChild(fileInput);
|
|
|
|
function getPreviewDims(name) {
|
|
if (/hero/i.test(name)) return { h: '250px', w: '100%' };
|
|
if (/intro/i.test(name) || /mission/i.test(name) || /features/i.test(name)) return { h: '150px', w: '100%' };
|
|
return { h: '120px', w: '100%' };
|
|
}
|
|
|
|
fileInput.onchange = async function (e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
// Disable upload button during upload
|
|
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
|
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = true;
|
|
}
|
|
|
|
const response = await fetch(`/admin/upload/image?imageType=${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 value
|
|
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
|
if (!input) {
|
|
throw new Error('Target input not found');
|
|
}
|
|
|
|
// Use absolute URL for preview when necessary
|
|
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://'))) ? result.path : (window.location.origin + result.path);
|
|
|
|
input.value = result.path;
|
|
|
|
// Try to find an existing image preview inside the closest card or input group (prefer .uploaded-preview)
|
|
let card = input.closest('.card');
|
|
let previewImg = card ? card.querySelector('.uploaded-preview') : null;
|
|
|
|
if (!previewImg) {
|
|
// Look for a preview right after the input group
|
|
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
|
|
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
|
|
}
|
|
|
|
const dims = getPreviewDims(targetInput);
|
|
|
|
if (previewImg) {
|
|
previewImg.src = previewUrl;
|
|
previewImg.style.height = dims.h;
|
|
previewImg.style.width = dims.w;
|
|
} else {
|
|
// Create a preview image and attach it after the input group
|
|
const img = document.createElement('img');
|
|
img.src = previewUrl;
|
|
img.className = 'img-thumbnail uploaded-preview mt-2';
|
|
img.style.height = dims.h;
|
|
img.style.width = dims.w;
|
|
img.style.objectFit = 'cover';
|
|
img.alt = 'Image preview';
|
|
|
|
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
|
|
if (parent) parent.appendChild(img);
|
|
}
|
|
|
|
// Removed toast for silent upload
|
|
|
|
// Restore button state
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
|
|
|
|
// Restore button state
|
|
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = false;
|
|
}
|
|
} finally {
|
|
document.body.removeChild(fileInput);
|
|
}
|
|
};
|
|
|
|
fileInput.click();
|
|
}
|
|
|
|
function resetForm() {
|
|
if (confirm('Are you sure you want to reset all changes?')) {
|
|
updateAllJsonInputs(originalFormData);
|
|
showToast('Reset', 'Form restored to last saved state', 'info');
|
|
}
|
|
}
|
|
|
|
function showError(message) {
|
|
const alertHtml = `
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
`;
|
|
document.querySelector('.container').insertAdjacentHTML('afterbegin', alertHtml);
|
|
}
|
|
|
|
// Show toast message
|
|
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>
|
|
`;
|
|
|
|
// Add toast to container
|
|
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);
|
|
|
|
// Show toast
|
|
const bsToast = new bootstrap.Toast(toast, {
|
|
animation: true,
|
|
autohide: true,
|
|
delay: 3000
|
|
});
|
|
bsToast.show();
|
|
|
|
// Remove toast after hide
|
|
toast.addEventListener('hidden.bs.toast', () => {
|
|
toast.remove();
|
|
});
|
|
}
|
|
|
|
function updateAllJsonInputs(data) {
|
|
if (!data) return;
|
|
|
|
// 1. Hero
|
|
const hero = data.hero || {};
|
|
document.getElementById('heroJson').value = JSON.stringify(hero);
|
|
document.getElementById('heroTitle').value = hero.title || '';
|
|
document.getElementById('heroBreadcrumb').value = (hero.breadcrumb || []).join(', ');
|
|
document.getElementById('heroBackgroundImage').value = hero.backgroundImage || '';
|
|
updateImagePreview('heroBackgroundImage', hero.backgroundImage);
|
|
|
|
// 2. Intro
|
|
const intro = data.intro || {};
|
|
document.getElementById('introJson').value = JSON.stringify(intro);
|
|
document.getElementById('introSubheading').value = intro.subheading || '';
|
|
document.getElementById('introHeading').value = intro.heading || '';
|
|
document.getElementById('introDescription').value = intro.description || '';
|
|
document.getElementById('introImage').value = intro.image || '';
|
|
updateImagePreview('introImage', intro.image);
|
|
|
|
// 3. Mission
|
|
const mission = data.mission || {};
|
|
document.getElementById('missionJson').value = JSON.stringify(mission);
|
|
document.getElementById('missionSubheading').value = mission.subheading || '';
|
|
document.getElementById('missionHeading').value = mission.heading || '';
|
|
document.getElementById('missionDescription').value = mission.description || '';
|
|
document.getElementById('missionCtaLabel').value = mission.ctaButton?.label || '';
|
|
document.getElementById('missionCtaHref').value = mission.ctaButton?.href || '';
|
|
|
|
['main', 'secondary'].forEach(k => {
|
|
const el = document.getElementById('missionImg_' + k);
|
|
const val = mission.images?.[k] || '';
|
|
if (el) {
|
|
el.value = val;
|
|
updateImagePreview('missionImg_' + k, val);
|
|
}
|
|
});
|
|
populateMissionItems(mission.items || []);
|
|
populateMissionFeatures(mission.features || []);
|
|
|
|
// 4. Features
|
|
const features = data.features || {};
|
|
document.getElementById('featuresJson').value = JSON.stringify(features);
|
|
document.getElementById('featuresSubheading').value = features.subheading || '';
|
|
document.getElementById('featuresHeading').value = features.heading || '';
|
|
document.getElementById('featuresDescription').value = features.description || '';
|
|
document.getElementById('featuresBgImage').value = features.backgroundImage || '';
|
|
document.getElementById('featuresImage').value = features.image || '';
|
|
document.getElementById('featuresCtaLabel').value = features.ctaButton?.label || '';
|
|
document.getElementById('featuresCtaHref').value = features.ctaButton?.href || '';
|
|
updateImagePreview('featuresBgImage', features.backgroundImage);
|
|
updateImagePreview('featuresImage', features.image);
|
|
populateFeatureItems(features.items || []);
|
|
|
|
// 5. News
|
|
const news = data.news || {};
|
|
document.getElementById('newsJson').value = JSON.stringify(news);
|
|
document.getElementById('newsSubheading').value = news.subheading || '';
|
|
document.getElementById('newsHeading').value = news.heading || '';
|
|
document.getElementById('newsCtaLabel').value = news.ctaButton?.label || '';
|
|
document.getElementById('newsCtaHref').value = news.ctaButton?.href || '';
|
|
|
|
// Update blog selection checkboxes
|
|
document.querySelectorAll('.about-blog-checkbox').forEach(cb => {
|
|
const isSelected = news.selectedBlogIds && news.selectedBlogIds.some(id => id.toString() === cb.value);
|
|
cb.checked = isSelected;
|
|
const card = cb.closest('.blog-select-card');
|
|
if (card) {
|
|
handleAboutCheckboxUpdate(card, isSelected);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateImagePreview(inputId, imagePath) {
|
|
if (!imagePath) return;
|
|
const input = document.getElementById(inputId);
|
|
if (!input) return;
|
|
|
|
let card = input.closest('.card');
|
|
let previewImg = card ? card.querySelector('.uploaded-preview') : null;
|
|
|
|
if (!previewImg) {
|
|
const parent = input.closest('.input-group') || input.parentElement;
|
|
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
|
|
}
|
|
|
|
if (previewImg) {
|
|
previewImg.src = imagePath;
|
|
}
|
|
}
|
|
|
|
// --- Helper dynamic populations ---
|
|
|
|
function addMissionItem() {
|
|
const container = document.getElementById('missionItemsContainer');
|
|
const idx = container.children.length;
|
|
const html = `
|
|
<div class="card mb-2 mission-item">
|
|
<div class="card-body p-2">
|
|
<div class="row g-2">
|
|
<div class="col-md-5">
|
|
<input type="text" class="form-control form-control-sm" name="missionItemLabel_${idx}" placeholder="Label">
|
|
</div>
|
|
<div class="col-md-7">
|
|
<input type="text" class="form-control form-control-sm" name="missionItemDesc_${idx}" placeholder="Description">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.mission-item').remove()">Remove</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function populateMissionItems(items) {
|
|
const container = document.getElementById('missionItemsContainer');
|
|
container.innerHTML = '';
|
|
items.forEach((item) => {
|
|
addMissionItem();
|
|
const last = container.lastElementChild;
|
|
last.querySelector(`[name^="missionItemLabel_"]`).value = item.label || item.title || '';
|
|
last.querySelector(`[name^="missionItemDesc_"]`).value = item.description || '';
|
|
});
|
|
}
|
|
|
|
function addMissionFeature() {
|
|
const container = document.getElementById('missionFeaturesContainer');
|
|
const html = `
|
|
<div class="input-group input-group-sm mb-1">
|
|
<input type="text" class="form-control mission-feature-input" placeholder="Feature text">
|
|
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()">x</button>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function populateMissionFeatures(features) {
|
|
const container = document.getElementById('missionFeaturesContainer');
|
|
container.innerHTML = '';
|
|
features.forEach(f => {
|
|
addMissionFeature();
|
|
container.lastElementChild.querySelector('input').value = f || '';
|
|
});
|
|
}
|
|
|
|
function addFeatureItem() {
|
|
const container = document.getElementById('featureItemsContainer');
|
|
const idx = container.children.length;
|
|
const html = `
|
|
<div class="card mb-2 feature-item-row">
|
|
<div class="card-body p-2">
|
|
<div class="row g-2">
|
|
<div class="col-md-4">
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control" name="featureItemIcon_${idx}" placeholder="Icon path">
|
|
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featureItemIcon_${idx}" data-image-type="about">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control form-control-sm" name="featureItemTitle_${idx}" placeholder="Title">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control form-control-sm" name="featureItemDesc_${idx}" placeholder="Description">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.feature-item-row').remove()">Remove</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function populateFeatureItems(items) {
|
|
const container = document.getElementById('featureItemsContainer');
|
|
container.innerHTML = '';
|
|
items.forEach((item, i) => {
|
|
addFeatureItem();
|
|
const last = container.lastElementChild;
|
|
last.querySelector(`[name="featureItemIcon_${i}"]`).value = item.icon || '';
|
|
last.querySelector(`[name="featureItemTitle_${i}"]`).value = item.title || '';
|
|
last.querySelector(`[name="featureItemDesc_${i}"]`).value = item.description || '';
|
|
});
|
|
}
|
|
|
|
// Blog selection functions for About News section
|
|
function toggleAboutBlogSelection(card, blogId) {
|
|
const checkbox = card.querySelector('.about-blog-checkbox');
|
|
const isChecking = !checkbox.checked;
|
|
|
|
if (isChecking) {
|
|
const checkedCount = document.querySelectorAll('.about-blog-checkbox:checked').length;
|
|
if (checkedCount >= 3) {
|
|
alert('You can only select up to 3 blogs.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
checkbox.checked = isChecking;
|
|
handleAboutCheckboxUpdate(card, checkbox.checked);
|
|
}
|
|
|
|
function handleAboutCheckboxChange(checkbox) {
|
|
if (checkbox.checked) {
|
|
const checkedCount = document.querySelectorAll('.about-blog-checkbox:checked').length;
|
|
if (checkedCount > 3) {
|
|
checkbox.checked = false;
|
|
alert('You can only select up to 3 blogs.');
|
|
return;
|
|
}
|
|
}
|
|
const card = checkbox.closest('.blog-select-card');
|
|
handleAboutCheckboxUpdate(card, checkbox.checked);
|
|
}
|
|
|
|
function handleAboutCheckboxUpdate(card, isChecked) {
|
|
if (isChecked) {
|
|
card.classList.add('border-primary', 'bg-light');
|
|
} else {
|
|
card.classList.remove('border-primary', 'bg-light');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function addService() {
|
|
const container = document.getElementById('servicesContainer');
|
|
const index = container.children.length;
|
|
const html = `
|
|
<div class="card mb-3 service-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" name="serviceTitle_${index}">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" name="serviceDescription_${index}" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeService(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Service
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
|
|
function updateJsonData() {
|
|
try {
|
|
// Hero
|
|
const heroData = {
|
|
title: document.getElementById('heroTitle').value.trim(),
|
|
breadcrumb: document.getElementById('heroBreadcrumb').value.split(',').map(s => s.trim()).filter(s => s !== ''),
|
|
backgroundImage: document.getElementById('heroBackgroundImage').value.trim()
|
|
};
|
|
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
|
|
|
// Intro
|
|
const introData = {
|
|
subheading: document.getElementById('introSubheading').value.trim(),
|
|
heading: document.getElementById('introHeading').value.trim(),
|
|
description: document.getElementById('introDescription').value.trim(),
|
|
image: document.getElementById('introImage').value.trim()
|
|
};
|
|
document.getElementById('introJson').value = JSON.stringify(introData);
|
|
|
|
// Mission
|
|
const missionData = {
|
|
subheading: document.getElementById('missionSubheading').value.trim(),
|
|
heading: document.getElementById('missionHeading').value.trim(),
|
|
description: document.getElementById('missionDescription').value.trim(),
|
|
images: {
|
|
main: document.getElementById('missionImg_main').value.trim(),
|
|
secondary: document.getElementById('missionImg_secondary').value.trim()
|
|
},
|
|
items: Array.from(document.querySelectorAll('.mission-item')).map(item => ({
|
|
icon: '/assets/img/home-1/icon/01.svg',
|
|
label: item.querySelector('[name^="missionItemLabel_"]').value.trim(),
|
|
description: item.querySelector('[name^="missionItemDesc_"]').value.trim()
|
|
})).filter(i => i.label !== ''),
|
|
features: Array.from(document.querySelectorAll('.mission-feature-input')).map(input => input.value.trim()).filter(v => v !== ''),
|
|
ctaButton: {
|
|
label: document.getElementById('missionCtaLabel').value.trim(),
|
|
href: document.getElementById('missionCtaHref').value.trim()
|
|
}
|
|
};
|
|
document.getElementById('missionJson').value = JSON.stringify(missionData);
|
|
|
|
// Features
|
|
const featuresData = {
|
|
backgroundImage: document.getElementById('featuresBgImage').value.trim(),
|
|
subheading: document.getElementById('featuresSubheading').value.trim(),
|
|
heading: document.getElementById('featuresHeading').value.trim(),
|
|
description: document.getElementById('featuresDescription').value.trim(),
|
|
image: document.getElementById('featuresImage').value.trim(),
|
|
items: Array.from(document.querySelectorAll('.feature-item-row')).map(item => ({
|
|
icon: item.querySelector('[name^="featureItemIcon_"]').value.trim(),
|
|
title: item.querySelector('[name^="featureItemTitle_"]').value.trim(),
|
|
description: item.querySelector('[name^="featureItemDesc_"]').value.trim()
|
|
})).filter(i => i.title !== ''),
|
|
ctaButton: {
|
|
label: document.getElementById('featuresCtaLabel').value.trim(),
|
|
href: document.getElementById('featuresCtaHref').value.trim()
|
|
}
|
|
};
|
|
document.getElementById('featuresJson').value = JSON.stringify(featuresData);
|
|
|
|
// News
|
|
const selectedIds = [];
|
|
document.querySelectorAll('.about-blog-checkbox:checked').forEach(cb => {
|
|
selectedIds.push(cb.value);
|
|
});
|
|
|
|
const newsData = {
|
|
subheading: document.getElementById('newsSubheading').value.trim(),
|
|
heading: document.getElementById('newsHeading').value.trim(),
|
|
ctaButton: {
|
|
label: document.getElementById('newsCtaLabel').value.trim(),
|
|
href: document.getElementById('newsCtaHref').value.trim()
|
|
},
|
|
selectedBlogIds: selectedIds,
|
|
items: [] // Server will populate this from selectedBlogIds
|
|
};
|
|
document.getElementById('newsJson').value = JSON.stringify(newsData);
|
|
|
|
} catch (error) {
|
|
console.error('Error updating JSON data:', error);
|
|
throw new Error('Failed to process form data');
|
|
}
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
<style>
|
|
.tiny {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.blog-select-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
</style>
|