forked from UKSOURCE/cms.lams
Refactor Homepage, About, Footer (Controller,Model, View, Data), Update: Dashboard, Delete old file
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on the About page</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" id="aboutForm" action="/admin/about-us/update">
|
||||
<!-- Hidden JSON inputs -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="leadership" id="leadershipJson">
|
||||
<input type="hidden" name="learningModel" id="learningModelJson">
|
||||
<input type="hidden" name="accreditation" id="accreditationJson">
|
||||
<input type="hidden" name="successStories" id="successStoriesJson">
|
||||
<input type="hidden" name="cta" id="ctaJson">
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" id="aboutTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#tab-hero-pane" role="tab"><i
|
||||
class="fas fa-image me-2"></i>Hero</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-leadership-pane" role="tab"><i
|
||||
class="fas fa-users me-2"></i>Leadership</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-learningModel-pane" role="tab"><i
|
||||
class="fas fa-chalkboard-teacher me-2"></i>Learning Model</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-accreditation-pane" role="tab"><i
|
||||
class="fas fa-award me-2"></i>Accreditation</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-successStories-pane" role="tab"><i
|
||||
class="fas fa-quote-left me-2"></i>Success Stories</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-cta-pane" role="tab"><i
|
||||
class="fas fa-bullhorn me-2"></i>CTA</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
|
||||
<!-- ===== HERO TAB ===== -->
|
||||
<div class="tab-pane fade show active" id="tab-hero-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-image me-2"></i>Hero Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Badge</label>
|
||||
<input type="text" class="form-control" id="heroBadge"
|
||||
value="<%= data.hero?.badge || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Student Count</label>
|
||||
<input type="text" class="form-control" id="heroStudentCount"
|
||||
value="<%= data.hero?.studentCount || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Image Alt Text</label>
|
||||
<input type="text" class="form-control" id="heroImageAlt"
|
||||
value="<%= data.hero?.imageAlt || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="heroDescription"
|
||||
rows="3"><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Image URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroImage"
|
||||
value="<%= data.hero?.image || '' %>">
|
||||
<button class="btn btn-outline-primary" type="button"
|
||||
onclick="openImageUploader('heroImage', 'about')"><i
|
||||
class="fas fa-upload me-1"></i>Upload</button>
|
||||
</div>
|
||||
<% if (data.hero?.image) { %>
|
||||
<img src="<%= data.hero.image %>"
|
||||
class="img-thumbnail uploaded-preview mt-2"
|
||||
style="max-height:180px;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Core Values</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addCoreValue()"><i class="fas fa-plus me-1"></i>Add
|
||||
Value</button>
|
||||
</div>
|
||||
<div id="coreValuesContainer">
|
||||
<% (data.hero?.coreValues || []).forEach(val=> { %>
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control core-value-input"
|
||||
value="<%= val %>">
|
||||
<button class="btn btn-outline-danger" type="button"
|
||||
onclick="this.parentElement.remove()"><i
|
||||
class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LEADERSHIP TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-leadership-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="fas fa-users me-2"></i>Leadership</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addLeadershipMember()"><i class="fas fa-plus me-1"></i>Add
|
||||
Member</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="leadershipHeading"
|
||||
value="<%= data.leadership?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="leadershipDescription"
|
||||
rows="2"><%= data.leadership?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="leadershipMembersContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LEARNING MODEL TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-learningModel-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-chalkboard-teacher me-2"></i>Learning Model
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="lmHeading"
|
||||
value="<%= data.learningModel?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="lmDescription"
|
||||
rows="2"><%= data.learningModel?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Async -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border">
|
||||
<div
|
||||
class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Asynchronous Learning</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addLearningFeature('async')"><i
|
||||
class="fas fa-plus me-1"></i>Add Feature</button>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="asyncTitle"
|
||||
value="<%= data.learningModel?.async?.title || '' %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Description</label>
|
||||
<textarea class="form-control form-control-sm"
|
||||
id="asyncDescription"
|
||||
rows="2"><%= data.learningModel?.async?.description || '' %></textarea>
|
||||
</div>
|
||||
<div id="asyncFeaturesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sync -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border">
|
||||
<div
|
||||
class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Synchronous Learning</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addLearningFeature('sync')"><i
|
||||
class="fas fa-plus me-1"></i>Add Feature</button>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="syncTitle"
|
||||
value="<%= data.learningModel?.sync?.title || '' %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Description</label>
|
||||
<textarea class="form-control form-control-sm"
|
||||
id="syncDescription"
|
||||
rows="2"><%= data.learningModel?.sync?.description || '' %></textarea>
|
||||
</div>
|
||||
<div id="syncFeaturesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== ACCREDITATION TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-accreditation-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-award me-2"></i>Accreditation</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="accHeading"
|
||||
value="<%= data.accreditation?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="accDescription"
|
||||
rows="2"><%= data.accreditation?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Badges</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addAccBadge()"><i class="fas fa-plus me-1"></i>Add
|
||||
Badge</button>
|
||||
</div>
|
||||
<div id="accBadgesContainer"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Stats</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addAccStat()"><i class="fas fa-plus me-1"></i>Add
|
||||
Stat</button>
|
||||
</div>
|
||||
<div id="accStatsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== SUCCESS STORIES TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-successStories-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="fas fa-quote-left me-2"></i>Success Stories</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addStory()"><i class="fas fa-plus me-1"></i>Add Story</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="ssHeading"
|
||||
value="<%= data.successStories?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="ssDescription"
|
||||
rows="2"><%= data.successStories?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div id="storiesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== CTA TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-cta-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-bullhorn me-2"></i>Call to Action</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="ctaHeading"
|
||||
value="<%= data.cta?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="ctaDescription"
|
||||
rows="2"><%= data.cta?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Primary Button Label</label>
|
||||
<input type="text" class="form-control" id="ctaPrimaryLabel"
|
||||
value="<%= data.cta?.primaryButton?.label || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Primary Button Href</label>
|
||||
<input type="text" class="form-control" id="ctaPrimaryHref"
|
||||
value="<%= data.cta?.primaryButton?.href || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Secondary Button Label</label>
|
||||
<input type="text" class="form-control" id="ctaSecondaryLabel"
|
||||
value="<%= data.cta?.secondaryButton?.label || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Secondary Button Href</label>
|
||||
<input type="text" class="form-control" id="ctaSecondaryHref"
|
||||
value="<%= data.cta?.secondaryButton?.href || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.tab-content -->
|
||||
</div><!-- /.card-body -->
|
||||
|
||||
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary px-4" onclick="resetForm()"><i
|
||||
class="fas fa-undo me-2"></i>Reset</button>
|
||||
<button type="submit" class="btn btn-outline-primary px-4" id="submitBtn"><i
|
||||
class="fas fa-save me-2"></i>Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
populateAll(originalFormData);
|
||||
|
||||
document.getElementById('aboutForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
serializeAll();
|
||||
this.submit();
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-upload-image');
|
||||
if (btn) openImageUploader(btn.dataset.targetInput, btn.dataset.imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Populate all sections ────────────────────────────────────────────────
|
||||
function populateAll(data) {
|
||||
if (!data) return;
|
||||
|
||||
// Hero
|
||||
const h = data.hero || {};
|
||||
setVal('heroBadge', h.badge);
|
||||
setVal('heroTitle', h.title);
|
||||
setVal('heroDescription', h.description);
|
||||
setVal('heroStudentCount', h.studentCount);
|
||||
setVal('heroImage', h.image);
|
||||
setVal('heroImageAlt', h.imageAlt);
|
||||
updateImagePreview('heroImage', h.image);
|
||||
populateCoreValues(h.coreValues || []);
|
||||
|
||||
// Leadership
|
||||
const l = data.leadership || {};
|
||||
setVal('leadershipHeading', l.heading);
|
||||
setVal('leadershipDescription', l.description);
|
||||
populateLeadershipMembers(l.members || []);
|
||||
|
||||
// Learning Model
|
||||
const lm = data.learningModel || {};
|
||||
setVal('lmHeading', lm.heading);
|
||||
setVal('lmDescription', lm.description);
|
||||
setVal('asyncTitle', lm.async?.title);
|
||||
setVal('asyncDescription', lm.async?.description);
|
||||
setVal('syncTitle', lm.sync?.title);
|
||||
setVal('syncDescription', lm.sync?.description);
|
||||
populateLearningFeatures('async', lm.async?.features || []);
|
||||
populateLearningFeatures('sync', lm.sync?.features || []);
|
||||
|
||||
// Accreditation
|
||||
const acc = data.accreditation || {};
|
||||
setVal('accHeading', acc.heading);
|
||||
setVal('accDescription', acc.description);
|
||||
populateAccBadges(acc.badges || []);
|
||||
populateAccStats(acc.stats || []);
|
||||
|
||||
// Success Stories
|
||||
const ss = data.successStories || {};
|
||||
setVal('ssHeading', ss.heading);
|
||||
setVal('ssDescription', ss.description);
|
||||
populateStories(ss.stories || []);
|
||||
|
||||
// CTA
|
||||
const cta = data.cta || {};
|
||||
setVal('ctaHeading', cta.heading);
|
||||
setVal('ctaDescription', cta.description);
|
||||
setVal('ctaPrimaryLabel', cta.primaryButton?.label);
|
||||
setVal('ctaPrimaryHref', cta.primaryButton?.href);
|
||||
setVal('ctaSecondaryLabel', cta.secondaryButton?.label);
|
||||
setVal('ctaSecondaryHref', cta.secondaryButton?.href);
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = val || '';
|
||||
}
|
||||
|
||||
// ── Serialize all sections into hidden JSON inputs ───────────────────────
|
||||
function serializeAll() {
|
||||
// Hero
|
||||
document.getElementById('heroJson').value = JSON.stringify({
|
||||
badge: v('heroBadge'),
|
||||
title: v('heroTitle'),
|
||||
description: v('heroDescription'),
|
||||
studentCount: v('heroStudentCount'),
|
||||
image: v('heroImage'),
|
||||
imageAlt: v('heroImageAlt'),
|
||||
coreValues: Array.from(document.querySelectorAll('.core-value-input')).map(i => i.value.trim()).filter(Boolean)
|
||||
});
|
||||
|
||||
// Leadership
|
||||
document.getElementById('leadershipJson').value = JSON.stringify({
|
||||
heading: v('leadershipHeading'),
|
||||
description: v('leadershipDescription'),
|
||||
members: Array.from(document.querySelectorAll('.leadership-member')).map(row => ({
|
||||
name: row.querySelector('[data-field="name"]').value.trim(),
|
||||
role: row.querySelector('[data-field="role"]').value.trim(),
|
||||
bio: row.querySelector('[data-field="bio"]').value.trim(),
|
||||
avatar: row.querySelector('[data-field="avatar"]').value.trim(),
|
||||
social: {
|
||||
linkedin: row.querySelector('[data-field="linkedin"]').value.trim(),
|
||||
twitter: row.querySelector('[data-field="twitter"]').value.trim()
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
// Learning Model
|
||||
document.getElementById('learningModelJson').value = JSON.stringify({
|
||||
heading: v('lmHeading'),
|
||||
description: v('lmDescription'),
|
||||
async: {
|
||||
title: v('asyncTitle'),
|
||||
description: v('asyncDescription'),
|
||||
features: collectLearningFeatures('async')
|
||||
},
|
||||
sync: {
|
||||
title: v('syncTitle'),
|
||||
description: v('syncDescription'),
|
||||
features: collectLearningFeatures('sync')
|
||||
}
|
||||
});
|
||||
|
||||
// Accreditation
|
||||
document.getElementById('accreditationJson').value = JSON.stringify({
|
||||
heading: v('accHeading'),
|
||||
description: v('accDescription'),
|
||||
badges: Array.from(document.querySelectorAll('.acc-badge-item')).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
desc: row.querySelector('[data-field="desc"]').value.trim()
|
||||
})),
|
||||
stats: Array.from(document.querySelectorAll('.acc-stat-item')).map(row => ({
|
||||
value: row.querySelector('[data-field="value"]').value.trim(),
|
||||
label: row.querySelector('[data-field="label"]').value.trim(),
|
||||
isPrimary: row.querySelector('[data-field="isPrimary"]').checked
|
||||
}))
|
||||
});
|
||||
|
||||
// Success Stories
|
||||
document.getElementById('successStoriesJson').value = JSON.stringify({
|
||||
heading: v('ssHeading'),
|
||||
description: v('ssDescription'),
|
||||
stories: Array.from(document.querySelectorAll('.story-item')).map(row => ({
|
||||
quote: row.querySelector('[data-field="quote"]').value.trim(),
|
||||
name: row.querySelector('[data-field="name"]').value.trim(),
|
||||
program: row.querySelector('[data-field="program"]').value.trim(),
|
||||
avatar: row.querySelector('[data-field="avatar"]').value.trim()
|
||||
}))
|
||||
});
|
||||
|
||||
// CTA
|
||||
document.getElementById('ctaJson').value = JSON.stringify({
|
||||
heading: v('ctaHeading'),
|
||||
description: v('ctaDescription'),
|
||||
primaryButton: { label: v('ctaPrimaryLabel'), href: v('ctaPrimaryHref') },
|
||||
secondaryButton: { label: v('ctaSecondaryLabel'), href: v('ctaSecondaryHref') }
|
||||
});
|
||||
}
|
||||
|
||||
function v(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
// ── Core Values ──────────────────────────────────────────────────────────
|
||||
function addCoreValue(val = '') {
|
||||
const c = document.getElementById('coreValuesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control core-value-input" value="${esc(val)}" placeholder="e.g. Radical Affordability">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateCoreValues(values) {
|
||||
document.getElementById('coreValuesContainer').innerHTML = '';
|
||||
values.forEach(v => addCoreValue(v));
|
||||
}
|
||||
|
||||
// ── Leadership Members ───────────────────────────────────────────────────
|
||||
function addLeadershipMember(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('leadershipMembersContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-3 leadership-member">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="name" value="${esc(item.name)}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Role</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="role" value="${esc(item.role)}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Bio</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="bio" value="${esc(item.bio)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Avatar URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="avatar" id="memberAvatar_${idx}" value="${esc(item.avatar)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="memberAvatar_${idx}" data-image-type="about"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">LinkedIn URL</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="linkedin" value="${esc(item.social?.linkedin)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Twitter URL</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="twitter" value="${esc(item.social?.twitter)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.leadership-member').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateLeadershipMembers(members) {
|
||||
document.getElementById('leadershipMembersContainer').innerHTML = '';
|
||||
members.forEach(m => addLeadershipMember(m));
|
||||
}
|
||||
|
||||
// ── Learning Features ────────────────────────────────────────────────────
|
||||
function addLearningFeature(mode, item = {}) {
|
||||
const c = document.getElementById(mode + 'FeaturesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-2 lf-feature-item" data-mode="${mode}">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-play">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}" placeholder="Title">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" data-field="desc" value="${esc(item.desc)}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.lf-feature-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateLearningFeatures(mode, features) {
|
||||
document.getElementById(mode + 'FeaturesContainer').innerHTML = '';
|
||||
features.forEach(f => addLearningFeature(mode, f));
|
||||
}
|
||||
|
||||
function collectLearningFeatures(mode) {
|
||||
return Array.from(document.querySelectorAll(`.lf-feature-item[data-mode="${mode}"]`)).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
desc: row.querySelector('[data-field="desc"]').value.trim()
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Accreditation Badges ─────────────────────────────────────────────────
|
||||
function addAccBadge(item = {}) {
|
||||
const c = document.getElementById('accBadgesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-2 acc-badge-item">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-award">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}" placeholder="Title">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" data-field="desc" value="${esc(item.desc)}" placeholder="Description">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.acc-badge-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateAccBadges(badges) {
|
||||
document.getElementById('accBadgesContainer').innerHTML = '';
|
||||
badges.forEach(b => addAccBadge(b));
|
||||
}
|
||||
|
||||
// ── Accreditation Stats ──────────────────────────────────────────────────
|
||||
function addAccStat(item = {}) {
|
||||
const c = document.getElementById('accStatsContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-2 acc-stat-item">
|
||||
<div class="card-body p-2">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control form-control-sm" data-field="value" value="${esc(item.value)}" placeholder="e.g. 89%">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control form-control-sm" data-field="label" value="${esc(item.label)}" placeholder="Label">
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-center gap-2">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" data-field="isPrimary" ${item.isPrimary ? 'checked' : ''}>
|
||||
<label class="form-check-label small">Primary</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0" onclick="this.closest('.acc-stat-item').remove()"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateAccStats(stats) {
|
||||
document.getElementById('accStatsContainer').innerHTML = '';
|
||||
stats.forEach(s => addAccStat(s));
|
||||
}
|
||||
|
||||
// ── Success Stories ──────────────────────────────────────────────────────
|
||||
function addStory(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('storiesContainer');
|
||||
c.insertAdjacentHTML('beforeend', `
|
||||
<div class="card mb-3 story-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small">Quote</label>
|
||||
<textarea class="form-control form-control-sm" data-field="quote" rows="2">${esc(item.quote)}</textarea>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="name" value="${esc(item.name)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Program</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="program" value="${esc(item.program)}">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small">Avatar URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="avatar" id="storyAvatar_${idx}" value="${esc(item.avatar)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="storyAvatar_${idx}" data-image-type="about"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.story-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function populateStories(stories) {
|
||||
document.getElementById('storiesContainer').innerHTML = '';
|
||||
stories.forEach(s => addStory(s));
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────
|
||||
function esc(val) {
|
||||
if (!val) return '';
|
||||
return String(val).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Reset all changes to last saved state?')) {
|
||||
populateAll(originalFormData);
|
||||
}
|
||||
}
|
||||
|
||||
function updateImagePreview(inputId, imagePath) {
|
||||
if (!imagePath) return;
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
let preview = input.closest('.card')?.querySelector('.uploaded-preview');
|
||||
if (preview) {
|
||||
preview.src = imagePath;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = imagePath;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.maxHeight = '180px';
|
||||
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Dùng persistent hidden file input để tránh browser block programmatic click
|
||||
let fileInput = document.getElementById('__globalFileInput');
|
||||
if (!fileInput) {
|
||||
fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.id = '__globalFileInput';
|
||||
fileInput.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;width:1px;height:1px;';
|
||||
document.body.appendChild(fileInput);
|
||||
}
|
||||
|
||||
// Reset để có thể chọn lại cùng file
|
||||
fileInput.value = '';
|
||||
|
||||
// Gán handler mới mỗi lần gọi
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadBtn = document.querySelector(`[onclick*="${targetInput}"]`) ||
|
||||
document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadBtn) uploadBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (!response.ok) throw new Error('Upload failed');
|
||||
const result = await response.json();
|
||||
if (!result.success) throw new Error(result.error || 'Upload failed');
|
||||
|
||||
const input = document.getElementById(targetInput);
|
||||
if (input) {
|
||||
input.value = result.path;
|
||||
const previewUrl = result.path.startsWith('http') ? result.path : window.location.origin + result.path;
|
||||
let preview = input.closest('.col-md-12, .col-md-4, .col-md-5')?.querySelector('.uploaded-preview');
|
||||
if (preview) {
|
||||
preview.src = previewUrl;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.maxHeight = '150px';
|
||||
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
alert('Upload failed: ' + err.message);
|
||||
} finally {
|
||||
if (uploadBtn) uploadBtn.disabled = false;
|
||||
fileInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user