forked from UKSOURCE/cms.lams
843 lines
49 KiB
Plaintext
843 lines
49 KiB
Plaintext
<div class="container">
|
|
<div class="d-flex justify-content-between align-items-center mt-4 mb-4 flex-wrap gap-2">
|
|
<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 class="d-flex gap-2">
|
|
<% if (frontendUrl) { %>
|
|
<a href="<%= frontendUrl %>/about" class="btn btn-outline-primary" target="_blank" rel="noopener">
|
|
<i class="fas fa-external-link-alt me-2"></i>View About Page
|
|
</a>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<form method="POST" id="aboutForm" action="/admin/about/update" class="content-with-fixed-buttons">
|
|
<!-- 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>
|
|
|
|
<div class="fixed-bottom-buttons">
|
|
<button type="button" class="btn btn-secondary" onclick="resetForm()">
|
|
<i class="fas fa-undo"></i>
|
|
<span>Reset</span>
|
|
</button>
|
|
<button type="submit" class="btn btn-primary" id="submitBtn">
|
|
<i class="fas fa-save"></i>
|
|
<span>Save Changes</span>
|
|
</button>
|
|
</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 align-items-center">
|
|
<div class="col-md-3">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text icon-preview-cell" style="min-width:32px">
|
|
${item.icon ? `<i class="${esc(item.icon)}"></i>` : ''}
|
|
</span>
|
|
<input type="text" class="form-control form-control-sm" data-field="icon"
|
|
value="${esc(item.icon)}" placeholder="Click to pick..." readonly
|
|
style="cursor:pointer;background:#fff"
|
|
onclick="IconPicker.open(this)">
|
|
</div>
|
|
</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-4">
|
|
<input type="text" class="form-control form-control-sm" data-field="desc" value="${esc(item.desc)}" placeholder="Description">
|
|
</div>
|
|
<div class="col-md-1 d-flex justify-content-center">
|
|
<button type="button" class="btn btn-link text-danger btn-sm p-0" onclick="this.closest('.lf-feature-item').remove()"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
</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">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text icon-preview-cell" style="min-width:32px">
|
|
${item.icon ? `<i class="${esc(item.icon)}"></i>` : ''}
|
|
</span>
|
|
<input type="text" class="form-control form-control-sm" data-field="icon"
|
|
value="${esc(item.icon)}" placeholder="Click to pick..." readonly
|
|
style="cursor:pointer;background:#fff"
|
|
onclick="IconPicker.open(this)">
|
|
</div>
|
|
</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-4">
|
|
<input type="text" class="form-control form-control-sm" data-field="desc" value="${esc(item.desc)}" placeholder="Description">
|
|
</div>
|
|
<div class="col-md-1 d-flex justify-content-center">
|
|
<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></button>
|
|
</div>
|
|
</div>
|
|
</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> |