forked from UKSOURCE/cms.lams
Refactor Homepage, About, Footer (Controller,Model, View, Data), Update: Dashboard, Delete old file
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
<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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" id="aboutForm" action="/admin/about-us/update">
|
||||
<!-- Hidden JSON inputs -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="leadership" id="leadershipJson">
|
||||
<input type="hidden" name="learningModel" id="learningModelJson">
|
||||
<input type="hidden" name="accreditation" id="accreditationJson">
|
||||
<input type="hidden" name="successStories" id="successStoriesJson">
|
||||
<input type="hidden" name="cta" id="ctaJson">
|
||||
|
||||
<!-- 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" id="aboutTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#tab-hero-pane" role="tab"><i
|
||||
class="fas fa-image me-2"></i>Hero</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-leadership-pane" role="tab"><i
|
||||
class="fas fa-users me-2"></i>Leadership</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-learningModel-pane" role="tab"><i
|
||||
class="fas fa-chalkboard-teacher me-2"></i>Learning Model</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-accreditation-pane" role="tab"><i
|
||||
class="fas fa-award me-2"></i>Accreditation</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-successStories-pane" role="tab"><i
|
||||
class="fas fa-quote-left me-2"></i>Success Stories</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-cta-pane" role="tab"><i
|
||||
class="fas fa-bullhorn me-2"></i>CTA</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
|
||||
<!-- ===== HERO TAB ===== -->
|
||||
<div class="tab-pane fade show active" id="tab-hero-pane" 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-4">
|
||||
<label class="form-label">Badge</label>
|
||||
<input type="text" class="form-control" id="heroBadge"
|
||||
value="<%= data.hero?.badge || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Student Count</label>
|
||||
<input type="text" class="form-control" id="heroStudentCount"
|
||||
value="<%= data.hero?.studentCount || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Image Alt Text</label>
|
||||
<input type="text" class="form-control" id="heroImageAlt"
|
||||
value="<%= data.hero?.imageAlt || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="heroDescription"
|
||||
rows="3"><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Image URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroImage"
|
||||
value="<%= data.hero?.image || '' %>">
|
||||
<button class="btn btn-outline-primary" type="button"
|
||||
onclick="openImageUploader('heroImage', 'about')"><i
|
||||
class="fas fa-upload me-1"></i>Upload</button>
|
||||
</div>
|
||||
<% if (data.hero?.image) { %>
|
||||
<img src="<%= data.hero.image %>"
|
||||
class="img-thumbnail uploaded-preview mt-2"
|
||||
style="max-height:180px;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Core Values</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addCoreValue()"><i class="fas fa-plus me-1"></i>Add
|
||||
Value</button>
|
||||
</div>
|
||||
<div id="coreValuesContainer">
|
||||
<% (data.hero?.coreValues || []).forEach(val=> { %>
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control core-value-input"
|
||||
value="<%= val %>">
|
||||
<button class="btn btn-outline-danger" type="button"
|
||||
onclick="this.parentElement.remove()"><i
|
||||
class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LEADERSHIP TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-leadership-pane" 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-users me-2"></i>Leadership</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addLeadershipMember()"><i class="fas fa-plus me-1"></i>Add
|
||||
Member</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="leadershipHeading"
|
||||
value="<%= data.leadership?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="leadershipDescription"
|
||||
rows="2"><%= data.leadership?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="leadershipMembersContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LEARNING MODEL TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-learningModel-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-chalkboard-teacher me-2"></i>Learning Model
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="lmHeading"
|
||||
value="<%= data.learningModel?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="lmDescription"
|
||||
rows="2"><%= data.learningModel?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Async -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border">
|
||||
<div
|
||||
class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Asynchronous Learning</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addLearningFeature('async')"><i
|
||||
class="fas fa-plus me-1"></i>Add Feature</button>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="asyncTitle"
|
||||
value="<%= data.learningModel?.async?.title || '' %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Description</label>
|
||||
<textarea class="form-control form-control-sm"
|
||||
id="asyncDescription"
|
||||
rows="2"><%= data.learningModel?.async?.description || '' %></textarea>
|
||||
</div>
|
||||
<div id="asyncFeaturesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sync -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border">
|
||||
<div
|
||||
class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Synchronous Learning</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addLearningFeature('sync')"><i
|
||||
class="fas fa-plus me-1"></i>Add Feature</button>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="syncTitle"
|
||||
value="<%= data.learningModel?.sync?.title || '' %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Description</label>
|
||||
<textarea class="form-control form-control-sm"
|
||||
id="syncDescription"
|
||||
rows="2"><%= data.learningModel?.sync?.description || '' %></textarea>
|
||||
</div>
|
||||
<div id="syncFeaturesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== ACCREDITATION TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-accreditation-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-award me-2"></i>Accreditation</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="accHeading"
|
||||
value="<%= data.accreditation?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="accDescription"
|
||||
rows="2"><%= data.accreditation?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Badges</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addAccBadge()"><i class="fas fa-plus me-1"></i>Add
|
||||
Badge</button>
|
||||
</div>
|
||||
<div id="accBadgesContainer"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Stats</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addAccStat()"><i class="fas fa-plus me-1"></i>Add
|
||||
Stat</button>
|
||||
</div>
|
||||
<div id="accStatsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== SUCCESS STORIES TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-successStories-pane" 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-quote-left me-2"></i>Success Stories</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addStory()"><i class="fas fa-plus me-1"></i>Add Story</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="ssHeading"
|
||||
value="<%= data.successStories?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="ssDescription"
|
||||
rows="2"><%= data.successStories?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="storiesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== CTA TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-cta-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-bullhorn me-2"></i>Call to Action</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="ctaHeading"
|
||||
value="<%= data.cta?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="ctaDescription"
|
||||
rows="2"><%= data.cta?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Primary Button Label</label>
|
||||
<input type="text" class="form-control" id="ctaPrimaryLabel"
|
||||
value="<%= data.cta?.primaryButton?.label || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Primary Button Href</label>
|
||||
<input type="text" class="form-control" id="ctaPrimaryHref"
|
||||
value="<%= data.cta?.primaryButton?.href || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Secondary Button Label</label>
|
||||
<input type="text" class="form-control" id="ctaSecondaryLabel"
|
||||
value="<%= data.cta?.secondaryButton?.label || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Secondary Button Href</label>
|
||||
<input type="text" class="form-control" id="ctaSecondaryHref"
|
||||
value="<%= data.cta?.secondaryButton?.href || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.tab-content -->
|
||||
</div><!-- /.card-body -->
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
populateAll(originalFormData);
|
||||
|
||||
document.getElementById('aboutForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
serializeAll();
|
||||
this.submit();
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-upload-image');
|
||||
if (btn) openImageUploader(btn.dataset.targetInput, btn.dataset.imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Populate all sections ────────────────────────────────────────────────
|
||||
function populateAll(data) {
|
||||
if (!data) return;
|
||||
|
||||
// Hero
|
||||
const h = data.hero || {};
|
||||
setVal('heroBadge', h.badge);
|
||||
setVal('heroTitle', h.title);
|
||||
setVal('heroDescription', h.description);
|
||||
setVal('heroStudentCount', h.studentCount);
|
||||
setVal('heroImage', h.image);
|
||||
setVal('heroImageAlt', h.imageAlt);
|
||||
updateImagePreview('heroImage', h.image);
|
||||
populateCoreValues(h.coreValues || []);
|
||||
|
||||
// Leadership
|
||||
const l = data.leadership || {};
|
||||
setVal('leadershipHeading', l.heading);
|
||||
setVal('leadershipDescription', l.description);
|
||||
populateLeadershipMembers(l.members || []);
|
||||
|
||||
// Learning Model
|
||||
const lm = data.learningModel || {};
|
||||
setVal('lmHeading', lm.heading);
|
||||
setVal('lmDescription', lm.description);
|
||||
setVal('asyncTitle', lm.async?.title);
|
||||
setVal('asyncDescription', lm.async?.description);
|
||||
setVal('syncTitle', lm.sync?.title);
|
||||
setVal('syncDescription', lm.sync?.description);
|
||||
populateLearningFeatures('async', lm.async?.features || []);
|
||||
populateLearningFeatures('sync', lm.sync?.features || []);
|
||||
|
||||
// Accreditation
|
||||
const acc = data.accreditation || {};
|
||||
setVal('accHeading', acc.heading);
|
||||
setVal('accDescription', acc.description);
|
||||
populateAccBadges(acc.badges || []);
|
||||
populateAccStats(acc.stats || []);
|
||||
|
||||
// Success Stories
|
||||
const ss = data.successStories || {};
|
||||
setVal('ssHeading', ss.heading);
|
||||
setVal('ssDescription', ss.description);
|
||||
populateStories(ss.stories || []);
|
||||
|
||||
// CTA
|
||||
const cta = data.cta || {};
|
||||
setVal('ctaHeading', cta.heading);
|
||||
setVal('ctaDescription', cta.description);
|
||||
setVal('ctaPrimaryLabel', cta.primaryButton?.label);
|
||||
setVal('ctaPrimaryHref', cta.primaryButton?.href);
|
||||
setVal('ctaSecondaryLabel', cta.secondaryButton?.label);
|
||||
setVal('ctaSecondaryHref', cta.secondaryButton?.href);
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = val || '';
|
||||
}
|
||||
|
||||
// ── Serialize all sections into hidden JSON inputs ───────────────────────
|
||||
function serializeAll() {
|
||||
// Hero
|
||||
document.getElementById('heroJson').value = JSON.stringify({
|
||||
badge: v('heroBadge'),
|
||||
title: v('heroTitle'),
|
||||
description: v('heroDescription'),
|
||||
studentCount: v('heroStudentCount'),
|
||||
image: v('heroImage'),
|
||||
imageAlt: v('heroImageAlt'),
|
||||
coreValues: Array.from(document.querySelectorAll('.core-value-input')).map(i => i.value.trim()).filter(Boolean)
|
||||
});
|
||||
|
||||
// Leadership
|
||||
document.getElementById('leadershipJson').value = JSON.stringify({
|
||||
heading: v('leadershipHeading'),
|
||||
description: v('leadershipDescription'),
|
||||
members: Array.from(document.querySelectorAll('.leadership-member')).map(row => ({
|
||||
name: row.querySelector('[data-field="name"]').value.trim(),
|
||||
role: row.querySelector('[data-field="role"]').value.trim(),
|
||||
bio: row.querySelector('[data-field="bio"]').value.trim(),
|
||||
avatar: row.querySelector('[data-field="avatar"]').value.trim(),
|
||||
social: {
|
||||
linkedin: row.querySelector('[data-field="linkedin"]').value.trim(),
|
||||
twitter: row.querySelector('[data-field="twitter"]').value.trim()
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
// Learning Model
|
||||
document.getElementById('learningModelJson').value = JSON.stringify({
|
||||
heading: v('lmHeading'),
|
||||
description: v('lmDescription'),
|
||||
async: {
|
||||
title: v('asyncTitle'),
|
||||
description: v('asyncDescription'),
|
||||
features: collectLearningFeatures('async')
|
||||
},
|
||||
sync: {
|
||||
title: v('syncTitle'),
|
||||
description: v('syncDescription'),
|
||||
features: collectLearningFeatures('sync')
|
||||
}
|
||||
});
|
||||
|
||||
// Accreditation
|
||||
document.getElementById('accreditationJson').value = JSON.stringify({
|
||||
heading: v('accHeading'),
|
||||
description: v('accDescription'),
|
||||
badges: Array.from(document.querySelectorAll('.acc-badge-item')).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
desc: row.querySelector('[data-field="desc"]').value.trim()
|
||||
})),
|
||||
stats: Array.from(document.querySelectorAll('.acc-stat-item')).map(row => ({
|
||||
value: row.querySelector('[data-field="value"]').value.trim(),
|
||||
label: row.querySelector('[data-field="label"]').value.trim(),
|
||||
isPrimary: row.querySelector('[data-field="isPrimary"]').checked
|
||||
}))
|
||||
});
|
||||
|
||||
// Success Stories
|
||||
document.getElementById('successStoriesJson').value = JSON.stringify({
|
||||
heading: v('ssHeading'),
|
||||
description: v('ssDescription'),
|
||||
stories: Array.from(document.querySelectorAll('.story-item')).map(row => ({
|
||||
quote: row.querySelector('[data-field="quote"]').value.trim(),
|
||||
name: row.querySelector('[data-field="name"]').value.trim(),
|
||||
program: row.querySelector('[data-field="program"]').value.trim(),
|
||||
avatar: row.querySelector('[data-field="avatar"]').value.trim()
|
||||
}))
|
||||
});
|
||||
|
||||
// CTA
|
||||
document.getElementById('ctaJson').value = JSON.stringify({
|
||||
heading: v('ctaHeading'),
|
||||
description: v('ctaDescription'),
|
||||
primaryButton: { label: v('ctaPrimaryLabel'), href: v('ctaPrimaryHref') },
|
||||
secondaryButton: { label: v('ctaSecondaryLabel'), href: v('ctaSecondaryHref') }
|
||||
});
|
||||
}
|
||||
|
||||
function v(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
// ── Core Values ──────────────────────────────────────────────────────────
|
||||
function addCoreValue(val = '') {
|
||||
const c = document.getElementById('coreValuesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control core-value-input" value="${esc(val)}" placeholder="e.g. Radical Affordability">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateCoreValues(values) {
|
||||
document.getElementById('coreValuesContainer').innerHTML = '';
|
||||
values.forEach(v => addCoreValue(v));
|
||||
}
|
||||
|
||||
// ── Leadership Members ───────────────────────────────────────────────────
|
||||
function addLeadershipMember(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('leadershipMembersContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-3 leadership-member">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="name" value="${esc(item.name)}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Role</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="role" value="${esc(item.role)}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Bio</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="bio" value="${esc(item.bio)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Avatar URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="avatar" id="memberAvatar_${idx}" value="${esc(item.avatar)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="memberAvatar_${idx}" data-image-type="about"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">LinkedIn URL</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="linkedin" value="${esc(item.social?.linkedin)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Twitter URL</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="twitter" value="${esc(item.social?.twitter)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.leadership-member').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateLeadershipMembers(members) {
|
||||
document.getElementById('leadershipMembersContainer').innerHTML = '';
|
||||
members.forEach(m => addLeadershipMember(m));
|
||||
}
|
||||
|
||||
// ── Learning Features ────────────────────────────────────────────────────
|
||||
function addLearningFeature(mode, item = {}) {
|
||||
const c = document.getElementById(mode + 'FeaturesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-2 lf-feature-item" data-mode="${mode}">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-play">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}" placeholder="Title">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" data-field="desc" value="${esc(item.desc)}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.lf-feature-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateLearningFeatures(mode, features) {
|
||||
document.getElementById(mode + 'FeaturesContainer').innerHTML = '';
|
||||
features.forEach(f => addLearningFeature(mode, f));
|
||||
}
|
||||
|
||||
function collectLearningFeatures(mode) {
|
||||
return Array.from(document.querySelectorAll(`.lf-feature-item[data-mode="${mode}"]`)).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
desc: row.querySelector('[data-field="desc"]').value.trim()
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Accreditation Badges ─────────────────────────────────────────────────
|
||||
function addAccBadge(item = {}) {
|
||||
const c = document.getElementById('accBadgesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-2 acc-badge-item">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-award">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}" placeholder="Title">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" data-field="desc" value="${esc(item.desc)}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.acc-badge-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateAccBadges(badges) {
|
||||
document.getElementById('accBadgesContainer').innerHTML = '';
|
||||
badges.forEach(b => addAccBadge(b));
|
||||
}
|
||||
|
||||
// ── Accreditation Stats ──────────────────────────────────────────────────
|
||||
function addAccStat(item = {}) {
|
||||
const c = document.getElementById('accStatsContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-2 acc-stat-item">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" data-field="value" value="${esc(item.value)}" placeholder="e.g. 89%">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" data-field="label" value="${esc(item.label)}" placeholder="Label">
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-center gap-2">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" data-field="isPrimary" ${item.isPrimary ? 'checked' : ''}>
|
||||
<label class="form-check-label small">Primary</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0" onclick="this.closest('.acc-stat-item').remove()"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateAccStats(stats) {
|
||||
document.getElementById('accStatsContainer').innerHTML = '';
|
||||
stats.forEach(s => addAccStat(s));
|
||||
}
|
||||
|
||||
// ── Success Stories ──────────────────────────────────────────────────────
|
||||
function addStory(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('storiesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-3 story-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small">Quote</label>
|
||||
<textarea class="form-control form-control-sm" data-field="quote" rows="2">${esc(item.quote)}</textarea>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="name" value="${esc(item.name)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Program</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="program" value="${esc(item.program)}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small">Avatar URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="avatar" id="storyAvatar_${idx}" value="${esc(item.avatar)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="storyAvatar_${idx}" data-image-type="about"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.story-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateStories(stories) {
|
||||
document.getElementById('storiesContainer').innerHTML = '';
|
||||
stories.forEach(s => addStory(s));
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────
|
||||
function esc(val) {
|
||||
if (!val) return '';
|
||||
return String(val).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Reset all changes to last saved state?')) {
|
||||
populateAll(originalFormData);
|
||||
}
|
||||
}
|
||||
|
||||
function updateImagePreview(inputId, imagePath) {
|
||||
if (!imagePath) return;
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
let preview = input.closest('.card')?.querySelector('.uploaded-preview');
|
||||
if (preview) {
|
||||
preview.src = imagePath;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = imagePath;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.maxHeight = '180px';
|
||||
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Dùng persistent hidden file input để tránh browser block programmatic click
|
||||
let fileInput = document.getElementById('__globalFileInput');
|
||||
if (!fileInput) {
|
||||
fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.id = '__globalFileInput';
|
||||
fileInput.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;width:1px;height:1px;';
|
||||
document.body.appendChild(fileInput);
|
||||
}
|
||||
|
||||
// Reset để có thể chọn lại cùng file
|
||||
fileInput.value = '';
|
||||
|
||||
// Gán handler mới mỗi lần gọi
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadBtn = document.querySelector(`[onclick*="${targetInput}"]`) ||
|
||||
document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadBtn) uploadBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) throw new Error('Upload failed');
|
||||
const result = await response.json();
|
||||
if (!result.success) throw new Error(result.error || 'Upload failed');
|
||||
|
||||
const input = document.getElementById(targetInput);
|
||||
if (input) {
|
||||
input.value = result.path;
|
||||
const previewUrl = result.path.startsWith('http') ? result.path : window.location.origin + result.path;
|
||||
let preview = input.closest('.col-md-12, .col-md-4, .col-md-5')?.querySelector('.uploaded-preview');
|
||||
if (preview) {
|
||||
preview.src = previewUrl;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.maxHeight = '150px';
|
||||
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
alert('Upload failed: ' + err.message);
|
||||
} finally {
|
||||
if (uploadBtn) uploadBtn.disabled = false;
|
||||
fileInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
@@ -1,967 +0,0 @@
|
||||
<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 About Us page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/about-us/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View About Us 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' ? 'active' : '' %>" id="mission-tab"
|
||||
data-bs-toggle="tab" href="#mission" role="tab"
|
||||
aria-selected="false"><i class="fas fa-bullseye me-2"></i>Mission</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link <%= locals.activeTab === 'features' ? 'active' : '' %>" id="features-tab"
|
||||
data-bs-toggle="tab" href="#features" role="tab"
|
||||
aria-selected="false"><i class="fas fa-star me-2"></i>Features</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>
|
||||
<% 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>
|
||||
<% 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' ? 'show active' : '' %>" id="mission" 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', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(imgKey => { %>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><%= imgKey.charAt(0).toUpperCase() + imgKey.slice(1) %></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>
|
||||
</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 (Icons & Labels)</label>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Features Tab -->
|
||||
<div class="tab-pane fade <%= locals.activeTab === 'features' ? 'show active' : '' %>" id="features" role="tabpanel">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<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 : 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>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
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 Us 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', 'bgShape', 'planeShape', 'topShape', 'globeShape'].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-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" name="missionItemIcon_${idx}" placeholder="Icon path">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="missionItemIcon_${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="missionItemLabel_${idx}" placeholder="Label">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<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, i) => {
|
||||
addMissionItem();
|
||||
const last = container.lastElementChild;
|
||||
last.querySelector(`[name="missionItemIcon_${i}"]`).value = item.icon || '';
|
||||
last.querySelector(`[name="missionItemLabel_${i}"]`).value = item.label || '';
|
||||
last.querySelector(`[name="missionItemDesc_${i}"]`).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(),
|
||||
bgShape: document.getElementById('missionImg_bgShape').value.trim(),
|
||||
planeShape: document.getElementById('missionImg_planeShape').value.trim(),
|
||||
topShape: document.getElementById('missionImg_topShape').value.trim(),
|
||||
globeShape: document.getElementById('missionImg_globeShape').value.trim()
|
||||
},
|
||||
items: Array.from(document.querySelectorAll('.mission-item')).map(item => ({
|
||||
icon: item.querySelector('[name^="missionItemIcon_"]').value.trim(),
|
||||
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>
|
||||
@@ -1,791 +0,0 @@
|
||||
<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 Appointment page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/make-appointment/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Appointment Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="appointmentForm"
|
||||
action="/admin/appointment/update">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="visaOptions" id="visaOptionsJson">
|
||||
<input type="hidden" name="form" id="formJson">
|
||||
|
||||
<!-- 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="#visaOptions" role="tab">
|
||||
<i class="fas fa-passport me-2"></i>Visa Options
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
|
||||
<i class="fas fa-envelope me-2"></i>Form
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#submissions" role="tab">
|
||||
<i class="fas fa-list me-2"></i>Submissions
|
||||
</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">
|
||||
<h6 class="fw-medium mb-3">Hero Section</h6>
|
||||
<div class="row g-3">
|
||||
<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="appointment">
|
||||
<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: 200px;">
|
||||
<% 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: 200px; 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: 200px; display: none; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">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 fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSubtitle"
|
||||
name="heroSubtitle" value="<%= data.hero?.subtitle || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" id="heroHeading"
|
||||
name="heroHeading" value="<%= data.hero?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="heroDescription"
|
||||
name="heroDescription"
|
||||
rows="2"><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visa Options Tab -->
|
||||
<div class="tab-pane fade" id="visaOptions" 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">Visa Options</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addVisaOption()">
|
||||
<i class="fas fa-plus"></i> Add Option
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-muted small">These options will appear in the visa type selection
|
||||
dropdown on the appointment form.</p>
|
||||
<div id="visaOptionsContainer">
|
||||
<% if (data.visaOptions && data.visaOptions.length> 0) { %>
|
||||
<% data.visaOptions.forEach((option, index)=> { %>
|
||||
<div class="input-group mb-2 visa-option-item">
|
||||
<span class="input-group-text"><i
|
||||
class="fas fa-passport"></i></span>
|
||||
<input type="text" class="form-control visa-option-input"
|
||||
value="<%= option %>" placeholder="Enter visa option">
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
onclick="removeVisaOption(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Tab -->
|
||||
<div class="tab-pane fade" id="form" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Form Settings</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Form Heading</label>
|
||||
<input type="text" class="form-control" id="formHeading"
|
||||
value="<%= data.form?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Submit Button Text</label>
|
||||
<input type="text" class="form-control" id="formSubmitButtonText"
|
||||
value="<%= data.form?.submitButton?.text || 'Request Appointment' %>">
|
||||
</div>
|
||||
<!-- Hidden fields for submitButton icon and buttonClass -->
|
||||
<input type="hidden" id="formSubmitButtonIcon"
|
||||
value="<%= data.form?.submitButton?.icon || 'fa-solid fa-arrow-right' %>">
|
||||
<input type="hidden" id="formSubmitButtonClass"
|
||||
value="<%= data.form?.submitButton?.buttonClass || 'theme-btn' %>">
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Form Fields</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="addFormField()">
|
||||
<i class="fas fa-plus"></i> Add Field
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="formFieldsContainer">
|
||||
<% if (data.form?.fields && data.form.fields.length> 0) { %>
|
||||
<% data.form.fields.forEach((field, index)=> { %>
|
||||
<div class="card mb-3 form-field-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Name</label>
|
||||
<input type="text"
|
||||
class="form-control field-name-input"
|
||||
value="<%= field.name || '' %>"
|
||||
placeholder="e.g., name">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text"
|
||||
class="form-control field-label-input"
|
||||
value="<%= field.label || '' %>"
|
||||
placeholder="e.g., Your Name">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select field-type-select">
|
||||
<option value="text" <%=field.type==='text'
|
||||
? 'selected' : '' %>>Text</option>
|
||||
<option value="email" <%=field.type==='email'
|
||||
? 'selected' : '' %>>Email</option>
|
||||
<option value="tel" <%=field.type==='tel'
|
||||
? 'selected' : '' %>>Phone</option>
|
||||
<option value="textarea"
|
||||
<%=field.type==='textarea' ? 'selected' : ''
|
||||
%>>Textarea</option>
|
||||
<option value="date" <%=field.type==='date'
|
||||
? 'selected' : '' %>>Date</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Col Class</label>
|
||||
<select class="form-select field-col-select">
|
||||
<option value="col-lg-4"
|
||||
<%=field.colClass==='col-lg-4' ? 'selected'
|
||||
: '' %>>1/3 Width</option>
|
||||
<option value="col-lg-6"
|
||||
<%=field.colClass==='col-lg-6' ? 'selected'
|
||||
: '' %>>1/2 Width</option>
|
||||
<option value="col-lg-12"
|
||||
<%=field.colClass==='col-lg-12' ? 'selected'
|
||||
: '' %>>Full Width</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Required</label>
|
||||
<div class="form-check mt-2">
|
||||
<input
|
||||
class="form-check-input field-required-check"
|
||||
type="checkbox" <%=field.required
|
||||
? 'checked' : '' %>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text"
|
||||
class="form-control field-placeholder-input"
|
||||
value="<%= field.placeholder || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeFormField(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submissions Tab -->
|
||||
<div class="tab-pane fade" id="submissions" 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">Recent Submissions</h6>
|
||||
</div>
|
||||
|
||||
<!-- Date Filter -->
|
||||
<div class="row g-2 mb-4 align-items-end" id="filterContainer">
|
||||
<input type="hidden" id="filterTab" value="submissions">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Start Date</label>
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
id="filterStartDate" value="<%= locals.startDate || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">End Date</label>
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
id="filterEndDate" value="<%= locals.endDate || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="button" class="btn btn-sm btn-primary w-100"
|
||||
onclick="applyDateFilter()">
|
||||
<i class="fas fa-filter me-1"></i> Filter
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/admin/appointment?tab=submissions"
|
||||
class="btn btn-sm btn-outline-secondary w-100">
|
||||
<i class="fas fa-times me-1"></i> Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Name</th>
|
||||
<th>Contact</th>
|
||||
<th>Appt Date</th>
|
||||
<th>Visa Types</th>
|
||||
<th>Message</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (locals.submissions && submissions.length> 0) { %>
|
||||
<% submissions.forEach(submission=> { %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= new
|
||||
Date(submission.createdAt).toLocaleDateString()
|
||||
%>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
<%= new
|
||||
Date(submission.createdAt).toLocaleTimeString([],
|
||||
{hour: '2-digit' , minute:'2-digit'}) %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<%= submission.name %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<a href="mailto:<%= submission.email %>"
|
||||
class="text-decoration-none"><i
|
||||
class="fas fa-envelope me-1"></i>
|
||||
<%= submission.email %>
|
||||
</a>
|
||||
<% if(submission.phone) { %>
|
||||
<span class="text-muted small"><i
|
||||
class="fas fa-phone me-1"></i>
|
||||
<%= submission.phone %>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<%= submission.appointmentDate || '-' %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (submission.visaTypes &&
|
||||
submission.visaTypes.length> 0) { %>
|
||||
<% submission.visaTypes.forEach(type=> { %>
|
||||
<span
|
||||
class="badge bg-light text-dark border me-1">
|
||||
<%= type %>
|
||||
</span>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (submission.message) { %>
|
||||
<div title="<%= submission.message %>"
|
||||
style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<%= submission.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% let statusClass='bg-secondary' ;
|
||||
if(submission.status==='pending' )
|
||||
statusClass='bg-warning text-dark' ;
|
||||
if(submission.status==='confirmed' )
|
||||
statusClass='bg-success' ;
|
||||
if(submission.status==='completed' )
|
||||
statusClass='bg-info text-dark' ;
|
||||
if(submission.status==='cancelled' )
|
||||
statusClass='bg-danger' ; %>
|
||||
<span
|
||||
class="badge <%= statusClass %> rounded-pill">
|
||||
<%= submission.status %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
onclick="openStatusModal('<%= submission._id %>', '<%= submission.status %>')"
|
||||
title="Update Status">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<tr>
|
||||
<td colspan="8"
|
||||
class="text-center py-4 text-muted">No
|
||||
submissions found</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3 text-end">
|
||||
<small class="text-muted">Showing last 50 submissions</small>
|
||||
</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>
|
||||
|
||||
<!-- Status Update Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Update Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm">
|
||||
<input type="hidden" id="statusSubmissionId">
|
||||
<div class="mb-3">
|
||||
<label for="statusSelect" class="form-label">Status</label>
|
||||
<select class="form-select" id="statusSelect">
|
||||
<option value="pending">Pending</option>
|
||||
<option value="confirmed">Confirmed</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveStatus()">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="appointmentDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
let statusModal = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('appointmentDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = {};
|
||||
}
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tab = urlParams.get('tab');
|
||||
if (tab) {
|
||||
const triggerEl = document.querySelector(`a[href="#${tab}"]`);
|
||||
if (triggerEl) {
|
||||
const tabInstance = new bootstrap.Tab(triggerEl);
|
||||
tabInstance.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Move modal to body to prevent backdrop issues
|
||||
const statusModalEl = document.getElementById('statusModal');
|
||||
if (statusModalEl) {
|
||||
document.body.appendChild(statusModalEl);
|
||||
}
|
||||
statusModal = new bootstrap.Modal(statusModalEl);
|
||||
|
||||
updateAllJsonInputs();
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function applyDateFilter() {
|
||||
const startDate = document.getElementById('filterStartDate').value;
|
||||
const endDate = document.getElementById('filterEndDate').value;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('tab', 'submissions');
|
||||
|
||||
if (startDate) {
|
||||
url.searchParams.set('startDate', startDate);
|
||||
} else {
|
||||
url.searchParams.delete('startDate');
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
url.searchParams.set('endDate', endDate);
|
||||
} else {
|
||||
url.searchParams.delete('endDate');
|
||||
}
|
||||
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function openStatusModal(id, currentStatus) {
|
||||
document.getElementById('statusSubmissionId').value = id;
|
||||
document.getElementById('statusSelect').value = currentStatus;
|
||||
statusModal.show();
|
||||
}
|
||||
|
||||
async function saveStatus() {
|
||||
const id = document.getElementById('statusSubmissionId').value;
|
||||
const status = document.getElementById('statusSelect').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/appointments/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Determine CSS class for the notification or badge
|
||||
// Since this is generic, we'll reload or update UI manually if complex.
|
||||
// Reload is safest to show updated table state (including sorting/filtering if any)
|
||||
// But let's try to be smooth:
|
||||
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to update status: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating status:', error);
|
||||
alert('Error updating status');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('appointmentForm');
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
// Image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Update preview when background image changes
|
||||
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
|
||||
updateHeroImagePreview(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHeroImagePreview(imagePath) {
|
||||
const previewContainer = document.getElementById('heroImagePreview');
|
||||
if (imagePath) {
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
|
||||
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
|
||||
}
|
||||
previewContainer.innerHTML = `
|
||||
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
|
||||
style="height: 200px; 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: 200px; display: none; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
previewContainer.innerHTML = `
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
Image preview
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllJsonInputs() {
|
||||
updateJsonData();
|
||||
}
|
||||
|
||||
function updateJsonData() {
|
||||
// Hero data
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value || '',
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
|
||||
subtitle: document.getElementById('heroSubtitle').value || '',
|
||||
heading: document.getElementById('heroHeading').value || '',
|
||||
description: document.getElementById('heroDescription').value || '',
|
||||
};
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
|
||||
// Visa options
|
||||
const visaOptions = [];
|
||||
document.querySelectorAll('.visa-option-input').forEach(input => {
|
||||
if (input.value.trim()) {
|
||||
visaOptions.push(input.value.trim());
|
||||
}
|
||||
});
|
||||
document.getElementById('visaOptionsJson').value = JSON.stringify(visaOptions);
|
||||
|
||||
// Form data
|
||||
const fields = [];
|
||||
document.querySelectorAll('.form-field-item').forEach(item => {
|
||||
fields.push({
|
||||
name: item.querySelector('.field-name-input').value || '',
|
||||
label: item.querySelector('.field-label-input').value || '',
|
||||
type: item.querySelector('.field-type-select').value || 'text',
|
||||
placeholder: item.querySelector('.field-placeholder-input').value || '',
|
||||
required: item.querySelector('.field-required-check').checked,
|
||||
colClass: item.querySelector('.field-col-select').value || 'col-lg-12',
|
||||
});
|
||||
});
|
||||
|
||||
const formData = {
|
||||
heading: document.getElementById('formHeading').value || '',
|
||||
fields: fields,
|
||||
submitButton: {
|
||||
text: document.getElementById('formSubmitButtonText').value || 'Request Appointment',
|
||||
icon: document.getElementById('formSubmitButtonIcon').value || 'fa-solid fa-arrow-right',
|
||||
buttonClass: document.getElementById('formSubmitButtonClass').value || 'theme-btn',
|
||||
},
|
||||
};
|
||||
document.getElementById('formJson').value = JSON.stringify(formData);
|
||||
}
|
||||
|
||||
function addVisaOption() {
|
||||
const container = document.getElementById('visaOptionsContainer');
|
||||
const html = `
|
||||
<div class="input-group mb-2 visa-option-item">
|
||||
<span class="input-group-text"><i class="fas fa-passport"></i></span>
|
||||
<input type="text" class="form-control visa-option-input" value="" placeholder="Enter visa option">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeVisaOption(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeVisaOption(button) {
|
||||
button.closest('.visa-option-item').remove();
|
||||
}
|
||||
|
||||
function addFormField() {
|
||||
const container = document.getElementById('formFieldsContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 form-field-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Name</label>
|
||||
<input type="text" class="form-control field-name-input" value="" placeholder="e.g., name">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" class="form-control field-label-input" value="" placeholder="e.g., Your Name">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select field-type-select">
|
||||
<option value="text" selected>Text</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="tel">Phone</option>
|
||||
<option value="textarea">Textarea</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Col Class</label>
|
||||
<select class="form-select field-col-select">
|
||||
<option value="col-lg-4">1/3 Width</option>
|
||||
<option value="col-lg-6">1/2 Width</option>
|
||||
<option value="col-lg-12" selected>Full Width</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Required</label>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input field-required-check" type="checkbox">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control field-placeholder-input" value="">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFormField(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeFormField(button) {
|
||||
button.closest('.form-field-item').remove();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// Image uploader function (reuse from shared)
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Open upload modal or trigger file input
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/upload/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.imagePath) {
|
||||
document.getElementById(targetInput).value = result.imagePath;
|
||||
if (targetInput === 'heroBackgroundImage') {
|
||||
updateHeroImagePreview(result.imagePath);
|
||||
}
|
||||
} else {
|
||||
alert('Upload failed: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Upload failed. Please try again.');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
+69
-104
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Us -->
|
||||
<!-- About -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
@@ -78,7 +78,7 @@
|
||||
<p class="text-muted mb-0 small">Manage about us</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/about-us" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<a href="/admin/about" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Request Info -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -141,64 +141,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appointment -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-calendar-check fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Appointment</h5>
|
||||
<p class="text-muted mb-0 small">Manage appointment page</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/appointment" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-tags fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Pricing</h5>
|
||||
<p class="text-muted mb-0 small">Manage pricing page</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/pricing" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Services -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-concierge-bell fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Services</h5>
|
||||
<p class="text-muted mb-0 small">Manage services</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/service" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -216,27 +160,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visa -->
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-passport fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Visa</h5>
|
||||
<p class="text-muted mb-0 small">Manage visa countries</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/visa" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Programme -->
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
@@ -276,6 +201,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!--Menu Header API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -297,6 +223,8 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--Home API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -318,6 +246,31 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--Footer API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-window-minimize" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Footer API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/footer</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get footer data</td>
|
||||
<td>
|
||||
<a href="/api/footer" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--About API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -339,30 +292,8 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-users" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>About Us API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/about-us</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get about us data</td>
|
||||
<td>
|
||||
<a href="/api/about-us" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<!--Contact API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -384,6 +315,8 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--Student Support API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -405,6 +338,8 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--Request Inf API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -426,6 +361,8 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--Blog API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -447,6 +384,32 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!--Blog Details API-->
|
||||
<!-- <tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-blog" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Blog API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/blog</code></td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>List published blog posts (supports ?page, ?category, ?search)</td>
|
||||
<td>
|
||||
<a href="/api/blog" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr> -->
|
||||
|
||||
<!--Programmes API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -468,6 +431,8 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--Programmes Details API-->
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -515,7 +480,7 @@
|
||||
<div>
|
||||
<div class="text-muted small">Version</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color)">
|
||||
CMS.HAILearning v1.0.0
|
||||
CMS.LAMS v1.0.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<!-- Header section -->
|
||||
<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);">Create New Department</h1>
|
||||
<p class="text-muted mb-0">Add a new department to the system</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Department Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form action="/admin/department/create" method="POST" id="createForm">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label fw-medium">Department Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required
|
||||
placeholder="Enter department name">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Enter the full name of the department (e.g. Business, Engineering)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
<strong>Tips:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Department name should be clear and descriptive</li>
|
||||
<li>Use proper capitalization</li>
|
||||
<li>Avoid abbreviations unless commonly known</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create Department
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Notification</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Content of the notification will appear here.</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-primary custom-modal-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize custom modal
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
const form = document.getElementById('createForm');
|
||||
const nameInput = document.getElementById('name');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Validate department name
|
||||
if (!nameInput.value.trim()) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Please enter a department name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for special characters
|
||||
if (/[^a-zA-Z0-9\s-]/.test(nameInput.value.trim())) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Department name can only contain letters, numbers, spaces and hyphens.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,100 +0,0 @@
|
||||
<!-- Header section -->
|
||||
<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);">Edit Department</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Editing department: <span class="badge bg-primary"><%= department.name %></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Department Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form action="/admin/department/edit/<%= department._id %>" method="POST" id="editForm">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label fw-medium">Department Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required
|
||||
value="<%= department.name %>" placeholder="Enter department name">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Enter the full name of the department (e.g. Business, Engineering)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
<strong>Tips:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Department name should be clear and descriptive</li>
|
||||
<li>Use proper capitalization</li>
|
||||
<li>Avoid abbreviations unless commonly known</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Notification</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Content of the notification will appear here.</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-primary custom-modal-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize custom modal
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
const form = document.getElementById('editForm');
|
||||
const nameInput = document.getElementById('name');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Validate department name
|
||||
if (!nameInput.value.trim()) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Please enter a department name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for special characters
|
||||
if (/[^a-zA-Z0-9\s-]/.test(nameInput.value.trim())) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Department name can only contain letters, numbers, spaces and hyphens.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,126 +0,0 @@
|
||||
<!-- Header section -->
|
||||
<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);">Department Management</h1>
|
||||
<p class="text-muted mb-0">Manage all departments in the system</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create New Department
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<% if (departments && departments.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 50px">#</th>
|
||||
<th scope="col">Department Name</th>
|
||||
<th scope="col">Slug</th>
|
||||
<th scope="col" style="width: 200px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% departments.forEach((department, index) => { %>
|
||||
<tr>
|
||||
<td><%= index + 1 %></td>
|
||||
<td>
|
||||
<span class="fw-medium"><%= department.name %></span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-muted"><%= department.slug %></code>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/department/edit/<%= department._id %>" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-custom-modal="open"
|
||||
data-id="<%= department._id %>"
|
||||
data-name="<%= department.name %>">
|
||||
<i class="fas fa-trash-alt me-1"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-folder-open text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mb-3">No Departments Found</h5>
|
||||
<a href="/admin/department/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create First Department
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Delete Confirmation</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Are you sure you want to delete this department?</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary custom-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-danger custom-modal-ok">Delete Permanently</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Khởi tạo modal tùy chỉnh
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
// Lắng nghe click vào nút xóa
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.getAttribute('data-custom-modal') === 'open' ||
|
||||
e.target.parentElement.getAttribute('data-custom-modal') === 'open') {
|
||||
|
||||
// Lấy button hoặc icon parent nếu click vào icon
|
||||
const button = e.target.getAttribute('data-custom-modal') === 'open' ?
|
||||
e.target : e.target.parentElement;
|
||||
|
||||
const id = button.getAttribute('data-id');
|
||||
const name = button.getAttribute('data-name');
|
||||
|
||||
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
|
||||
CustomModal.confirm(
|
||||
`Are you sure you want to delete department "${name}"? This action cannot be undone.`,
|
||||
function() {
|
||||
// Hành động khi xác nhận
|
||||
window.location.href = `/admin/department/delete/${id}`;
|
||||
},
|
||||
null,
|
||||
'Delete Confirmation'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
+429
-1526
File diff suppressed because it is too large
Load Diff
+624
-180
@@ -1,231 +1,675 @@
|
||||
<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)">
|
||||
Homepage Management
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on homepage</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
<p class="text-muted mb-0">Edit content displayed on the Home page</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/home/update" method="POST" class="content-with-fixed-buttons">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="hero" id="heroJson" />
|
||||
<input type="hidden" name="whyChooseUs" id="whyChooseUsJson" />
|
||||
<input type="hidden" name="visaSolutions" id="visaSolutionsJson" />
|
||||
<input type="hidden" name="visaCountries" id="visaCountriesJson" />
|
||||
<input type="hidden" name="testimonials" id="testimonialsJson" />
|
||||
<input type="hidden" name="videoGallery" id="videoGalleryJson" />
|
||||
<input type="hidden" name="faq" id="faqJson" />
|
||||
<input type="hidden" name="achievements" id="achievementsJson" />
|
||||
<input type="hidden" name="partners" id="partnersJson" />
|
||||
<input type="hidden" name="blogPreview" id="blogPreviewJson" />
|
||||
<form method="POST" id="homeForm" action="/admin/home/update">
|
||||
<!-- Hidden JSON inputs -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="quickLinks" id="quickLinksJson">
|
||||
<input type="hidden" name="valueProp" id="valuePropJson">
|
||||
<input type="hidden" name="programs" id="programsJson">
|
||||
<input type="hidden" name="requestInfo" id="requestInfoJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<!-- 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>
|
||||
<ul class="nav nav-tabs card-header-tabs" id="homeTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="tab-hero" data-bs-toggle="tab" href="#tab-hero-pane" role="tab"><i
|
||||
class="fas fa-image me-2"></i>Hero</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#whychooseus" role="tab">
|
||||
<i class="fas fa-star me-2"></i>Why Choose Us
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-quickLinks" data-bs-toggle="tab" href="#tab-quickLinks-pane" role="tab"><i
|
||||
class="fas fa-link me-2"></i>Quick Links</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visasolutions" role="tab">
|
||||
<i class="fas fa-concierge-bell me-2"></i>Visa Solutions
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-valueProp" data-bs-toggle="tab" href="#tab-valueProp-pane" role="tab"><i
|
||||
class="fas fa-star me-2"></i>Value Prop</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visacountries" role="tab">
|
||||
<i class="fas fa-globe-americas me-2"></i>Visa Countries
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-programs" data-bs-toggle="tab" href="#tab-programs-pane" role="tab"><i
|
||||
class="fas fa-graduation-cap me-2"></i>Programs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
|
||||
<i class="fas fa-comments me-2"></i>Testimonials
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#videogallery" role="tab">
|
||||
<i class="fas fa-video me-2"></i>Video Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#faq" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#achievements" role="tab">
|
||||
<i class="fas fa-chart-pie me-2"></i>Achievements
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#partners" role="tab">
|
||||
<i class="fas fa-handshake me-2"></i>Partners
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#blogpreview" role="tab">
|
||||
<i class="fas fa-blog me-2"></i>Blog Preview
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-requestInfo" data-bs-toggle="tab" href="#tab-requestInfo-pane" role="tab"><i
|
||||
class="fas fa-envelope me-2"></i>Request Info</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<%- include('sections/hero') %>
|
||||
<%- include('sections/whyChooseUs') %>
|
||||
<%- include('sections/visaSolutions') %>
|
||||
<%- include('sections/visaCountries') %>
|
||||
<%- include('sections/testimonials') %>
|
||||
<%- include('sections/videoGallery') %>
|
||||
<%- include('sections/faq') %>
|
||||
<%- include('sections/achievements') %>
|
||||
<%- include('sections/partners') %>
|
||||
<%- include('sections/blogPreview') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
<!-- ===== HERO TAB ===== -->
|
||||
<div class="tab-pane fade show active" id="tab-hero-pane" 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">Badge</label>
|
||||
<input type="text" class="form-control" id="heroBadge" value="<%= data.hero?.badge || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Button Label</label>
|
||||
<input type="text" class="form-control" id="heroButtonLabel"
|
||||
value="<%= data.hero?.buttonLabel || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle" value="<%= data.hero?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="heroDescription"
|
||||
rows="3"><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Search Placeholder</label>
|
||||
<input type="text" class="form-control" id="heroSearchPlaceholder"
|
||||
value="<%= data.hero?.searchPlaceholder || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Image Alt Text</label>
|
||||
<input type="text" class="form-control" id="heroImageAlt"
|
||||
value="<%= data.hero?.imageAlt || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Image URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroImage" value="<%= data.hero?.image || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button"
|
||||
data-target-input="heroImage" data-image-type="home"><i
|
||||
class="fas fa-upload me-1"></i>Upload</button>
|
||||
</div>
|
||||
<% if (data.hero?.image) { %>
|
||||
<img src="<%= data.hero.image %>" class="img-thumbnail uploaded-preview mt-2"
|
||||
style="max-height:180px;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4 mb-3">Floating Badge</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Icon (FA class)</label>
|
||||
<input type="text" class="form-control" id="floatingBadgeIcon"
|
||||
value="<%= data.hero?.floatingBadge?.icon || '' %>" placeholder="e.g. fa-users">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" id="floatingBadgeValue"
|
||||
value="<%= data.hero?.floatingBadge?.value || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" class="form-control" id="floatingBadgeLabel"
|
||||
value="<%= data.hero?.floatingBadge?.label || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== QUICK LINKS TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-quickLinks-pane" 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-link me-2"></i>Quick Links</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addQuickLink()"><i
|
||||
class="fas fa-plus me-1"></i>Add Link</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div id="quickLinksContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== VALUE PROP TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-valueProp-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Value Proposition</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Badge</label>
|
||||
<input type="text" class="form-control" id="valuePropBadge"
|
||||
value="<%= data.valueProp?.badge || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="valuePropTitle"
|
||||
value="<%= data.valueProp?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="valuePropDescription"
|
||||
rows="3"><%= data.valueProp?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Features</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addValuePropFeature()"><i
|
||||
class="fas fa-plus me-1"></i>Add Feature</button>
|
||||
</div>
|
||||
<div id="valuePropFeaturesContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Stats</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addValuePropStat()"><i
|
||||
class="fas fa-plus me-1"></i>Add Stat</button>
|
||||
</div>
|
||||
<div id="valuePropStatsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== PROGRAMS TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-programs-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-graduation-cap me-2"></i>Programs Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="programsHeading"
|
||||
value="<%= data.programs?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="programsDescription"
|
||||
rows="2"><%= data.programs?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Program Items</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addProgramItem()"><i
|
||||
class="fas fa-plus me-1"></i>Add Program</button>
|
||||
</div>
|
||||
<div id="programItemsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== REQUEST INFO TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-requestInfo-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-envelope me-2"></i>Request Info Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="requestInfoHeading"
|
||||
value="<%= data.requestInfo?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="requestInfoDescription"
|
||||
rows="2"><%= data.requestInfo?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" class="form-control" id="requestInfoPhone"
|
||||
value="<%= data.requestInfo?.phone || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="text" class="form-control" id="requestInfoEmail"
|
||||
value="<%= data.requestInfo?.email || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Programs List</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addRequestInfoProgram()"><i class="fas fa-plus me-1"></i>Add Program</button>
|
||||
</div>
|
||||
<div id="requestInfoProgramsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.tab-content -->
|
||||
</div><!-- /.card-body -->
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image upload input -->
|
||||
<input type="file" id="directImageUpload" style="display: none" />
|
||||
<input type="hidden" id="currentImageType" name="imageType" />
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput" />
|
||||
|
||||
<script>
|
||||
/**
|
||||
* BRIDGE SCRIPT: Cho phép các section lẻ tự đăng ký logic lấy dữ liệu.
|
||||
* Cách dùng trong file lẻ (vị dụ hero.ejs):
|
||||
* <script>
|
||||
* window.homeScrapers = window.homeScrapers || {};
|
||||
* window.homeScrapers.hero = () => ({ title: document.getElementById('heroTitle').value, ... });
|
||||
* <\/script>
|
||||
*/
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
console.log("Form submitting, collecting data from scrapers...");
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
populateAll(originalFormData);
|
||||
|
||||
// Tự động thu gom dữ liệu từ các section đã đăng ký
|
||||
Object.keys(window.homeScrapers).forEach(section => {
|
||||
const input = document.getElementById(section + 'Json');
|
||||
if (input) {
|
||||
try {
|
||||
const data = window.homeScrapers[section]();
|
||||
console.log(`- Collected data for [${section}]:`, data);
|
||||
input.value = JSON.stringify(data);
|
||||
} catch (err) {
|
||||
console.error(`Error scraping section [${section}]:`, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById('homeForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
serializeAll();
|
||||
this.submit();
|
||||
});
|
||||
|
||||
// Để form tự submit tự nhiên sau khi đã điền xong các hidden inputs
|
||||
});
|
||||
}
|
||||
|
||||
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
|
||||
initImageUploads();
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-upload-image');
|
||||
if (btn) openImageUploader(btn.dataset.targetInput, btn.dataset.imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// --- UTILITIES (Dùng chung) ---
|
||||
// ── Populate all sections from data object ──────────────────────────────
|
||||
function populateAll(data) {
|
||||
if (!data) return;
|
||||
|
||||
function initImageUploads() {
|
||||
document.addEventListener("click", function (e) {
|
||||
const btn = e.target.closest(".btn-upload-image");
|
||||
if (btn) {
|
||||
document.getElementById("currentImageType").value = btn.dataset.imageType;
|
||||
document.getElementById("currentTargetInput").value = btn.dataset.targetInput;
|
||||
document.getElementById("directImageUpload").click();
|
||||
// Hero
|
||||
const h = data.hero || {};
|
||||
setVal('heroBadge', h.badge);
|
||||
setVal('heroTitle', h.title);
|
||||
setVal('heroDescription', h.description);
|
||||
setVal('heroSearchPlaceholder', h.searchPlaceholder);
|
||||
setVal('heroButtonLabel', h.buttonLabel);
|
||||
setVal('heroImage', h.image);
|
||||
setVal('heroImageAlt', h.imageAlt);
|
||||
setVal('floatingBadgeIcon', h.floatingBadge?.icon);
|
||||
setVal('floatingBadgeValue', h.floatingBadge?.value);
|
||||
setVal('floatingBadgeLabel', h.floatingBadge?.label);
|
||||
updateImagePreview('heroImage', h.image);
|
||||
|
||||
// Quick Links
|
||||
populateQuickLinks(data.quickLinks || []);
|
||||
|
||||
// Value Prop
|
||||
const vp = data.valueProp || {};
|
||||
setVal('valuePropBadge', vp.badge);
|
||||
setVal('valuePropTitle', vp.title);
|
||||
setVal('valuePropDescription', vp.description);
|
||||
populateValuePropFeatures(vp.features || []);
|
||||
populateValuePropStats(vp.stats || []);
|
||||
|
||||
// Programs
|
||||
const pr = data.programs || {};
|
||||
setVal('programsHeading', pr.heading);
|
||||
setVal('programsDescription', pr.description);
|
||||
populateProgramItems(pr.items || []);
|
||||
|
||||
// Request Info
|
||||
const ri = data.requestInfo || {};
|
||||
setVal('requestInfoHeading', ri.heading);
|
||||
setVal('requestInfoDescription', ri.description);
|
||||
setVal('requestInfoPhone', ri.phone);
|
||||
setVal('requestInfoEmail', ri.email);
|
||||
populateRequestInfoPrograms(ri.programs || []);
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.tagName === 'TEXTAREA' ? (el.value = val || '') : (el.value = val || '');
|
||||
}
|
||||
|
||||
// ── Serialize all sections into hidden JSON inputs ───────────────────────
|
||||
function serializeAll() {
|
||||
// Hero
|
||||
document.getElementById('heroJson').value = JSON.stringify({
|
||||
badge: v('heroBadge'),
|
||||
title: v('heroTitle'),
|
||||
description: v('heroDescription'),
|
||||
searchPlaceholder: v('heroSearchPlaceholder'),
|
||||
buttonLabel: v('heroButtonLabel'),
|
||||
image: v('heroImage'),
|
||||
imageAlt: v('heroImageAlt'),
|
||||
floatingBadge: {
|
||||
icon: v('floatingBadgeIcon'),
|
||||
value: v('floatingBadgeValue'),
|
||||
label: v('floatingBadgeLabel')
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("directImageUpload");
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", handleDirectImageUpload);
|
||||
// Quick Links
|
||||
document.getElementById('quickLinksJson').value = JSON.stringify(
|
||||
Array.from(document.querySelectorAll('.quicklink-item')).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim(),
|
||||
linkText: row.querySelector('[data-field="linkText"]').value.trim(),
|
||||
href: row.querySelector('[data-field="href"]').value.trim()
|
||||
}))
|
||||
);
|
||||
|
||||
// Value Prop
|
||||
document.getElementById('valuePropJson').value = JSON.stringify({
|
||||
badge: v('valuePropBadge'),
|
||||
title: v('valuePropTitle'),
|
||||
description: v('valuePropDescription'),
|
||||
features: Array.from(document.querySelectorAll('.vp-feature-item')).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim()
|
||||
})),
|
||||
stats: Array.from(document.querySelectorAll('.vp-stat-item')).map(row => ({
|
||||
value: row.querySelector('[data-field="value"]').value.trim(),
|
||||
label: row.querySelector('[data-field="label"]').value.trim(),
|
||||
image: row.querySelector('[data-field="image"]').value.trim(),
|
||||
imageAlt: row.querySelector('[data-field="imageAlt"]').value.trim()
|
||||
}))
|
||||
});
|
||||
|
||||
// Programs
|
||||
document.getElementById('programsJson').value = JSON.stringify({
|
||||
heading: v('programsHeading'),
|
||||
description: v('programsDescription'),
|
||||
items: Array.from(document.querySelectorAll('.program-item')).map(row => ({
|
||||
category: row.querySelector('[data-field="category"]').value.trim(),
|
||||
image: row.querySelector('[data-field="image"]').value.trim(),
|
||||
duration: row.querySelector('[data-field="duration"]').value.trim(),
|
||||
rating: row.querySelector('[data-field="rating"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim(),
|
||||
studentCount: row.querySelector('[data-field="studentCount"]').value.trim(),
|
||||
href: row.querySelector('[data-field="href"]').value.trim()
|
||||
}))
|
||||
});
|
||||
|
||||
// Request Info
|
||||
document.getElementById('requestInfoJson').value = JSON.stringify({
|
||||
heading: v('requestInfoHeading'),
|
||||
description: v('requestInfoDescription'),
|
||||
phone: v('requestInfoPhone'),
|
||||
email: v('requestInfoEmail'),
|
||||
programs: Array.from(document.querySelectorAll('.ri-program-input')).map(i => i.value.trim()).filter(Boolean)
|
||||
});
|
||||
}
|
||||
|
||||
function v(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
// ── Quick Links ──────────────────────────────────────────────────────────
|
||||
function addQuickLink(item = {}) {
|
||||
const c = document.getElementById('quickLinksContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 quicklink-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Icon (FA class)</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-trophy">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="description" value="${esc(item.description)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Link Text</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="linkText" value="${esc(item.linkText)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Href</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="href" value="${esc(item.href)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.quicklink-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateQuickLinks(items) {
|
||||
document.getElementById('quickLinksContainer').innerHTML = '';
|
||||
items.forEach(item => addQuickLink(item));
|
||||
}
|
||||
|
||||
// ── Value Prop Features ──────────────────────────────────────────────────
|
||||
function addValuePropFeature(item = {}) {
|
||||
const c = document.getElementById('valuePropFeaturesContainer');
|
||||
const html = `
|
||||
<div class="card mb-2 vp-feature-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Icon (FA class)</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-laptop-code">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="description" value="${esc(item.description)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.vp-feature-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateValuePropFeatures(items) {
|
||||
document.getElementById('valuePropFeaturesContainer').innerHTML = '';
|
||||
items.forEach(item => addValuePropFeature(item));
|
||||
}
|
||||
|
||||
// ── Value Prop Stats ─────────────────────────────────────────────────────
|
||||
function addValuePropStat(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('valuePropStatsContainer');
|
||||
const html = `
|
||||
<div class="card mb-2 vp-stat-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Value</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="value" value="${esc(item.value)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Label</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="label" value="${esc(item.label)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Image URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="image" id="vpStatImg_${idx}" value="${esc(item.image)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="vpStatImg_${idx}" data-image-type="home"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Image Alt</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="imageAlt" value="${esc(item.imageAlt)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.vp-stat-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateValuePropStats(items) {
|
||||
document.getElementById('valuePropStatsContainer').innerHTML = '';
|
||||
items.forEach(item => addValuePropStat(item));
|
||||
}
|
||||
|
||||
// ── Program Items ────────────────────────────────────────────────────────
|
||||
function addProgramItem(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('programItemsContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 program-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Category</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="category" value="${esc(item.category)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Duration</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="duration" value="${esc(item.duration)}">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label small">Rating</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="rating" value="${esc(item.rating)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Student Count</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="studentCount" value="${esc(item.studentCount)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Href</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="href" value="${esc(item.href)}">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="description" value="${esc(item.description)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Image URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="image" id="progImg_${idx}" value="${esc(item.image)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="progImg_${idx}" data-image-type="home"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.program-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateProgramItems(items) {
|
||||
document.getElementById('programItemsContainer').innerHTML = '';
|
||||
items.forEach(item => addProgramItem(item));
|
||||
}
|
||||
|
||||
// ── Request Info Programs ────────────────────────────────────────────────
|
||||
function addRequestInfoProgram(val = '') {
|
||||
const c = document.getElementById('requestInfoProgramsContainer');
|
||||
const html = `
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control ri-program-input" value="${esc(val)}" placeholder="Program name">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateRequestInfoPrograms(programs) {
|
||||
document.getElementById('requestInfoProgramsContainer').innerHTML = '';
|
||||
programs.forEach(p => addRequestInfoProgram(p));
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────
|
||||
function esc(val) {
|
||||
if (!val) return '';
|
||||
return String(val).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Reset all changes to last saved state?')) {
|
||||
populateAll(originalFormData);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDirectImageUpload(e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
const file = this.files[0];
|
||||
const imageType = document.getElementById("currentImageType").value;
|
||||
const targetInput = document.getElementById("currentTargetInput").value;
|
||||
function updateImagePreview(inputId, imagePath) {
|
||||
if (!imagePath) return;
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
const card = input.closest('.card');
|
||||
const preview = card ? card.querySelector('.uploaded-preview') : null;
|
||||
if (preview) preview.src = imagePath;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: "POST", body: formData });
|
||||
const result = await response.json();
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Dùng persistent hidden file input để tránh browser block programmatic click
|
||||
let fileInput = document.getElementById('__globalFileInput');
|
||||
if (!fileInput) {
|
||||
fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.id = '__globalFileInput';
|
||||
fileInput.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;width:1px;height:1px;';
|
||||
document.body.appendChild(fileInput);
|
||||
}
|
||||
|
||||
if (result.success && result.path) {
|
||||
const input = document.getElementById(targetInput);
|
||||
fileInput.value = '';
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`) ||
|
||||
document.querySelector(`[onclick*="${targetInput}"]`);
|
||||
if (uploadBtn) uploadBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: 'POST', body: formData });
|
||||
if (!response.ok) throw new Error('Upload failed');
|
||||
const result = await response.json();
|
||||
if (!result.success) throw new Error(result.error || 'Upload failed');
|
||||
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[data-target-input-id="${targetInput}"]`);
|
||||
if (input) {
|
||||
input.value = result.path;
|
||||
// Cập nhật preview nếu có img ngay sau input group
|
||||
const previewImg = input.closest('.input-group')?.nextElementSibling?.querySelector('img');
|
||||
if (previewImg) {
|
||||
previewImg.src = result.path;
|
||||
previewImg.classList.remove('d-none');
|
||||
const previewUrl = result.path.startsWith('http') ? result.path : window.location.origin + result.path;
|
||||
let preview = input.closest('.card')?.querySelector('.uploaded-preview');
|
||||
if (preview) {
|
||||
preview.src = previewUrl;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.maxHeight = '150px';
|
||||
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
}
|
||||
showToast("Success", "Image uploaded successfully", "success");
|
||||
} else {
|
||||
throw new Error(result.error || "Upload failed");
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
alert('Upload failed: ' + err.message);
|
||||
} finally {
|
||||
if (uploadBtn) uploadBtn.disabled = false;
|
||||
fileInput.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
showToast("Error", "Upload failed: " + error.message, "error");
|
||||
}
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
function showToast(title, message, type = "info") {
|
||||
let container = document.querySelector(".toast-container") || (() => {
|
||||
const c = document.createElement("div");
|
||||
c.className = "toast-container position-fixed top-0 end-0 p-3";
|
||||
document.body.appendChild(c);
|
||||
return c;
|
||||
})();
|
||||
|
||||
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.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"></button></div>`;
|
||||
container.appendChild(toast);
|
||||
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
|
||||
toast.addEventListener("hidden.bs.toast", () => toast.remove());
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user