Merge branch 'develop' into fea/dat-20042026-CMS-Partnerships-Accreditation-History-Admissions-Policies

This commit is contained in:
Tống Thành Đạt
2026-04-22 18:08:53 +07:00
80 changed files with 3334 additions and 19058 deletions
+812
View File
@@ -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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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>
-967
View File
@@ -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>
-791
View File
@@ -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
+45 -127
View File
@@ -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">
@@ -286,17 +212,19 @@
<span>Menu Header API</span>
</div>
</td>
<td><code>/api/header</code></td>
<td><code>/api/header-menu</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get menu header data</td>
<td>
<a href="/api/header" class="btn btn-sm btn-outline-primary" target="_blank">
<a href="/api/header-menu" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</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,8 @@
</a>
</td>
</tr>
<!--Programmes API-->
<tr>
<td>
<div class="d-flex align-items-center">
@@ -468,27 +407,6 @@
</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-graduation-cap" style="color: var(--primary-color);"></i>
</div>
<span>Programme Detail API</span>
</div>
</td>
<td><code>/api/programmes/:id</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>Single programme by slug ID</td>
<td>
<a href="/api/programmes" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
</tbody>
</table>
</div>
@@ -515,7 +433,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>
-98
View File
@@ -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">&times;</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>
-100
View File
@@ -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">&times;</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>
-126
View File
@@ -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">&times;</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
View File
File diff suppressed because it is too large Load Diff
+69 -544
View File
@@ -12,25 +12,13 @@
<div class="col-12">
<div class="content-with-fixed-buttons">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="topbarJson" id="topbarJson" />
<input type="hidden" name="logo" id="logoInput" />
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar" />
<input type="hidden" name="menuUpdates" id="menuUpdates" />
<input type="hidden" name="activeTab" id="activeTabInput" value="logo" />
<!-- 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 <%= activeTab === 'topbar' ? 'active' : '' %>"
data-bs-toggle="tab"
href="#topbar"
role="tab"
>
<i class="fas fa-bars me-2"></i>Topbar
</a>
</li>
<li class="nav-item">
<a
class="nav-link <%= activeTab === 'logo' ? 'active' : '' %>"
@@ -41,6 +29,16 @@
<i class="fas fa-image me-2"></i>Logo
</a>
</li>
<li class="nav-item">
<a
class="nav-link <%= activeTab === 'buttons' ? 'active' : '' %>"
data-bs-toggle="tab"
href="#buttons"
role="tab"
>
<i class="fas fa-mouse-pointer me-2"></i>Buttons
</a>
</li>
<li class="nav-item">
<a
class="nav-link <%= activeTab === 'menu' ? 'active' : '' %>"
@@ -56,79 +54,6 @@
<div class="card-body">
<div class="tab-content">
<!-- Topbar Tab -->
<div class="tab-pane fade <%= activeTab === 'topbar' ? 'show active' : '' %>" id="topbar" role="tabpanel">
<div class="row g-4">
<!-- Contact Information -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-phone me-2"></i>Contact Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Phone Number</label>
<input
type="text"
class="form-control"
id="contactPhone"
value="<%= data.topbar.contactInfo.phone %>"
placeholder="+1 234 567 890"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Email Address</label>
<input
type="email"
class="form-control"
id="contactEmail"
value="<%= data.topbar.contactInfo.email || '' %>"
placeholder="info@example.com"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Location</label>
<input
type="text"
class="form-control"
id="contactLocation"
value="<%= data.topbar.contactInfo.location || '' %>"
placeholder="69 Street, 5th Avenue LA, United States"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Social Links -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-share-alt me-2"></i>Social Media Links
</h6>
<button
type="button"
class="btn btn-primary btn-sm"
id="addSocialLink"
>
<i class="fas fa-plus me-1"></i>Add Social Link
</button>
</div>
<div class="card-body">
<div id="socialLinksContainer" class="social-links-sortable">
<!-- Social links will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Logo Tab -->
<div class="tab-pane fade <%= activeTab === 'logo' ? 'show active' : '' %>" id="logo" role="tabpanel">
<div class="row g-4">
@@ -179,6 +104,52 @@
</div>
</div>
<!-- Buttons Tab -->
<div class="tab-pane fade <%= activeTab === 'buttons' ? 'show active' : '' %>" id="buttons" role="tabpanel">
<div class="row g-4">
<!-- Sign In Button -->
<div class="col-md-6">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-sign-in-alt me-2"></i>Sign In Button</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="signInLabel"
value="<%= data.signInButton.label %>" placeholder="Sign In" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" id="signInHref"
value="<%= data.signInButton.href %>" placeholder="/signin" />
</div>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-6">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-mouse-pointer me-2"></i>CTA Button</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="ctaLabel"
value="<%= data.ctaButton.label %>" placeholder="Request Info" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control" id="ctaHref"
value="<%= data.ctaButton.href %>" placeholder="/request" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Menu Structure Tab -->
<div class="tab-pane fade <%= activeTab === 'menu' ? 'show active' : '' %>" id="menu" role="tabpanel">
<%- include('menu') %>
@@ -202,12 +173,6 @@
<script>
document.addEventListener('DOMContentLoaded', function () {
let socialLinkIndex = 0;
// Initialize social links from data
initializeSocialLinks();
updateHiddenInputs();
// Safely remove any lingering modal backdrops on page load/navigation
function cleanupModals() {
// Basic reset
@@ -291,7 +256,7 @@
}
// Attach listeners to all inputs for change detection
const headerInputs = document.querySelectorAll('#topbar input, #logo input');
const headerInputs = document.querySelectorAll('#logo input, #buttons input');
headerInputs.forEach(input => {
input.addEventListener('input', markChanged);
input.addEventListener('change', markChanged);
@@ -338,7 +303,6 @@
if (saveHeaderBtn) {
saveHeaderBtn.addEventListener('click', async function (e) {
console.log('=== TRACE: saveHeaderBtn Clicked (Unified) ===');
updateHiddenInputs();
const submitBtn = this;
const originalText = submitBtn.innerHTML;
@@ -346,11 +310,17 @@
submitBtn.disabled = true;
try {
// 1. Collect and Save Topbar & Logo
// 1. Collect and Save Header Data (Logo + Buttons)
const headerData = {
topbarJson: document.getElementById('topbarJson').value,
logo: document.getElementById('logoInput').value,
activeTab: document.getElementById('activeTabInput').value
logo: document.getElementById('logoImage').value,
signInButton: {
label: document.getElementById('signInLabel').value,
href: document.getElementById('signInHref').value,
},
ctaButton: {
label: document.getElementById('ctaLabel').value,
href: document.getElementById('ctaHref').value,
},
};
const headerResponse = await fetch('/admin/header/update', {
@@ -382,7 +352,7 @@
}
} catch (error) {
console.error('=== TRACE: Unified Save ERROR ===', error);
showNotification('Lỗi: ' + error.message, 'error');
showNotification('Error: ' + error.message, 'error');
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
@@ -390,416 +360,6 @@
});
}
function updateHiddenInputs() {
const topbarData = {
contactInfo: {
phone: document.getElementById('contactPhone').value || '',
email: document.getElementById('contactEmail').value || '',
location: document.getElementById('contactLocation').value || ''
},
socialLinks: []
};
// Collect social links
document.querySelectorAll('.social-platform').forEach((input) => {
const platform = input.value.trim();
const urlInput = document.querySelector(`.social-url[data-index="${input.dataset.index}"]`);
const iconInput = document.querySelector(`.social-icon[data-index="${input.dataset.index}"]`);
const url = urlInput ? urlInput.value.trim() : '';
const icon = iconInput ? iconInput.value.trim() : '';
if (platform && url) {
topbarData.socialLinks.push({
platform: platform,
url: url,
icon: icon
});
}
});
document.getElementById('topbarJson').value = JSON.stringify(topbarData);
document.getElementById('logoInput').value = document.getElementById('logoImage').value || '';
try {
const menuUpdates = collectMenuUpdates();
document.getElementById('menuUpdates').value = JSON.stringify(menuUpdates);
} catch (e) {
console.error('Error collecting menu updates:', e);
document.getElementById('menuUpdates').value = '[]';
}
}
/**
* Collect menu updates from the menu tree
* Returns an empty array if no menu tree exists
*/
function collectMenuUpdates() {
// For now, return empty array as menu structure management is coming soon
// This function can be expanded when menu tree functionality is implemented
return [];
}
function initializeSocialLinks() {
const container = document.getElementById('socialLinksContainer');
if (!container) return;
// Extract social links safely from EJS data
let socialLinks = [];
try {
socialLinks = <%- JSON.stringify(data.topbar.socialLinks || []) %>;
} catch (e) {
console.error('Error parsing social links data:', e);
}
if (socialLinks.length === 0) {
// Add default platforms if no social links exist
const platforms = ['linkedin', 'twitter', 'instagram', 'youtube'];
platforms.forEach((platform, index) => {
addSocialLinkRow(platform, '', `fa-brands fa-${platform}`, index);
});
socialLinkIndex = platforms.length;
} else {
// Load existing social links
socialLinks.forEach((social, index) => {
const platform = social.platform || '';
const url = social.url || '';
const icon = social.icon || '';
addSocialLinkRow(platform, url, icon, index);
});
socialLinkIndex = socialLinks.length;
}
}
function addSocialLinkRow(platform, url, icon, index) {
const container = document.getElementById('socialLinksContainer');
const socialLink = document.createElement('div');
socialLink.className = 'card mb-3 border social-link-item';
socialLink.dataset.platform = platform;
// Escape HTML to prevent XSS
const escapedPlatform = (platform || '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
const escapedUrl = (url || '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
const escapedIcon = (icon || '').replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
socialLink.innerHTML = `
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-1 d-flex justify-content-center align-items-center w-auto">
<label class="form-label fw-medium">&nbsp;</label>
<div class="drag-handle" title="Drag to reorder" style="cursor: grab; font-size: 1.2rem; color: #999; user-select: none;">
<i class="fas fa-grip-vertical"></i>
</div>
</div>
<div class="col-md-2">
<label class="form-label fw-medium">Platform</label>
<input type="text" class="form-control social-platform" value="${escapedPlatform}" data-index="${index}" disabled />
</div>
<div class="col-md-4">
<label class="form-label fw-medium">URL</label>
<input type="text" class="form-control social-url" value="${escapedUrl}" data-index="${index}" placeholder="https://..." />
</div>
<div class="col-md-3">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control social-icon" value="${escapedIcon}" data-index="${index}" />
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-primary btn-sm edit-social-link mx-3 rounded" style="transform: none" data-index="${index}" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm remove-social-link rounded" data-index="${index}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(socialLink);
}
/* ============================================
DRAG & DROP STYLING
============================================ */
const dragDropStyles = document.createElement('style');
dragDropStyles.textContent = `
.social-link-item {
transition: transform 0.2s ease;
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
cursor: grab !important;
}
.drag-handle:active {
cursor: grabbing !important;
}
.drag-handle:hover {
background-color: #f0f0f0;
color: #0d6efd;
}
/* SortableJS Classes */
.social-ghost {
opacity: 0.4;
border: 2px dashed #0d6efd !important;
background-color: #f8f9fa !important;
}
.social-chosen {
background-color: #eef3ff !important;
box-shadow: 0 5px 15px rgba(0,0,0,0.1) !important;
}
.social-drag {
opacity: 0.9;
}
/* Fix Modal Freeze & Z-Index issues */
body.modal-open {
overflow: hidden !important;
padding-right: 0 !important;
}
`;
document.head.appendChild(dragDropStyles);
document.addEventListener('click', function (e) {
if (e.target.closest('.remove-social-link')) {
e.preventDefault();
const btn = e.target.closest('.remove-social-link');
const index = btn.dataset.index;
const platformInput = document.querySelector(`.social-platform[data-index="${index}"]`);
const platform = platformInput.value;
if (confirm(`Delete ${platform} social link?`)) {
deleteSocialLink(platform, btn.closest('.card'));
}
}
if (e.target.closest('.edit-social-link')) {
e.preventDefault();
const btn = e.target.closest('.edit-social-link');
const index = btn.dataset.index;
const platformInput = document.querySelector(`.social-platform[data-index="${index}"]`);
const urlInput = document.querySelector(`.social-url[data-index="${index}"]`);
const iconInput = document.querySelector(`.social-icon[data-index="${index}"]`);
const platform = platformInput.value;
const url = urlInput.value;
const icon = iconInput.value;
showEditSocialLinkModal(platform, url, icon);
}
if (e.target.closest('#addSocialLink')) {
e.preventDefault();
showAddSocialLinkModal();
}
});
function deleteSocialLink(platform, cardElement) {
if (confirm(`Delete ${platform} social link?`)) {
cardElement.remove();
updateHiddenInputs();
markChanged();
}
}
function showAddSocialLinkModal() {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'addSocialLinkModal';
modal.setAttribute('tabindex', '-1');
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Social Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Platform <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newSocialPlatform" placeholder="e.g., linkedin, twitter, instagram, youtube, facebook" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newSocialUrl" placeholder="https://..." />
</div>
<div class="mb-3">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control" id="newSocialIcon" placeholder="e.g., fa-brands fa-linkedin" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveSocialLink">Save</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
document.getElementById('saveSocialLink').addEventListener('click', function() {
const platform = document.getElementById('newSocialPlatform').value.trim();
const url = document.getElementById('newSocialUrl').value.trim();
const icon = document.getElementById('newSocialIcon').value.trim();
if (!platform) {
alert('Vui lòng nhập tên nền tảng');
return;
}
if (!url) {
alert('Vui lòng nhập URL');
return;
}
// Check if platform already exists
const existingPlatforms = Array.from(document.querySelectorAll('.social-platform')).map(el => el.value);
if (existingPlatforms.includes(platform)) {
alert(`${platform} đã tồn tại`);
return;
}
addSocialLinkViaAPI(platform, url, icon || `fa-brands fa-${platform}`, bsModal, modal);
});
bsModal.show();
// Focus on platform input
setTimeout(() => {
document.getElementById('newSocialPlatform').focus();
}, 500);
}
function showEditSocialLinkModal(platform, url, icon) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'editSocialLinkModal';
modal.setAttribute('tabindex', '-1');
const platformDisplay = platform ? platform.charAt(0).toUpperCase() + platform.slice(1) : 'Social';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit ${platformDisplay} Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-medium">Platform <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSocialPlatform" value="${platform}" placeholder="e.g., linkedin, twitter, instagram" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">URL <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSocialUrl" value="${url}" placeholder="https://www.example.com" />
</div>
<div class="mb-3">
<label class="form-label fw-medium">Icon Class</label>
<input type="text" class="form-control" id="editSocialIcon" value="${icon}" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="updateSocialLink">Update</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
document.getElementById('updateSocialLink').addEventListener('click', function() {
const newPlatform = document.getElementById('editSocialPlatform').value.trim();
const newUrl = document.getElementById('editSocialUrl').value.trim();
const newIcon = document.getElementById('editSocialIcon').value.trim();
if (!newPlatform) {
alert('Vui lòng nhập tên nền tảng');
return;
}
if (!newUrl) {
alert('Vui lòng nhập URL');
return;
}
updateSocialLinkViaAPIModal(platform, newPlatform, newUrl, newIcon, bsModal, modal);
});
bsModal.show();
// Focus on platform input
setTimeout(() => {
document.getElementById('editSocialPlatform').focus();
}, 500);
}
function addSocialLinkViaAPI(platform, url, icon, modal, modalElement) {
addSocialLinkRow(platform, url, icon, Date.now());
updateHiddenInputs();
markChanged();
modal.hide();
modalElement.remove();
}
function updateSocialLinkViaAPIModal(oldPlatform, newPlatform, url, icon, modal, modalElement) {
newPlatform = newPlatform.toLowerCase().trim();
// Update the input fields in the DOM
const platformInputs = document.querySelectorAll(`.social-platform`);
const urlInputs = document.querySelectorAll(`.social-url`);
const iconInputs = document.querySelectorAll(`.social-icon`);
let found = false;
for (let i = 0; i < platformInputs.length; i++) {
if (platformInputs[i].value.toLowerCase() === oldPlatform.toLowerCase()) {
platformInputs[i].value = newPlatform;
urlInputs[i].value = url;
if (iconInputs[i]) iconInputs[i].value = icon;
found = true;
break;
}
}
if (found) {
modal.hide();
modalElement.remove();
updateHiddenInputs();
markChanged();
}
}
/**
* Show toast notification at top of page
@@ -990,41 +550,6 @@
previewContainer.appendChild(img);
}
}
// Initialize Sortable for Social Links
const socialLinksContainer = document.getElementById('socialLinksContainer');
const SortableLib = window.Sortable || Sortable;
console.log('=== TRACE: Social Sortable Init ===', {
containerExists: !!socialLinksContainer,
sortableDefined: typeof SortableLib !== 'undefined'
});
if (socialLinksContainer && typeof SortableLib !== 'undefined') {
try {
new SortableLib(socialLinksContainer, {
animation: 150,
handle: '.drag-handle',
ghostClass: 'social-ghost',
chosenClass: 'social-chosen',
dragClass: 'social-drag',
forceFallback: true, // Use transition-based dragging for better compatibility
onStart: function() {
console.log('=== TRACE: Social Drag Started ===');
},
onEnd: function() {
console.log('=== TRACE: Social Drag Ended ===');
updateHiddenInputs();
markChanged();
}
});
console.log('=== TRACE: Sortable initialized for socialLinksContainer ===');
} catch (err) {
console.error('=== TRACE: Sortable Init Error ===', err);
}
} else {
console.warn('SortableJS not loaded or socialLinksContainer not found');
}
});
</script>
+624 -180
View File
@@ -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, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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>
-742
View File
@@ -1,742 +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 Pricing page</p>
</div>
<div>
<a href="<%= frontendUrl %>/pricing/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Pricing Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="pricingForm" action="/admin/pricing/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="pricingSection" id="pricingSectionJson">
<input type="hidden" name="plans" id="plansJson">
<input type="hidden" name="testimonials" id="testimonialsJson">
<!-- 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="#pricingSection" role="tab">
<i class="fas fa-tags me-2"></i>Pricing Section
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#plans" role="tab">
<i class="fas fa-dollar-sign me-2"></i>Plans
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
<i class="fas fa-quote-right me-2"></i>Testimonials
</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="pricing">
<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-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Section Tab -->
<div class="tab-pane fade" id="pricingSection" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Pricing Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="pricingSectionSubtitle"
value="<%= data.pricingSection?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="pricingSectionHeading"
value="<%= data.pricingSection?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="pricingSectionDescription"
rows="3"><%= data.pricingSection?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Plans Tab -->
<div class="tab-pane fade" id="plans" role="tabpanel">
<!-- Monthly Plans -->
<div class="card border shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Monthly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('monthly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="monthlyPlansContainer">
<% if (data.plans?.monthly && data.plans.monthly.length> 0) { %>
<% data.plans.monthly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="monthly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<!-- Yearly Plans -->
<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">Yearly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('yearly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="yearlyPlansContainer">
<% if (data.plans?.yearly && data.plans.yearly.length> 0) { %>
<% data.plans.yearly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="yearly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Testimonials Tab -->
<div class="tab-pane fade" id="testimonials" role="tabpanel">
<div class="card border shadow-sm mb-4">
<div class="card-body">
<h6 class="fw-medium mb-3">Testimonials Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="testimonialsSubtitle"
value="<%= data.testimonials?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Text</label>
<input type="text" class="form-control" id="testimonialsButtonText"
value="<%= data.testimonials?.buttonText || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Link</label>
<input type="text" class="form-control" id="testimonialsButtonLink"
value="<%= data.testimonials?.buttonLink || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Section Image</label>
<div class="input-group">
<input type="text" class="form-control" id="testimonialsImage"
value="<%= data.testimonials?.image || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsImage" data-image-type="pricing">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<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">Testimonial Items</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addTestimonial()">
<i class="fas fa-plus"></i> Add Testimonial
</button>
</div>
<div id="testimonialsContainer">
<% if (data.testimonials?.items && data.testimonials.items.length> 0) { %>
<% data.testimonials.items.forEach((item, index)=> { %>
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text"
class="form-control testimonial-name"
value="<%= item.name || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text"
class="form-control testimonial-role"
value="<%= item.role || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1" <%=item.rating===1
? 'selected' : '' %>>1 Star</option>
<option value="2" <%=item.rating===2
? 'selected' : '' %>>2 Stars</option>
<option value="3" <%=item.rating===3
? 'selected' : '' %>>3 Stars</option>
<option value="4" <%=item.rating===4
? 'selected' : '' %>>4 Stars</option>
<option value="5" <%=item.rating===5
? 'selected' : '' %>>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content"
rows="3"><%= item.content || '' %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="pricingDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('pricingDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
updateAllJsonInputs();
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('pricingForm');
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 || '',
shapeImage: originalFormData?.hero?.shapeImage || '/assets/img/inner-page/shape.png',
breadcrumb: originalFormData?.hero?.breadcrumb || [],
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Pricing Section data
const pricingSectionData = {
subtitle: document.getElementById('pricingSectionSubtitle').value || '',
heading: document.getElementById('pricingSectionHeading').value || '',
description: document.getElementById('pricingSectionDescription').value || '',
};
document.getElementById('pricingSectionJson').value = JSON.stringify(pricingSectionData);
// Plans data
const monthlyPlans = [];
document.querySelectorAll('#monthlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
monthlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
const yearlyPlans = [];
document.querySelectorAll('#yearlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
yearlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
document.getElementById('plansJson').value = JSON.stringify({
monthly: monthlyPlans,
yearly: yearlyPlans,
});
// Testimonials data
const testimonialItems = [];
document.querySelectorAll('#testimonialsContainer .testimonial-item').forEach(item => {
testimonialItems.push({
name: item.querySelector('.testimonial-name').value || '',
role: item.querySelector('.testimonial-role').value || '',
rating: parseInt(item.querySelector('.testimonial-rating').value) || 5,
content: item.querySelector('.testimonial-content').value || '',
});
});
const testimonialsData = {
subtitle: document.getElementById('testimonialsSubtitle').value || '',
heading: document.getElementById('testimonialsHeading').value || '',
buttonText: document.getElementById('testimonialsButtonText').value || '',
buttonLink: document.getElementById('testimonialsButtonLink').value || '',
buttonIcon: originalFormData?.testimonials?.buttonIcon || 'fa-solid fa-arrow-right',
image: document.getElementById('testimonialsImage').value || '',
items: testimonialItems,
};
document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData);
}
function addPlan(type) {
const container = document.getElementById(type + 'PlansContainer');
const html = `
<div class="card mb-3 plan-item" data-type="${type}">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name" value="">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price" value="">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text" class="form-control plan-currency" value="$">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period" value="mo">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default" selected>Default</option>
<option value="style-2">Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text" class="form-control plan-button-text" value="Get Started Today">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text" class="form-control plan-button-link" value="/pricing">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text" class="form-control plan-button-icon" value="fa-solid fa-arrow-right">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per line)</label>
<textarea class="form-control plan-features" rows="4"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removePlan(button) {
if (confirm('Are you sure you want to remove this plan?')) {
button.closest('.plan-item').remove();
}
}
function addTestimonial() {
const container = document.getElementById('testimonialsContainer');
const html = `
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control testimonial-name" value="">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text" class="form-control testimonial-role" value="">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1">1 Star</option>
<option value="2">2 Stars</option>
<option value="3">3 Stars</option>
<option value="4">4 Stars</option>
<option value="5" selected>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content" rows="3"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeTestimonial(button) {
if (confirm('Are you sure you want to remove this testimonial?')) {
button.closest('.testimonial-item').remove();
}
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function
function openImageUploader(targetInput, imageType) {
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 {
// Send imageType via query string as controller expects req.query.imageType
const uploadUrl = '/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general');
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.path) {
document.getElementById(targetInput).value = result.path;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.path);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>
-755
View File
@@ -1,755 +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);">Service Details: <%= service.name %></h1>
<p class="text-muted mb-0">Edit detailed content for <%= service.name %> service</p>
</div>
<div>
<a href="/admin/service" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Services
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/<%= service.slug %>/details/update" method="POST" class="content-with-fixed-buttons" id="serviceDetailsForm">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="details" id="detailsJson">
<input type="hidden" name="features" id="featuresJson">
<input type="hidden" name="faq" id="faqJson">
<!-- 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="#basic-info" role="tab">
<i class="fas fa-info-circle me-2"></i>Basic Information
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#key-features" role="tab">
<i class="fas fa-star me-2"></i>Key Features
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#faq-section" role="tab">
<i class="fas fa-question-circle me-2"></i>FAQ Section
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Basic Information Tab -->
<div class="tab-pane fade show active" id="basic-info" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-5">
<label class="form-label fw-medium">Main Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="mainImage" name="mainImage"
value="<%= service.details?.mainImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="mainImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="form-text text-muted">Recommended size: 800x600px</small>
</div>
<div class="col-md-7">
<div id="mainImagePreview">
<% if (service.details?.mainImage) { %>
<img src="<%= getFullImageUrl(service.details.mainImage) %>" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="detailsTitle" name="title"
value="<%= service.details?.title || service.name %>">
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="detailsDescription" name="description" rows="3"><%= service.details?.description || service.description %></textarea>
</div>
</div>
<!-- Overview Section -->
<div class="row mt-4">
<div class="col-12">
<div class="card border">
<div class="card-header bg-light">
<h6 class="mb-0">Overview Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Overview Title</label>
<input type="text" class="form-control" id="overviewTitle" name="overviewTitle"
value="<%= service.details?.overviewTitle || 'Service Overview' %>">
</div>
<div class="col-md-12">
<label class="form-label">Overview Description</label>
<textarea class="form-control" id="overviewDescription" name="overviewDescription" rows="4"><%= service.details?.overviewDescription || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label">Additional Description</label>
<textarea class="form-control" id="additionalDescription" name="additionalDescription" rows="3"><%= service.details?.additionalDescription || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Key Features Tab -->
<div class="tab-pane fade" id="key-features" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- Features Header -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Features Title</label>
<input type="text" class="form-control" id="keyFeaturesTitle" name="keyFeaturesTitle"
value="<%= service.details?.keyFeaturesTitle || 'Key Features' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Features Image</label>
<div class="input-group">
<input type="text" class="form-control" id="keyFeaturesImage" name="keyFeaturesImage"
value="<%= service.details?.keyFeaturesImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="keyFeaturesImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<!-- Key Features Image Preview -->
<div class="row mt-3">
<div class="col-12">
<div id="keyFeaturesImagePreview">
<% if (service.details?.keyFeaturesImage) { %>
<img src="<%= getFullImageUrl(service.details.keyFeaturesImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">
<% } %>
</div>
</div>
</div>
<!-- Features List -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Features</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFeature()">
<i class="fas fa-plus"></i> Add Feature
</button>
</div>
<div id="featuresContainer">
<% if (service.details?.features && service.details.features.length > 0) { %>
<% service.details.features.forEach((feature, index) => { %>
<div class="card mb-3 feature-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">Feature <%= index + 1 %></h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Title</label>
<input type="text" class="form-control feature-title"
value="<%= feature.title || '' %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control feature-description" rows="2" required><%= feature.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FAQ Section Tab -->
<div class="tab-pane fade" id="faq-section" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- FAQ Header -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">FAQ Title</label>
<input type="text" class="form-control" id="faqTitle" name="faqTitle"
value="<%= service.details?.faqTitle || 'Frequently Asked Questions' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">FAQ Image</label>
<div class="input-group">
<input type="text" class="form-control" id="faqImage" name="faqImage"
value="<%= service.details?.faqImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="faqImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<!-- FAQ Image Preview -->
<div class="row mt-3">
<div class="col-12">
<div id="faqImagePreview">
<% if (service.details?.faqImage) { %>
<img src="<%= getFullImageUrl(service.details.faqImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">
<% } %>
</div>
</div>
</div>
<!-- FAQ List -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">FAQ Items</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFAQ()">
<i class="fas fa-plus"></i> Add FAQ
</button>
</div>
<div id="faqContainer">
<% if (service.details?.faq && service.details.faq.length > 0) { %>
<% service.details.faq.forEach((faq, index) => { %>
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ <%= index + 1 %></h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID (Auto-generated)</label>
<input type="text" class="form-control faq-id"
value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
</div>
<div class="col-md-6">
<label class="form-label">Expanded by Default</label>
<select class="form-control faq-expanded">
<option value="false" <%= !faq.isExpanded ? 'selected' : '' %>>No</option>
<option value="true" <%= faq.isExpanded ? 'selected' : '' %>>Yes</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question"
value="<%= faq.question || '' %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer" rows="3" required><%= faq.answer || '' %></textarea>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<button type="reset" class="btn btn-secondary">
<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>
<!-- Scripts -->
<script>
let originalFormData = null;
let featureIndex = <%= service.details?.features?.length || 0 %>;
let faqIndex = <%= service.details?.faq?.length || 0 %>;
document.addEventListener('DOMContentLoaded', function() {
// Initialize form data
originalFormData = <%- JSON.stringify(service) %>;
// Set initial JSON values
updateAllJsonInputs(originalFormData);
// Initialize form handlers
initializeFormHandlers();
});
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('serviceDetailsForm');
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);
showError('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Initialize 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);
});
});
// Initialize image input change listeners for manual URL input
const imageInputs = ['mainImage', 'keyFeaturesImage', 'faqImage'];
imageInputs.forEach(inputId => {
const input = document.getElementById(inputId);
if (input) {
input.addEventListener('input', function() {
updateImagePreviewAfterUpload(inputId, this.value);
});
}
});
}
function addFeature() {
const container = document.getElementById('featuresContainer');
const featureHtml = `
<div class="card mb-3 feature-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">Feature ${featureIndex + 1}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Title</label>
<input type="text" class="form-control feature-title" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control feature-description" rows="2" required></textarea>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', featureHtml);
featureIndex++;
}
function removeFeature(button) {
const featureItem = button.closest('.feature-item');
if (featureItem) {
featureItem.remove();
}
}
function addFAQ() {
const container = document.getElementById('faqContainer');
const newFaqId = generateFAQId();
const faqNumber = document.querySelectorAll('.faq-item').length + 1;
const faqHtml = `
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ ${faqNumber}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID (Auto-generated)</label>
<input type="text" class="form-control faq-id"
value="${newFaqId}" readonly>
</div>
<div class="col-md-6">
<label class="form-label">Expanded by Default</label>
<select class="form-control faq-expanded">
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer" rows="3" required></textarea>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', faqHtml);
faqIndex++;
}
function updateFAQId(questionInput) {
// Không cần update ID nữa vì đã tự động theo số thứ tự
}
function removeFAQ(button) {
const faqItem = button.closest('.faq-item');
if (faqItem) {
faqItem.remove();
// Cập nhật lại số thứ tự và ID của tất cả FAQ
updateFAQNumbers();
}
}
function generateFAQId() {
// Đếm số lượng FAQ hiện tại và tạo ID tiếp theo
const existingFAQs = document.querySelectorAll('.faq-item');
const nextNumber = existingFAQs.length + 1;
return `faq-${nextNumber}`;
}
function updateFAQNumbers() {
// Cập nhật lại tất cả FAQ ID và số thứ tự
const faqItems = document.querySelectorAll('.faq-item');
faqItems.forEach((item, index) => {
const number = index + 1;
const idInput = item.querySelector('.faq-id');
const titleElement = item.querySelector('h6');
if (idInput) {
idInput.value = `faq-${number}`;
}
if (titleElement) {
titleElement.textContent = `FAQ ${number}`;
}
});
}
function updateAllJsonInputs(data) {
// Collect basic details data
const details = {
title: data.details?.title || data.name,
description: data.details?.description || data.description,
mainImage: data.details?.mainImage || '',
overviewTitle: data.details?.overviewTitle || 'Service Overview',
overviewDescription: data.details?.overviewDescription || '',
additionalDescription: data.details?.additionalDescription || '',
keyFeaturesTitle: data.details?.keyFeaturesTitle || 'Key Features',
keyFeaturesImage: data.details?.keyFeaturesImage || '',
faqTitle: data.details?.faqTitle || 'Frequently Asked Questions',
faqImage: data.details?.faqImage || ''
};
document.getElementById('detailsJson').value = JSON.stringify(details);
document.getElementById('featuresJson').value = JSON.stringify(data.details?.features || []);
document.getElementById('faqJson').value = JSON.stringify(data.details?.faq || []);
}
function updateJsonData() {
// Collect basic details data
const details = {
title: document.getElementById('detailsTitle').value,
description: document.getElementById('detailsDescription').value,
mainImage: document.getElementById('mainImage').value,
overviewTitle: document.getElementById('overviewTitle').value,
overviewDescription: document.getElementById('overviewDescription').value,
additionalDescription: document.getElementById('additionalDescription').value,
keyFeaturesTitle: document.getElementById('keyFeaturesTitle').value,
keyFeaturesImage: document.getElementById('keyFeaturesImage').value,
faqTitle: document.getElementById('faqTitle').value,
faqImage: document.getElementById('faqImage').value
};
// Collect features data
const features = [];
document.querySelectorAll('.feature-item').forEach(item => {
features.push({
title: item.querySelector('.feature-title').value,
description: item.querySelector('.feature-description').value
});
});
// Collect FAQ data
const faq = [];
document.querySelectorAll('.faq-item').forEach(item => {
faq.push({
id: item.querySelector('.faq-id').value,
question: item.querySelector('.faq-question').value,
answer: item.querySelector('.faq-answer').value,
isExpanded: item.querySelector('.faq-expanded').value === 'true'
});
});
document.getElementById('detailsJson').value = JSON.stringify(details);
document.getElementById('featuresJson').value = JSON.stringify(features);
document.getElementById('faqJson').value = JSON.stringify(faq);
}
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
// Nếu đã là full URL thì return luôn
if (imagePath.startsWith('http')) {
return imagePath;
}
// Lấy backend URL
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
// Xử lý đường dẫn
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
// Function để cập nhật image preview sau khi upload
function updateImagePreviewAfterUpload(targetInput, imagePath) {
const fullImageUrl = getFullImageUrlJS(imagePath);
switch(targetInput) {
case 'mainImage':
const mainPreview = document.getElementById('mainImagePreview');
if (mainPreview) {
mainPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">`;
}
break;
case 'keyFeaturesImage':
const featuresPreview = document.getElementById('keyFeaturesImagePreview');
if (featuresPreview) {
featuresPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">`;
}
break;
case 'faqImage':
const faqPreview = document.getElementById('faqImagePreview');
if (faqPreview) {
faqPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">`;
}
break;
}
}
async function openImageUploader(targetInput, imageType) {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return reject(new Error('No file selected'));
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(imageType)}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update image preview based on target input
updateImagePreviewAfterUpload(targetInput, result.path);
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
resolve(result);
} catch (err) {
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
console.error('Upload error:', err);
showError('Upload failed: ' + (err.message || 'Unknown error'));
reject(err);
} finally {
fileInput.remove();
}
};
fileInput.click();
});
}
function showSuccess(message) {
// Create and show success alert
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showError(message) {
// Create and show error alert
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
</script>
<style>
.bottom-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid #dee2e6;
}
.content-with-fixed-buttons {
/* Remove bottom padding since buttons are no longer fixed */
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
.card-header h6 {
color: var(--primary-dark);
}
.text-decoration-underline {
text-decoration: underline;
color: var(--primary-dark);
}
/* Image Preview Styles */
#mainImagePreview, #keyFeaturesImagePreview, #faqImagePreview {
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 10px;
transition: all 0.3s ease;
}
#mainImagePreview:empty::before,
#keyFeaturesImagePreview:empty::before,
#faqImagePreview:empty::before {
content: "No image selected";
color: #6c757d;
font-style: italic;
}
#mainImagePreview img,
#keyFeaturesImagePreview img,
#faqImagePreview img {
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
-440
View File
@@ -1,440 +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);">Edit Service: <%= service.name %></h1>
<p class="text-muted mb-0">Update service information and settings</p>
</div>
<div>
<a href="/admin/service" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Services
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/<%= service.slug %>/edit" method="POST" id="editServiceForm">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--primary-dark);">
<i class="fas fa-edit me-2"></i>Service Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Service Name</label>
<input type="text" class="form-control" id="serviceName" name="name"
value="<%= service.name %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">
Slug
<small class="text-muted">(generated from name)</small>
<span id="slugAutoIndicator" class="badge bg-info ms-1" style="font-size: 0.7em;">EXISTING</span>
</label>
<div class="input-group">
<input type="text" class="form-control" id="serviceSlug" name="slug"
value="<%= service.slug %>" readonly>
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
<i class="fas fa-magic me-1"></i>Generate
</button>
</div>
<small class="form-text text-muted">URL-friendly version of the service name.</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Layout</label>
<select class="form-control" id="serviceLayout" name="layout">
<option value="left" <%= service.layout === 'left' ? 'selected' : '' %>>Left</option>
<option value="right" <%= service.layout === 'right' ? 'selected' : '' %>>Right</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-8">
<label class="form-label fw-medium">Image</label>
<div class="input-group">
<input type="text" class="form-control" id="serviceImage" name="image"
value="<%= service.image || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="serviceImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-4">
<div id="serviceImagePreview">
<% if (service.image) { %>
<img src="<%= getFullImageUrl(service.image) %>" class="img-thumbnail"
style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="serviceDescription" name="description"
rows="3" required><%= service.description %></textarea>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<a href="/admin/service" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>Cancel
</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Update Service
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let servicesData = []; // Will be populated for duplicate checking
document.addEventListener('DOMContentLoaded', function() {
// Load existing services data for duplicate checking
loadServicesData();
// Initialize form handlers
initializeFormHandlers();
});
async function loadServicesData() {
try {
const response = await fetch('/api/service');
const data = await response.json();
servicesData = data.services?.items || [];
} catch (error) {
console.error('Error loading services data:', error);
servicesData = [];
}
}
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('editServiceForm');
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>Updating...';
try {
// Check for duplicate slug before submitting
const slug = document.getElementById('serviceSlug').value.trim();
const currentSlug = '<%= service.slug %>';
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
showError('Service with this slug already exists. Please generate a new slug.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
return;
}
this.submit();
} catch (error) {
console.error('Error updating service:', error);
showError('Failed to update service. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
}
});
// Initialize 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);
});
});
// Image preview for service image
const serviceImageInput = document.getElementById('serviceImage');
if (serviceImageInput) {
serviceImageInput.addEventListener('input', function() {
updateImagePreview('serviceImagePreview', this.value);
});
}
// Generate slug from service name
const serviceNameInput = document.getElementById('serviceName');
const serviceSlugInput = document.getElementById('serviceSlug');
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
const generateSlugBtn = document.getElementById('generateSlugBtn');
if (serviceNameInput && serviceSlugInput && generateSlugBtn) {
// Generate slug button
generateSlugBtn.addEventListener('click', async function() {
const serviceName = serviceNameInput.value.trim();
if (serviceName) {
// Show loading state
const originalBtnHtml = this.innerHTML;
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
try {
const slug = await generateSlugFromText(serviceName);
const currentSlug = '<%= service.slug %>';
// Check for duplicate slug (excluding current service)
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
const uniqueSlug = generateUniqueSlug(slug);
serviceSlugInput.value = uniqueSlug;
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
} else {
serviceSlugInput.value = slug;
showSuccess('Slug generated successfully!');
}
if (slugAutoIndicator) {
slugAutoIndicator.textContent = 'GENERATED';
slugAutoIndicator.className = 'badge bg-success ms-1';
slugAutoIndicator.style.fontSize = '0.7em';
}
} catch (error) {
console.error('Error generating slug:', error);
showError('Failed to generate slug. Please try again.');
} finally {
// Restore button state
this.disabled = false;
this.innerHTML = originalBtnHtml;
}
} else {
showError('Please enter a service name first.');
}
});
}
}
// Generate slug using backend API
async function generateSlugFromText(text) {
try {
const response = await fetch('/admin/service/generate-slug', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: text })
});
const result = await response.json();
if (result.success) {
return result.slug;
} else {
throw new Error(result.message || 'Failed to generate slug');
}
} catch (error) {
console.error('Error generating slug:', error);
// Fallback to simple slug generation if API fails
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '');
}
}
// Check if slug already exists
function isSlugDuplicate(slug, excludeIndex = -1) {
return servicesData.some((service, index) => {
return service && service.slug === slug && index !== excludeIndex;
});
}
// Generate unique slug by appending number
function generateUniqueSlug(baseSlug) {
let counter = 1;
let uniqueSlug = baseSlug;
while (isSlugDuplicate(uniqueSlug, -1)) {
uniqueSlug = baseSlug + '-' + counter;
counter++;
}
return uniqueSlug;
}
function updateImagePreview(previewId, imagePath) {
const preview = document.getElementById(previewId);
if (imagePath) {
const fullImageUrl = getFullImageUrlJS(imagePath);
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
} else {
preview.innerHTML = '';
}
}
// Helper function để tạo full URL cho ảnh
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
if (imagePath.startsWith('http')) {
return imagePath;
}
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
async function openImageUploader(targetInput, imageType) {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return reject(new Error('No file selected'));
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=service`, {
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) throw new Error('Target input not found');
input.value = result.path;
// Update preview
if (targetInput === 'serviceImage') {
updateImagePreview('serviceImagePreview', result.path);
}
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
resolve(result);
} catch (err) {
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
console.error('Upload error:', err);
showError('Upload failed: ' + (err.message || 'Unknown error'));
reject(err);
} finally {
fileInput.remove();
}
};
fileInput.click();
});
}
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showWarning(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showError(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
</script>
<style>
.bottom-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid #dee2e6;
}
.card-header h5 {
color: var(--primary-dark);
}
</style>
File diff suppressed because it is too large Load Diff
-353
View File
@@ -1,353 +0,0 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
Travel Information Editor
</h1>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-primary preview-btn">
<i class="fas fa-eye me-2"></i>Preview
</button>
<button type="submit" form="travelForm" class="btn btn-primary" id="saveBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</div>
<form id="travelForm" action="/admin/travel/update" method="POST" class="needs-validation" novalidate>
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="page" id="pageJson">
<input type="hidden" name="content" id="contentJson">
<input type="hidden" name="enableScrollspy" id="enableScrollspyInput">
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Hero Section</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-5">
<div class="mb-3">
<label class="form-label">Background Image</label>
<div class="input-group">
<input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>" readonly>
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="travel">
<i class="fas fa-upload"></i>
</button>
</div>
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
</div>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px; width: 100%;">
<% if (data.hero?.backgroundImage) { %>
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Background image preview">
<% } else { %>
<div class="border rounded d-flex align-items-center justify-content-center h-100 bg-light">
<span class="text-muted">No image selected</span>
</div>
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label">Hero Title</label>
<textarea class="form-control" id="heroTitle" rows="2"><%= data.hero?.title || 'Travel Information' %></textarea>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Page Title</label>
<input type="text" class="form-control" id="pageTitle" value="<%= data.page?.title || 'Go and Grow Camp Travel Information' %>">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">As of / Year</label>
<input type="text" class="form-control" id="pageYear" value="<%= data.page?.year || '' %>">
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Editor</h5>
<p class="text-muted mb-0 small">Write content using the blog editor</p>
</div>
<div class="card-body">
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">SEO Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" class="form-control" id="metadataTitle" value="<%= data.page?.metadata?.title || '' %>">
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea class="form-control" id="metadataDescription" rows="3"><%= data.page?.metadata?.description || '' %></textarea>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enableScrollspy" <%= data.enableScrollspy ? 'checked' : '' %>>
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Tips</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Terms & Conditions:</h6>
<ul class="mb-0 small">
<li>Use <strong>Header 2</strong> for main sections</li>
<li>Use <strong>Header 3</strong> for subsections</li>
<li>Use <strong>Lists</strong> for terms items</li>
<li>Use <strong>Conclusion</strong> tool for important notes</li>
<li>Use <strong>Quote</strong> for legal references</li>
</ul>
<hr class="my-2">
<h6><i class="fas fa-keyboard me-2"></i>Keyboard Shortcuts:</h6>
<ul class="mb-0 small">
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>: Convert list item to header</li>
<li><kbd>Tab</kbd> in list: Indent item</li>
<li><kbd>Backspace</kbd> at start: Exit list</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Travel Information Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<iframe id="previewFrame" style="width: 100%; height: 600px; border: none;"></iframe>
</div>
</div>
</div>
</div>
<input type="file" id="directImageUpload" style="display: none;" accept="image/*">
<input type="hidden" id="currentImageType">
<input type="hidden" id="currentTargetInput">
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
<script type="module">
import BlogEditor from '/js/editor.js';
// Logic xử lý lọc dữ liệu để tránh duplicate video và xóa paragraph rỗng
class TravelContentManager {
cleanEditorData(editorData) {
const cleanedBlocks = [];
const seenVideoIds = new Set();
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|shorts\/)?([A-Za-z0-9_-]{11})/;
(editorData.blocks || []).forEach(block => {
if (!block) return;
// 1. Xử lý Video Embed (Deduplication)
if (block.type === 'embed') {
const bd = block.data || {};
const source = bd.source || bd.embed || '';
const match = source.match(youtubeRegex);
const vid = bd.videoId || (match ? match[1] : null);
if (vid) {
if (seenVideoIds.has(vid)) return; // Bỏ qua nếu đã có video này
seenVideoIds.add(vid);
}
cleanedBlocks.push(block);
return;
}
// 2. Xử lý Paragraph (Xóa dòng trống hoặc dòng chỉ chứa link đã embed)
if (block.type === 'paragraph') {
const text = (block.data?.text || '').toString().trim();
if (text === '') return; // Xóa paragraph rỗng
const match = text.match(youtubeRegex);
if (match && match[1]) {
// Nếu paragraph chỉ chứa link YouTube, và ta sẽ có/đã có block embed cho nó, thì bỏ qua paragraph
return;
}
}
cleanedBlocks.push(block);
});
return { ...editorData, blocks: cleanedBlocks };
}
}
document.addEventListener('DOMContentLoaded', async () => {
let blogEditorInstance = null;
const travelData = <%- JSON.stringify(data) %>;
const initialContent = travelData?.content || { blocks: [] };
try {
blogEditorInstance = new BlogEditor('editorjs', initialContent, 'travel');
window.blogEditorInstance = blogEditorInstance;
} catch (error) {
console.error('Error initializing BlogEditor:', error);
}
const form = document.getElementById('travelForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
// Lấy dữ liệu thô
const rawData = await blogEditorInstance.save();
// Làm sạch dữ liệu trước khi đóng gói JSON
const travelManager = new TravelContentManager();
const cleanedData = travelManager.cleanEditorData(rawData);
const heroData = {
title: document.getElementById('heroTitle').value.trim(),
backgroundImage: document.getElementById('heroBackgroundImage').value.trim(),
};
const pageData = {
title: document.getElementById('pageTitle').value.trim(),
year: document.getElementById('pageYear')?.value.trim(),
metadata: {
title: document.getElementById('metadataTitle').value.trim(),
description: document.getElementById('metadataDescription').value.trim(),
},
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
document.getElementById('pageJson').value = JSON.stringify(pageData);
document.getElementById('contentJson').value = JSON.stringify(cleanedData);
document.getElementById('enableScrollspyInput').value = document.getElementById('enableScrollspy').checked;
form.submit();
} catch (error) {
console.error('Save error:', error);
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Preview
const previewBtn = document.querySelector('.preview-btn');
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
previewBtn.addEventListener('click', async function () {
try {
const editorData = await blogEditorInstance.save();
const travelManager = new TravelContentManager();
const cleanedData = travelManager.cleanEditorData(editorData);
const formData = new FormData();
formData.append('content', JSON.stringify(cleanedData));
formData.append('heroTitle', document.getElementById('heroTitle').value);
formData.append('heroBackgroundImage', document.getElementById('heroBackgroundImage').value);
formData.append('pageTitle', document.getElementById('pageTitle').value);
formData.append('pageYear', document.getElementById('pageYear')?.value || '');
const response = await fetch('/admin/travel/preview', { method: 'POST', body: formData });
const html = await response.text();
const previewFrame = document.getElementById('previewFrame');
const blob = new Blob([html], { type: 'text/html' });
previewFrame.src = URL.createObjectURL(blob);
previewModal.show();
} catch (error) {
console.error('Preview error:', error);
}
});
// Image Upload Helpers
document.querySelectorAll('.btn-upload-image').forEach(btn => {
btn.addEventListener('click', function () {
document.getElementById('currentImageType').value = this.getAttribute('data-image-type');
document.getElementById('currentTargetInput').value = this.getAttribute('data-target-input');
document.getElementById('directImageUpload').click();
});
});
document.getElementById('directImageUpload').addEventListener('change', async function () {
if (!this.files || !this.files[0]) return;
const formData = new FormData();
formData.append('image', this.files[0]);
const imageType = document.getElementById('currentImageType').value;
const targetInput = document.getElementById('currentTargetInput').value;
try {
const resp = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
document.getElementById(targetInput).value = result.path;
if (targetInput === 'heroBackgroundImage') updateHeroImagePreview(result.path);
showToast('Uploaded successfully', 'success');
}
} catch (error) {
showToast('Upload failed', 'danger');
}
});
function updateHeroImagePreview(imageUrl) {
document.getElementById('heroImagePreview').innerHTML = `<img src="${imageUrl}" class="img-thumbnail" style="height: 200px; width: 100%; object-fit: cover;">`;
}
function showToast(message, type) {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
document.body.appendChild(toast);
new bootstrap.Toast(toast, { delay: 3000 }).show();
}
});
</script>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -105,7 +105,7 @@
<div class="card-body p-4 p-lg-5">
<div class="row align-items-center">
<div class="col-lg-8 mx-auto text-center">
<h2 class="fw-bold mb-4 text-white">Start Using CMS.HAILearning Today</h2>
<h2 class="fw-bold mb-4 text-white">Start Using CMS.LAMS Today</h2>
<p class="lead mb-4 text-white-50">Experience simple and effective API management system</p>
<a href="/admin/dashboard" class="btn btn-lg"
style="background-color: white; color: var(--primary-color); font-weight: 600; border-radius: 10px; padding: 12px 30px;">
-15
View File
@@ -1049,16 +1049,9 @@
<li><a class="dropdown-item <%= currentPath === '/admin/policies' ? 'active' : '' %>" href="/admin/policies">Policies</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/service' ? 'active' : '' %>"
href="/admin/service">Services</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/blog' ? 'active' : '' %>" href="/admin/blog">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/visa' ? 'active' : '' %>" href="/admin/visa">Visa</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
Us</a>
@@ -1067,14 +1060,6 @@
<a class="nav-link <%= currentPath === '/admin/student-support' ? 'active' : '' %>"
href="/admin/student-support">Student Support</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>"
href="/admin/appointment">Appointment</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/pricing' ? 'active' : '' %>"
href="/admin/pricing">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/programme">Programmes</a>
</li>