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-21 12:33:11 +07:00
31 changed files with 4477 additions and 2325 deletions
File diff suppressed because it is too large Load Diff
+141 -2
View File
@@ -103,7 +103,43 @@
</div>
</div>
<!-- Student Support -->
<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-life-ring fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Student Support</h5>
<p class="text-muted mb-0 small">LAMS student support page</p>
</div>
</div>
<a href="/admin/student-support" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Request Info -->
<div class="col-md-4 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-file-alt fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Request Info</h5>
<p class="text-muted mb-0 small">LAMS request information page</p>
</div>
</div>
<a href="/admin/request-info" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Appointment -->
<div class="col-md-4 border-end border-top">
@@ -198,6 +234,25 @@
</a>
</div>
</div>
<!-- Programme -->
<div class="col-md-4 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-graduation-cap fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Programmes</h5>
<p class="text-muted mb-0 small">Manage academic programmes</p>
</div>
</div>
<a href="/admin/programme" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Manage
</a>
</div>
</div>
</div>
</div>
</div>
@@ -206,7 +261,7 @@
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">API Endpoints</h5>
<span class="badge bg-primary">6 APIs</span>
<span class="badge bg-primary">9 APIs</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
@@ -329,6 +384,48 @@
</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-life-ring" style="color: var(--primary-color);"></i>
</div>
<span>Student Support API</span>
</div>
</td>
<td><code>/api/student-support</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>LAMS Student Support page JSON</td>
<td>
<a href="/api/student-support" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</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-file-alt" style="color: var(--primary-color);"></i>
</div>
<span>Request Info API</span>
</div>
</td>
<td><code>/api/request-info</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>LAMS Request Information page JSON</td>
<td>
<a href="/api/request-info" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
@@ -343,13 +440,55 @@
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get blog posts</td>
<td>List published blog posts (supports ?page, ?category, ?search)</td>
<td>
<a href="/api/blog" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<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>Programmes API</span>
</div>
</td>
<td><code>/api/programmes</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>List all academic programmes</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>
<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>
+610
View File
@@ -0,0 +1,610 @@
<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">Fields marked with <span class="text-danger">*</span> are required.</p>
</div>
<div class="d-flex gap-2">
<% if (programme._id && frontendUrl) { %>
<a href="<%= frontendUrl %>/programmes/<%= programme.id %>" class="btn btn-outline-primary" target="_blank" rel="noopener">
<i class="fas fa-external-link-alt me-2"></i>Preview
</a>
<% } %>
<a href="/admin/programme" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to list
</a>
</div>
</div>
<form
action="/admin/programme/update/<%= programme._id ? programme._id : 'new' %>?imageType=programmes"
method="POST"
enctype="multipart/form-data"
id="programmeForm"
class="content-with-fixed-buttons"
>
<%# Hidden JSON inputs for dynamic arrays %>
<input type="hidden" name="coreCourses" id="coreCoursesJson">
<input type="hidden" name="outcomes" id="outcomesJson">
<input type="hidden" name="faqs" id="faqsJson">
<input type="hidden" name="electives" id="electivesJson">
<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="#tab-basic" role="tab">
<i class="fas fa-info-circle me-2"></i>Basic Info
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-detail" role="tab">
<i class="fas fa-file-alt me-2"></i>Detail Page
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-curriculum" role="tab">
<i class="fas fa-book me-2"></i>Curriculum
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-outcomes" role="tab">
<i class="fas fa-star me-2"></i>Outcomes
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-faq" role="tab">
<i class="fas fa-question-circle me-2"></i>FAQs
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-pricing" role="tab">
<i class="fas fa-dollar-sign me-2"></i>Pricing
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<%# ─── TAB 1: Basic Info ─────────────────────────────────────────── %>
<div class="tab-pane fade show active" id="tab-basic" role="tabpanel">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldId">ID / Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="fieldId" name="id" value="<%= programme.id || '' %>" required placeholder="e.g. bs-cs, mba-db">
<div class="form-text">Dùng làm URL slug: /programmes/<strong>bs-cs</strong></div>
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldLevel">Level</label>
<input type="text" class="form-control" id="fieldLevel" name="level" value="<%= programme.level || '' %>" placeholder="Bachelor's Degree, Master's…">
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldShortName">Short Name</label>
<input type="text" class="form-control" id="fieldShortName" name="shortName" value="<%= programme.shortName || '' %>" placeholder="MBA, B.S., Cert…">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="fieldTitle">Title <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="fieldTitle" name="title" value="<%= programme.title || '' %>" required>
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="fieldDescription">Description (Listing card)</label>
<textarea class="form-control" id="fieldDescription" name="description" rows="3"><%= programme.description || '' %></textarea>
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldDuration">Duration</label>
<input type="text" class="form-control" id="fieldDuration" name="duration" value="<%= programme.duration || '' %>" placeholder="4 Years (Flexible)">
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldFormat">Format</label>
<input type="text" class="form-control" id="fieldFormat" name="format" value="<%= programme.format || '100% Online' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldLink">Public Link</label>
<input type="text" class="form-control" id="fieldLink" name="link" value="<%= programme.link || '' %>" placeholder="/programmes/bs-cs">
<div class="form-text">Tự sinh từ ID nếu để trống. Hiện tại: <code id="linkPreview">/programmes/<%= programme.id || '…' %></code></div>
</div>
<div class="col-md-12">
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" role="switch" id="fieldSelected" name="selected" value="true" <%= programme.selected ? 'checked' : '' %>>
<label class="form-check-label" for="fieldSelected">
<i class="fas fa-star text-warning me-1"></i>Featured / Selected (hiển thị nổi bật trên listing)
</label>
</div>
</div>
</div>
</div>
<%# ─── TAB 2: Detail Page ────────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-detail" role="tabpanel">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium" for="fieldDetailBadge">Detail Badge</label>
<input type="text" class="form-control" id="fieldDetailBadge" name="detailBadge" value="<%= programme.detailBadge || '' %>" placeholder="Bachelor of Science">
</div>
<div class="col-md-6">
<label class="form-label fw-medium" for="fieldDetailTitle">Detail Title</label>
<input type="text" class="form-control" id="fieldDetailTitle" name="detailTitle" value="<%= programme.detailTitle || '' %>" placeholder="Computer Science">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="fieldDetailDescription">Detail Description</label>
<textarea class="form-control" id="fieldDetailDescription" name="detailDescription" rows="3"><%= programme.detailDescription || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="fieldOverview">Overview (main body text)</label>
<textarea class="form-control" id="fieldOverview" name="overview" rows="5"><%= programme.overview || '' %></textarea>
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldCredits">Total Credits</label>
<input type="number" class="form-control" id="fieldCredits" name="credits" value="<%= programme.credits || 120 %>" min="0">
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldNextStartDate">Next Start Date</label>
<input type="text" class="form-control" id="fieldNextStartDate" name="nextStartDate" value="<%= programme.nextStartDate || '' %>" placeholder="September 1st">
</div>
<div class="col-md-4">
<% /* placeholder col */ %>
</div>
<%# Hero Image Upload %>
<div class="col-md-8">
<label class="form-label fw-medium" for="heroImageUrl">Hero Image URL</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroImageUrl" name="heroImageUrl" value="<%= programme.heroImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="heroImageUrl" data-image-type="programmes">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Hoặc upload file: </small>
<input type="file" name="heroImage" class="form-control form-control-sm mt-1" accept="image/*" id="heroImageFile">
</div>
<div class="col-md-4 d-flex align-items-end">
<div id="heroImagePreview" class="border rounded overflow-hidden bg-light w-100" style="height:100px;">
<% const hi = programme.heroImage; if (hi) { %>
<% let src = hi; if (!src.startsWith('http')) { src = src.startsWith('/') ? src : '/' + src; } %>
<img src="<%= src %>" alt="" class="w-100 h-100" style="object-fit:cover;" id="heroPreviewImg" onerror="this.style.display='none'">
<% } else { %>
<div class="h-100 d-flex align-items-center justify-content-center text-muted small">
<div class="text-center"><i class="fas fa-image fa-2x mb-1 d-block opacity-25"></i>No image</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
<%# ─── TAB 3: Curriculum ─────────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-curriculum" role="tabpanel">
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-0">Core Courses</h6>
<small class="text-muted">Môn học bắt buộc trong chương trình</small>
</div>
<button type="button" class="btn btn-sm btn-primary" id="addCourseBtn">
<i class="fas fa-plus me-1"></i>Add Course
</button>
</div>
<div id="coursesContainer" class="vstack gap-3">
<% (programme.coreCourses || []).forEach(function(course, idx) { %>
<div class="card course-row border">
<div class="card-body">
<div class="row g-2">
<div class="col-md-2">
<label class="form-label small">Course ID</label>
<input type="text" class="form-control course-id" value="<%= course.id || '' %>" placeholder="CS101">
</div>
<div class="col-md-4">
<label class="form-label small">Title</label>
<input type="text" class="form-control course-title" value="<%= course.title || '' %>">
</div>
<div class="col-md-4">
<label class="form-label small">Icon</label>
<select class="form-select course-icon-select">
<option value="">— No icon —</option>
<% iconOptions.forEach(function(opt) { %>
<option value="<%= opt.value %>" <%= (course.icon === opt.value) ? 'selected' : '' %>><%= opt.label %></option>
<% }); %>
</select>
</div>
<div class="col-md-2 d-flex align-items-end justify-content-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-course">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="col-md-12">
<label class="form-label small">Description</label>
<input type="text" class="form-control course-description" value="<%= course.description || '' %>">
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<hr>
<div>
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-0">Electives</h6>
<small class="text-muted">Môn học tự chọn</small>
</div>
<button type="button" class="btn btn-sm btn-secondary" id="addElectiveBtn">
<i class="fas fa-plus me-1"></i>Add Elective
</button>
</div>
<div id="electivesContainer" class="vstack gap-2">
<% (programme.electives || []).forEach(function(elective) { %>
<div class="input-group elective-row">
<input type="text" class="form-control elective-input" value="<%= elective %>">
<button type="button" class="btn btn-outline-danger remove-elective"><i class="fas fa-times"></i></button>
</div>
<% }); %>
</div>
</div>
</div>
<%# ─── TAB 4: Outcomes ───────────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-outcomes" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-0">Learning Outcomes</h6>
<small class="text-muted">Những gì sinh viên đạt được sau khoá học</small>
</div>
<button type="button" class="btn btn-sm btn-primary" id="addOutcomeBtn">
<i class="fas fa-plus me-1"></i>Add Outcome
</button>
</div>
<div id="outcomesContainer" class="vstack gap-3">
<% (programme.outcomes || []).forEach(function(outcome) { %>
<div class="card outcome-row border">
<div class="card-body">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label small">Title</label>
<input type="text" class="form-control outcome-title" value="<%= outcome.title || '' %>">
</div>
<div class="col-md-5">
<label class="form-label small">Icon</label>
<select class="form-select outcome-icon-select">
<option value="">— No icon —</option>
<% iconOptions.forEach(function(opt) { %>
<option value="<%= opt.value %>" <%= (outcome.icon === opt.value) ? 'selected' : '' %>><%= opt.label %></option>
<% }); %>
</select>
</div>
<div class="col-md-1 d-flex align-items-end justify-content-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-outcome"><i class="fas fa-trash"></i></button>
</div>
<div class="col-md-12">
<label class="form-label small">Description</label>
<textarea class="form-control outcome-description" rows="2"><%= outcome.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<%# ─── TAB 5: FAQs ───────────────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-faq" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-0">Frequently Asked Questions</h6>
<small class="text-muted">Câu hỏi thường gặp trên trang chi tiết chương trình</small>
</div>
<button type="button" class="btn btn-sm btn-primary" id="addFaqBtn">
<i class="fas fa-plus me-1"></i>Add FAQ
</button>
</div>
<div id="faqsContainer" class="vstack gap-3">
<% (programme.faqs || []).forEach(function(faq) { %>
<div class="card faq-row border">
<div class="card-body">
<div class="row g-2">
<div class="col-md-11">
<label class="form-label small">Question</label>
<input type="text" class="form-control faq-question" value="<%= faq.question || '' %>">
</div>
<div class="col-md-1 d-flex align-items-end justify-content-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-faq"><i class="fas fa-trash"></i></button>
</div>
<div class="col-md-12">
<label class="form-label small">Answer</label>
<textarea class="form-control faq-answer" rows="2"><%= faq.answer || '' %></textarea>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<%# ─── TAB 6: Pricing ────────────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-pricing" role="tabpanel">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldCost">Cost (hiển thị listing)</label>
<input type="text" class="form-control" id="fieldCost" name="cost" value="<%= programme.cost || '' %>" placeholder="$299 / month">
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldMonthlyCost">Monthly Cost (sidebar)</label>
<input type="text" class="form-control" id="fieldMonthlyCost" name="monthlyCost" value="<%= programme.monthlyCost || '' %>" placeholder="$299">
</div>
<div class="col-md-4">
<label class="form-label fw-medium" for="fieldPerCourseCost">Per Course Cost (sidebar)</label>
<input type="text" class="form-control" id="fieldPerCourseCost" name="perCourseCost" value="<%= programme.perCourseCost || '' %>" placeholder="$450">
</div>
</div>
</div>
</div><%# end .tab-content %>
</div><%# end .card-body %>
</div><%# end .card %>
<div class="fixed-bottom-buttons">
<a href="/admin/programme" 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>Save Programme
</button>
</div>
</form>
</div>
<script>
window.PROGRAMME_ICON_OPTIONS = <%- JSON.stringify(iconOptions || []) %>;
function escHtml(s) {
return String(s || '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function iconOptionsHtml(selected) {
var opts = window.PROGRAMME_ICON_OPTIONS;
var html = '<option value="">— No icon —</option>';
for (var i = 0; i < opts.length; i++) {
var o = opts[i];
var sel = (o.value === selected) ? ' selected' : '';
html += '<option value="' + escHtml(o.value) + '"' + sel + '>' + escHtml(o.label) + '</option>';
}
return html;
}
function buildCourseRow(c) {
c = c || {};
return (
'<div class="card course-row border">' +
'<div class="card-body">' +
'<div class="row g-2">' +
'<div class="col-md-2"><label class="form-label small">Course ID</label>' +
'<input type="text" class="form-control course-id" value="' + escHtml(c.id) + '" placeholder="CS101"></div>' +
'<div class="col-md-4"><label class="form-label small">Title</label>' +
'<input type="text" class="form-control course-title" value="' + escHtml(c.title) + '"></div>' +
'<div class="col-md-4"><label class="form-label small">Icon</label>' +
'<select class="form-select course-icon-select"><' + 'option value="">— No icon —</' + 'option>' + iconOptionsHtml(c.icon) + '</select></div>' +
'<div class="col-md-2 d-flex align-items-end justify-content-end">' +
'<button type="button" class="btn btn-outline-danger btn-sm remove-course"><i class="fas fa-trash"></i></button></div>' +
'<div class="col-md-12"><label class="form-label small">Description</label>' +
'<input type="text" class="form-control course-description" value="' + escHtml(c.description) + '"></div>' +
'</div>' +
'</div>' +
'</div>'
);
}
function buildOutcomeRow(o) {
o = o || {};
return (
'<div class="card outcome-row border">' +
'<div class="card-body">' +
'<div class="row g-2">' +
'<div class="col-md-6"><label class="form-label small">Title</label>' +
'<input type="text" class="form-control outcome-title" value="' + escHtml(o.title) + '"></div>' +
'<div class="col-md-5"><label class="form-label small">Icon</label>' +
'<select class="form-select outcome-icon-select"><option value="">— No icon —</option>' + iconOptionsHtml(o.icon) + '</select></div>' +
'<div class="col-md-1 d-flex align-items-end justify-content-end">' +
'<button type="button" class="btn btn-outline-danger btn-sm remove-outcome"><i class="fas fa-trash"></i></button></div>' +
'<div class="col-md-12"><label class="form-label small">Description</label>' +
'<textarea class="form-control outcome-description" rows="2">' + escHtml(o.description) + '</textarea></div>' +
'</div>' +
'</div>' +
'</div>'
);
}
function buildFaqRow(f) {
f = f || {};
return (
'<div class="card faq-row border">' +
'<div class="card-body">' +
'<div class="row g-2">' +
'<div class="col-md-11"><label class="form-label small">Question</label>' +
'<input type="text" class="form-control faq-question" value="' + escHtml(f.question) + '"></div>' +
'<div class="col-md-1 d-flex align-items-end justify-content-end">' +
'<button type="button" class="btn btn-outline-danger btn-sm remove-faq"><i class="fas fa-trash"></i></button></div>' +
'<div class="col-md-12"><label class="form-label small">Answer</label>' +
'<textarea class="form-control faq-answer" rows="2">' + escHtml(f.answer) + '</textarea></div>' +
'</div>' +
'</div>' +
'</div>'
);
}
function updateHeroPreview(url) {
var el = document.getElementById('heroImagePreview');
if (!el) return;
if (!url) {
el.innerHTML = '<div class="h-100 d-flex align-items-center justify-content-center text-muted small"><div class="text-center"><i class="fas fa-image fa-2x mb-1 d-block opacity-25"></i>No image</div></div>';
return;
}
var src = url;
if (!src.startsWith('http://') && !src.startsWith('https://')) {
src = src.startsWith('/') ? src : '/' + src;
}
el.innerHTML = '<img src="' + escHtml(src) + '" alt="" class="w-100 h-100" style="object-fit:cover;">';
}
function openImageUploader(targetInputId, imageType) {
var fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
var file = e.target.files[0];
if (!file) return;
try {
var formData = new FormData();
formData.append('image', file);
var uploadBtn = document.querySelector('[data-target-input="' + targetInputId + '"]');
var original = uploadBtn ? uploadBtn.innerHTML : '';
if (uploadBtn) { uploadBtn.disabled = true; uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>...'; }
var res = await fetch('/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general'), {
method: 'POST', body: formData,
});
var result = await res.json();
if (!result.success) throw new Error(result.error || 'Upload failed');
var input = document.getElementById(targetInputId);
if (input) { input.value = result.path || ''; updateHeroPreview(input.value); }
if (uploadBtn) { uploadBtn.disabled = false; uploadBtn.innerHTML = original; }
} catch (err) {
console.error(err);
alert('Upload failed: ' + (err.message || err));
var b = document.querySelector('[data-target-input="' + targetInputId + '"]');
if (b) { b.disabled = false; b.innerHTML = '<i class="fas fa-upload me-1"></i>Upload'; }
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
function syncHiddenJson() {
// Core Courses
var courses = [];
document.querySelectorAll('#coursesContainer .course-row').forEach(function(row) {
courses.push({
id: row.querySelector('.course-id').value.trim(),
title: row.querySelector('.course-title').value.trim(),
description: row.querySelector('.course-description').value.trim(),
icon: row.querySelector('.course-icon-select').value,
});
});
document.getElementById('coreCoursesJson').value = JSON.stringify(courses);
// Outcomes
var outcomes = [];
document.querySelectorAll('#outcomesContainer .outcome-row').forEach(function(row) {
outcomes.push({
title: row.querySelector('.outcome-title').value.trim(),
description: row.querySelector('.outcome-description').value.trim(),
icon: row.querySelector('.outcome-icon-select').value,
});
});
document.getElementById('outcomesJson').value = JSON.stringify(outcomes);
// FAQs
var faqs = [];
document.querySelectorAll('#faqsContainer .faq-row').forEach(function(row) {
faqs.push({
question: row.querySelector('.faq-question').value.trim(),
answer: row.querySelector('.faq-answer').value.trim(),
});
});
document.getElementById('faqsJson').value = JSON.stringify(faqs);
// Electives — gửi dạng [{value: "..."}, ...] giống student-support
var electives = [];
document.querySelectorAll('#electivesContainer .elective-input').forEach(function(inp) {
var v = inp.value.trim();
if (v) electives.push({ value: v });
});
document.getElementById('electivesJson').value = JSON.stringify(electives);
}
document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('programmeForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving…';
try {
syncHiddenJson();
form.submit();
} catch (err) {
console.error(err);
alert('Could not prepare form data. Please try again.');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-2"></i>Save Programme';
}
});
// Upload image buttons
document.querySelectorAll('.btn-upload-image').forEach(function(btn) {
btn.addEventListener('click', function () {
openImageUploader(this.dataset.targetInput, this.dataset.imageType);
});
});
// Hero image preview sync on text input
var heroInput = document.getElementById('heroImageUrl');
if (heroInput) {
heroInput.addEventListener('input', function () { updateHeroPreview(this.value); });
}
// Auto-sync link preview khi admin gõ ID
var fieldId = document.getElementById('fieldId');
var linkPreview = document.getElementById('linkPreview');
var fieldLink = document.getElementById('fieldLink');
if (fieldId && linkPreview) {
fieldId.addEventListener('input', function () {
var slug = this.value.trim().toLowerCase().replace(/\s+/g, '-');
linkPreview.textContent = '/programmes/' + (slug || '…');
// Nếu field link đang rỗng, tự động điền slug
if (fieldLink && !fieldLink.value) {
fieldLink.placeholder = '/programmes/' + (slug || 'your-id');
}
});
}
// Add buttons
document.getElementById('addCourseBtn').addEventListener('click', function () {
document.getElementById('coursesContainer').insertAdjacentHTML('beforeend', buildCourseRow({}));
});
document.getElementById('addOutcomeBtn').addEventListener('click', function () {
document.getElementById('outcomesContainer').insertAdjacentHTML('beforeend', buildOutcomeRow({}));
});
document.getElementById('addFaqBtn').addEventListener('click', function () {
document.getElementById('faqsContainer').insertAdjacentHTML('beforeend', buildFaqRow({}));
});
document.getElementById('addElectiveBtn').addEventListener('click', function () {
document.getElementById('electivesContainer').insertAdjacentHTML('beforeend',
'<div class="input-group elective-row">' +
'<input type="text" class="form-control elective-input" value="">' +
'<button type="button" class="btn btn-outline-danger remove-elective"><i class="fas fa-times"></i></button>' +
'</div>'
);
});
// Remove buttons (event delegation)
document.getElementById('coursesContainer').addEventListener('click', function(e) {
if (e.target.closest('.remove-course')) e.target.closest('.course-row').remove();
});
document.getElementById('outcomesContainer').addEventListener('click', function(e) {
if (e.target.closest('.remove-outcome')) e.target.closest('.outcome-row').remove();
});
document.getElementById('faqsContainer').addEventListener('click', function(e) {
if (e.target.closest('.remove-faq')) e.target.closest('.faq-row').remove();
});
document.getElementById('electivesContainer').addEventListener('click', function(e) {
if (e.target.closest('.remove-elective')) e.target.closest('.elective-row').remove();
});
});
</script>
+90
View File
@@ -0,0 +1,90 @@
<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);">Programmes Management</h1>
<p class="text-muted mb-0">Manage all academic programmes displayed on the public website.</p>
</div>
<div class="d-flex gap-2">
<% if (frontendUrl) { %>
<a href="<%= frontendUrl %>/programmes" class="btn btn-outline-primary" target="_blank" rel="noopener">
<i class="fas fa-external-link-alt me-2"></i>View Programmes Page
</a>
<% } %>
<a href="/admin/programme/edit/new" class="btn btn-primary shadow-sm">
<i class="fas fa-plus me-2"></i>Add New Programme
</a>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4" style="width:110px;">ID / Code</th>
<th>Title</th>
<th>Level</th>
<th>Format</th>
<th>Cost</th>
<th class="text-center" style="width:80px;">Courses</th>
<th class="text-center" style="width:80px;">Featured</th>
<th class="text-end pe-4" style="width:120px;">Actions</th>
</tr>
</thead>
<tbody>
<% if (data && data.length > 0) { %>
<% data.forEach(function(item) { %>
<tr>
<td class="ps-4">
<code class="text-primary"><%= item.id %></code>
</td>
<td>
<span class="fw-medium"><%= item.title %></span>
<% if (item.description) { %>
<div class="text-muted small text-truncate" style="max-width:280px;"><%= item.description %></div>
<% } %>
</td>
<td>
<span class="badge bg-light text-dark border"><%= item.level || '—' %></span>
</td>
<td class="text-muted small"><%= item.format || '—' %></td>
<td class="text-muted small"><%= item.monthlyCost || item.cost || '—' %></td>
<td class="text-center">
<span class="badge bg-secondary rounded-pill"><%= (item.coreCourses || []).length %></span>
</td>
<td class="text-center">
<% if (item.selected) { %>
<i class="fas fa-star text-warning" title="Featured"></i>
<% } else { %>
<i class="far fa-star text-muted" title="Not featured"></i>
<% } %>
</td>
<td class="text-end pe-4">
<a href="/admin/programme/edit/<%= item._id %>" class="btn btn-sm btn-outline-primary me-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form action="/admin/programme/delete/<%= item._id %>" method="POST" class="d-inline"
onsubmit="return confirm('Delete programme \'<%= item.title %>\'? This cannot be undone.');">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="8" class="text-center text-muted py-5">
<i class="fas fa-graduation-cap fa-2x mb-3 d-block opacity-25"></i>
No programmes found.
<a href="/admin/programme/edit/new" class="d-block mt-2">Add your first programme &rarr;</a>
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</div>
+481
View File
@@ -0,0 +1,481 @@
<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 the public Request Information page: hero, value props, and form configuration.</p>
</div>
<div class="d-flex gap-2">
<% if (frontendUrl) { %>
<a href="<%= frontendUrl %>/request" class="btn btn-outline-primary" target="_blank" rel="noopener">
<i class="fas fa-external-link-alt me-2"></i>View Request Info Page
</a>
<% } %>
</div>
</div>
<form method="POST" class="content-with-fixed-buttons" id="requestInfoForm" action="/admin/request-info/update">
<input type="hidden" name="metadata" id="metadataJson">
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="form" id="formJson">
<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="#tab-meta" role="tab">
<i class="fas fa-tags me-2"></i>Metadata
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-hero" role="tab">
<i class="fas fa-star me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-form" role="tab">
<i class="fas fa-wpforms me-2"></i>Form Config
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<%# ── TAB: Metadata ──────────────────────────────────────────── %>
<div class="tab-pane fade show active" id="tab-meta" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium" for="metaTitle">Title (SEO)</label>
<input type="text" class="form-control" id="metaTitle" value="<%= data.metadata?.title || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="metaDescription">Description (SEO)</label>
<textarea class="form-control" id="metaDescription" rows="3"><%= data.metadata?.description || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="metaKeywords">Keywords (SEO)</label>
<input type="text" class="form-control" id="metaKeywords" value="<%= data.metadata?.keywords || '' %>" placeholder="keyword1, keyword2, ...">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="metaOgImage">OG Image path</label>
<input type="text" class="form-control" id="metaOgImage" value="<%= data.metadata?.ogImage || '' %>" placeholder="/images/og-request.jpg">
</div>
</div>
</div>
<%# ── TAB: Hero ──────────────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-hero" role="tabpanel">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium" for="heroBadge">Badge</label>
<input type="text" class="form-control" id="heroBadge" value="<%= data.hero?.badge || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium" for="heroTitleHighlight">Title — Highlight word</label>
<input type="text" class="form-control" id="heroTitleHighlight" value="<%= data.hero?.titleHighlight || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="heroTitleMain">Title — Main text (before highlight)</label>
<input type="text" class="form-control" id="heroTitleMain" value="<%= data.hero?.titleMain || '' %>" placeholder="Take the Next Step in Your ">
<small class="text-muted">The highlight word will appear after this text, rendered in accent colour on the frontend.</small>
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="heroDescription">Description</label>
<textarea class="form-control" id="heroDescription" rows="3"><%= data.hero?.description || '' %></textarea>
</div>
</div>
<%# Value Props %>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 fw-semibold">Value Props</h6>
<button type="button" class="btn btn-sm btn-primary" id="addValuePropBtn">
<i class="fas fa-plus me-1"></i>Add value prop
</button>
</div>
<div id="valuePropsContainer" class="vstack gap-3">
<% const vps = data.hero?.valueProps || []; %>
<% vps.forEach(function(vp, index) { %>
<div class="card vp-row border">
<div class="card-body">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small">Icon</label>
<select class="form-select vp-icon-select">
<% iconOptions.forEach(function(opt) { %>
<option value="<%= opt.value %>" <%= (vp.icon === opt.value) ? 'selected' : '' %>><%= opt.label %></option>
<% }); %>
</select>
</div>
<div class="col-md-7">
<label class="form-label small">Title</label>
<input type="text" class="form-control vp-title" value="<%= vp.title || '' %>">
</div>
<div class="col-md-1 d-flex align-items-end justify-content-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-vp"><i class="fas fa-trash"></i></button>
</div>
<div class="col-md-12">
<label class="form-label small">Description</label>
<textarea class="form-control vp-description" rows="2"><%= vp.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<%# ── TAB: Form Config ───────────────────────────────────────── %>
<div class="tab-pane fade" id="tab-form" role="tabpanel">
<% const fm = data.form || {}; const fld = fm.fields || {}; %>
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label fw-medium" for="formHeading">Form Heading</label>
<input type="text" class="form-control" id="formHeading" value="<%= fm.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="formDescription">Form Description</label>
<textarea class="form-control" id="formDescription" rows="2"><%= fm.description || '' %></textarea>
</div>
</div>
<h6 class="text-uppercase text-muted small mb-3">Fields</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small">First Name — label</label>
<input type="text" class="form-control" id="fld_firstName_label" value="<%= fld.firstName?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">First Name — placeholder</label>
<input type="text" class="form-control" id="fld_firstName_ph" value="<%= fld.firstName?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Last Name — label</label>
<input type="text" class="form-control" id="fld_lastName_label" value="<%= fld.lastName?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Last Name — placeholder</label>
<input type="text" class="form-control" id="fld_lastName_ph" value="<%= fld.lastName?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Email — label</label>
<input type="text" class="form-control" id="fld_email_label" value="<%= fld.email?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Email — placeholder</label>
<input type="text" class="form-control" id="fld_email_ph" value="<%= fld.email?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Phone — label</label>
<input type="text" class="form-control" id="fld_phone_label" value="<%= fld.phone?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Phone — placeholder</label>
<input type="text" class="form-control" id="fld_phone_ph" value="<%= fld.phone?.placeholder || '' %>">
</div>
</div>
<h6 class="text-uppercase text-muted small mb-3">Program of Interest (dropdown options)</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small">Field label</label>
<input type="text" class="form-control" id="fld_program_label" value="<%= fld.program?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Placeholder</label>
<input type="text" class="form-control" id="fld_program_ph" value="<%= fld.program?.placeholder || '' %>">
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small fw-medium">Program options</span>
<button type="button" class="btn btn-sm btn-secondary" id="addProgramBtn">
<i class="fas fa-plus me-1"></i>Add option
</button>
</div>
<div id="programOptionsContainer" class="vstack gap-2 mb-4">
<% (fld.program?.options || []).forEach(function(opt) { %>
<div class="input-group program-opt-row">
<input type="text" class="form-control prog-label" placeholder="Label" value="<%= opt.label || '' %>">
<input type="text" class="form-control prog-value" placeholder="Value (e.g. bs-cs)" value="<%= opt.value || '' %>">
<button type="button" class="btn btn-outline-danger remove-prog-opt"><i class="fas fa-times"></i></button>
</div>
<% }); %>
</div>
<h6 class="text-uppercase text-muted small mb-3">Start Timeline (dropdown options)</h6>
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label small">Field label</label>
<input type="text" class="form-control" id="fld_timeline_label" value="<%= fld.timeline?.label || '' %>">
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small fw-medium">Timeline options</span>
<button type="button" class="btn btn-sm btn-secondary" id="addTimelineBtn">
<i class="fas fa-plus me-1"></i>Add option
</button>
</div>
<div id="timelineOptionsContainer" class="vstack gap-2 mb-4">
<% (fld.timeline?.options || []).forEach(function(opt) { %>
<div class="input-group timeline-opt-row">
<input type="text" class="form-control tl-label" placeholder="Label" value="<%= opt.label || '' %>">
<input type="text" class="form-control tl-value" placeholder="Value (e.g. immediate)" value="<%= opt.value || '' %>">
<button type="button" class="btn btn-outline-danger remove-tl-opt"><i class="fas fa-times"></i></button>
</div>
<% }); %>
</div>
<h6 class="text-uppercase text-muted small mb-3">Form texts</h6>
<div class="row g-3">
<div class="col-md-12">
<label class="form-label small">Consent text</label>
<textarea class="form-control" id="formConsentText" rows="3"><%= fm.consentText || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label small">Submit label</label>
<input type="text" class="form-control" id="formSubmitLabel" value="<%= fm.submitLabel || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Footer text (security note)</label>
<input type="text" class="form-control" id="formFooterText" value="<%= fm.footerText || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Success message</label>
<input type="text" class="form-control" id="formSuccessMsg" value="<%= fm.successMessage || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Error message</label>
<input type="text" class="form-control" id="formErrorMsg" value="<%= fm.errorMessage || '' %>">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="fixed-bottom-buttons">
<button type="button" class="btn btn-secondary" onclick="resetRequestInfoForm()">
<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>
<script>
function resetRequestInfoForm() {
if (confirm('Are you sure you want to reset all changes? Unsaved edits will be lost.')) {
location.reload();
}
}
window.REQUEST_INFO_ICON_OPTIONS = <%- JSON.stringify(iconOptions) %>;
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function iconOptionsHtml(selected) {
var opts = window.REQUEST_INFO_ICON_OPTIONS || [];
var html = '';
for (var i = 0; i < opts.length; i++) {
var o = opts[i];
var sel = (o.value === selected) ? ' selected' : '';
html += '<option value="' + escapeHtml(o.value) + '"' + sel + '>' + escapeHtml(o.label) + '</option>';
}
return html;
}
function buildValuePropRow(vp) {
vp = vp || {};
var iconSel = '<select class="form-select vp-icon-select">' + iconOptionsHtml(vp.icon) + '</select>';
return (
'<div class="card vp-row border"><div class="card-body">' +
'<div class="row g-2 align-items-end">' +
'<div class="col-md-4"><label class="form-label small">Icon</label>' + iconSel + '</div>' +
'<div class="col-md-7"><label class="form-label small">Title</label>' +
'<input type="text" class="form-control vp-title" value="' + escapeHtml(vp.title) + '"></div>' +
'<div class="col-md-1 d-flex align-items-end justify-content-end">' +
'<button type="button" class="btn btn-outline-danger btn-sm remove-vp"><i class="fas fa-trash"></i></button>' +
'</div>' +
'<div class="col-md-12"><label class="form-label small">Description</label>' +
'<textarea class="form-control vp-description" rows="2">' + escapeHtml(vp.description) + '</textarea></div>' +
'</div></div></div>'
);
}
function buildProgramOptRow(opt) {
opt = opt || {};
return (
'<div class="input-group program-opt-row">' +
'<input type="text" class="form-control prog-label" placeholder="Label" value="' + escapeHtml(opt.label) + '">' +
'<input type="text" class="form-control prog-value" placeholder="Value (e.g. bs-cs)" value="' + escapeHtml(opt.value) + '">' +
'<button type="button" class="btn btn-outline-danger remove-prog-opt"><i class="fas fa-times"></i></button>' +
'</div>'
);
}
function buildTimelineOptRow(opt) {
opt = opt || {};
return (
'<div class="input-group timeline-opt-row">' +
'<input type="text" class="form-control tl-label" placeholder="Label" value="' + escapeHtml(opt.label) + '">' +
'<input type="text" class="form-control tl-value" placeholder="Value (e.g. immediate)" value="' + escapeHtml(opt.value) + '">' +
'<button type="button" class="btn btn-outline-danger remove-tl-opt"><i class="fas fa-times"></i></button>' +
'</div>'
);
}
function syncHiddenJson() {
// Metadata
document.getElementById('metadataJson').value = JSON.stringify({
title: document.getElementById('metaTitle').value.trim(),
description: document.getElementById('metaDescription').value.trim(),
keywords: document.getElementById('metaKeywords').value.trim(),
ogImage: document.getElementById('metaOgImage').value.trim(),
});
// ValueProps
var valueProps = [];
document.querySelectorAll('#valuePropsContainer .vp-row').forEach(function(row) {
valueProps.push({
icon: row.querySelector('.vp-icon-select').value,
title: row.querySelector('.vp-title').value.trim(),
description: row.querySelector('.vp-description').value.trim(),
});
});
// Hero
document.getElementById('heroJson').value = JSON.stringify({
badge: document.getElementById('heroBadge').value.trim(),
titleMain: document.getElementById('heroTitleMain').value.trim(),
titleHighlight: document.getElementById('heroTitleHighlight').value.trim(),
description: document.getElementById('heroDescription').value.trim(),
valueProps: valueProps,
});
// Program options
var programOptions = [];
document.querySelectorAll('#programOptionsContainer .program-opt-row').forEach(function(row) {
var label = row.querySelector('.prog-label').value.trim();
var value = row.querySelector('.prog-value').value.trim();
if (label || value) programOptions.push({ label: label, value: value });
});
// Timeline options
var timelineOptions = [];
document.querySelectorAll('#timelineOptionsContainer .timeline-opt-row').forEach(function(row) {
var label = row.querySelector('.tl-label').value.trim();
var value = row.querySelector('.tl-value').value.trim();
if (label || value) timelineOptions.push({ label: label, value: value });
});
// Form
document.getElementById('formJson').value = JSON.stringify({
heading: document.getElementById('formHeading').value.trim(),
description: document.getElementById('formDescription').value.trim(),
fields: {
firstName: {
name: 'first_name',
label: document.getElementById('fld_firstName_label').value.trim(),
placeholder: document.getElementById('fld_firstName_ph').value.trim(),
required: true,
},
lastName: {
name: 'last_name',
label: document.getElementById('fld_lastName_label').value.trim(),
placeholder: document.getElementById('fld_lastName_ph').value.trim(),
required: true,
},
email: {
name: 'email_address',
label: document.getElementById('fld_email_label').value.trim(),
placeholder: document.getElementById('fld_email_ph').value.trim(),
required: true,
},
phone: {
name: 'phone_number',
label: document.getElementById('fld_phone_label').value.trim(),
placeholder: document.getElementById('fld_phone_ph').value.trim(),
required: false,
},
program: {
name: 'program_id',
label: document.getElementById('fld_program_label').value.trim(),
placeholder: document.getElementById('fld_program_ph').value.trim(),
required: true,
options: programOptions,
},
timeline: {
name: 'start_timeline',
label: document.getElementById('fld_timeline_label').value.trim(),
options: timelineOptions,
},
},
consentText: document.getElementById('formConsentText').value.trim(),
submitLabel: document.getElementById('formSubmitLabel').value.trim(),
footerText: document.getElementById('formFooterText').value.trim(),
successMessage: document.getElementById('formSuccessMsg').value.trim(),
errorMessage: document.getElementById('formErrorMsg').value.trim(),
});
}
document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('requestInfoForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
syncHiddenJson();
form.submit();
} catch (err) {
console.error(err);
alert('Could not prepare form data. Please try again.');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Value props
document.getElementById('addValuePropBtn').addEventListener('click', function () {
document.getElementById('valuePropsContainer').insertAdjacentHTML('beforeend',
buildValuePropRow({ icon: 'fa-solid fa-circle-info' })
);
});
document.getElementById('valuePropsContainer').addEventListener('click', function (e) {
if (e.target.closest('.remove-vp')) {
e.target.closest('.vp-row').remove();
}
});
// Program options
document.getElementById('addProgramBtn').addEventListener('click', function () {
document.getElementById('programOptionsContainer').insertAdjacentHTML('beforeend', buildProgramOptRow());
});
document.getElementById('programOptionsContainer').addEventListener('click', function (e) {
if (e.target.closest('.remove-prog-opt')) {
e.target.closest('.program-opt-row').remove();
}
});
// Timeline options
document.getElementById('addTimelineBtn').addEventListener('click', function () {
document.getElementById('timelineOptionsContainer').insertAdjacentHTML('beforeend', buildTimelineOptRow());
});
document.getElementById('timelineOptionsContainer').addEventListener('click', function (e) {
if (e.target.closest('.remove-tl-opt')) {
e.target.closest('.timeline-opt-row').remove();
}
});
});
</script>
+590
View File
@@ -0,0 +1,590 @@
<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 the public Student Support page: hero, services directory, contact channels, and form labels.</p>
</div>
<div class="d-flex gap-2">
<% if (frontendUrl) { %>
<a href="<%= frontendUrl %>/student-support" class="btn btn-outline-primary" target="_blank" rel="noopener">
<i class="fas fa-external-link-alt me-2"></i>View Student Support Page
</a>
<% } %>
</div>
</div>
<form method="POST" class="content-with-fixed-buttons" id="studentSupportForm" action="/admin/student-support/update">
<input type="hidden" name="metadata" id="metadataJson">
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="directory" id="directoryJson">
<input type="hidden" name="contactSection" id="contactSectionJson">
<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="#tab-meta" role="tab">
<i class="fas fa-tags me-2"></i>Metadata
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-hero" 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="#tab-directory" role="tab">
<i class="fas fa-list me-2"></i>Directory
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-contact" role="tab">
<i class="fas fa-headset me-2"></i>Contact &amp; form
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-meta" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium" for="metaTitle">Title (SEO)</label>
<input type="text" class="form-control" id="metaTitle" value="<%= data.metadata?.title || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="metaDescription">Description (SEO)</label>
<textarea class="form-control" id="metaDescription" rows="3"><%= data.metadata?.description || '' %></textarea>
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-hero" role="tabpanel">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium" for="heroBadge">Badge</label>
<input type="text" class="form-control" id="heroBadge" value="<%= data.hero?.badge || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="heroTitle">Title</label>
<input type="text" class="form-control" id="heroTitle" value="<%= data.hero?.title || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="heroDescription">Description</label>
<textarea class="form-control" id="heroDescription" rows="4"><%= data.hero?.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label">Primary button</label>
<input type="text" class="form-control mb-2" id="heroPrimaryLabel" placeholder="Label" value="<%= data.hero?.primaryButton?.label || '' %>">
<input type="text" class="form-control" id="heroPrimaryHref" placeholder="Href" value="<%= data.hero?.primaryButton?.href || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Secondary button</label>
<input type="text" class="form-control mb-2" id="heroSecondaryLabel" placeholder="Label" value="<%= data.hero?.secondaryButton?.label || '' %>">
<input type="text" class="form-control" id="heroSecondaryHref" placeholder="Href" value="<%= data.hero?.secondaryButton?.href || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium" for="heroImage">Hero image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroImage" value="<%= data.hero?.image || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="heroImage" data-image-type="student-support">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Full URL or relative path such as <code>/uploads/...</code> after upload.</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium" for="heroImageAlt">Image alt</label>
<input type="text" class="form-control" id="heroImageAlt" value="<%= data.hero?.imageAlt || '' %>">
</div>
<div class="col-12">
<label class="form-label text-muted small">Preview</label>
<div id="heroImagePreview" class="border rounded overflow-hidden bg-light" style="height: 380px; width: 100%;">
<% const hi = data.hero?.image; if (hi) { %>
<% let src = hi; if (!src.startsWith('http')) { src = src.startsWith('/') ? src : '/' + src; } %>
<img src="<%= src %>" alt="" class="w-100 h-100" style="object-fit: cover; object-position: center;" id="heroPreviewImg"
onerror="this.style.display='none'">
<% } else { %>
<div class="h-100 d-flex align-items-center justify-content-center text-muted">
<div><i class="fas fa-image fa-2x mb-2"></i><br>No image</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-directory" role="tabpanel">
<div class="row g-3 mb-3">
<div class="col-md-12">
<label class="form-label fw-medium" for="dirTitle">Section title</label>
<input type="text" class="form-control" id="dirTitle" value="<%= data.directory?.title || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium" for="dirSubtitle">Subtitle</label>
<textarea class="form-control" id="dirSubtitle" rows="2"><%= data.directory?.subtitle || '' %></textarea>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Services</h6>
<button type="button" class="btn btn-sm btn-primary" id="addServiceBtn"><i class="fas fa-plus me-1"></i>Add service</button>
</div>
<div id="servicesContainer" class="vstack gap-3">
<% const services = data.directory?.services || []; %>
<% services.forEach(function(svc, index) { %>
<div class="card service-row border">
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small">Icon</label>
<select class="form-select svc-icon-select">
<% iconOptions.forEach(function(opt) { %>
<option value="<%= opt.value %>" <%= (svc.icon === opt.value) ? 'selected' : '' %>><%= opt.label %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label small">Hours icon</label>
<select class="form-select svc-hours-icon-select">
<% iconOptions.forEach(function(opt) { %>
<option value="<%= opt.value %>" <%= (svc.hoursIcon === opt.value) ? 'selected' : '' %>><%= opt.label %></option>
<% }); %>
</select>
</div>
<div class="col-md-4 d-flex align-items-end justify-content-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-service"><i class="fas fa-trash"></i></button>
</div>
<div class="col-md-12">
<label class="form-label small">Title</label>
<input type="text" class="form-control svc-title" value="<%= svc.title || '' %>">
</div>
<div class="col-md-12">
<label class="form-label small">Description</label>
<textarea class="form-control svc-description" rows="2"><%= svc.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label small">Hours text</label>
<input type="text" class="form-control svc-hours" value="<%= svc.hours || '' %>">
</div>
<div class="col-md-3">
<label class="form-label small">Button label</label>
<input type="text" class="form-control svc-btn-label" value="<%= svc.buttonLabel || '' %>">
</div>
<div class="col-md-3">
<label class="form-label small">Button href</label>
<input type="text" class="form-control svc-btn-href" value="<%= svc.buttonHref || '' %>">
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
<div class="tab-pane fade" id="tab-contact" role="tabpanel">
<% const cs = data.contactSection || {}; const fm = cs.form || {}; const fld = fm.fields || {}; %>
<h6 class="text-uppercase text-muted small">Contact channels</h6>
<div class="row g-3 mb-3">
<div class="col-md-12">
<label class="form-label" for="csHeading">Heading</label>
<input type="text" class="form-control" id="csHeading" value="<%= cs.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label" for="csDescription">Description</label>
<textarea class="form-control" id="csDescription" rows="2"><%= cs.description || '' %></textarea>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-medium">Channels</span>
<button type="button" class="btn btn-sm btn-primary" id="addChannelBtn"><i class="fas fa-plus me-1"></i>Add channel</button>
</div>
<div id="channelsContainer" class="vstack gap-2 mb-4">
<% (cs.channels || []).forEach(function(ch) { %>
<div class="card channel-row border">
<div class="card-body py-2">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small">Icon</label>
<select class="form-select ch-icon-select">
<% iconOptions.forEach(function(opt) { %>
<option value="<%= opt.value %>" <%= (ch.icon === opt.value) ? 'selected' : '' %>><%= opt.label %></option>
<% }); %>
</select>
</div>
<div class="col-md-3">
<label class="form-label small">Title</label>
<input type="text" class="form-control ch-title" value="<%= ch.title || '' %>">
</div>
<div class="col-md-5">
<label class="form-label small">Detail</label>
<input type="text" class="form-control ch-detail" value="<%= ch.detail || '' %>">
</div>
<div class="col-md-1 text-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-channel"><i class="fas fa-trash"></i></button>
</div>
</div>
</div>
</div>
<% }); %>
</div>
<h6 class="text-uppercase text-muted small">Form (display labels)</h6>
<div class="row g-3 mb-3">
<div class="col-md-12">
<label class="form-label" for="formHeading">Form heading</label>
<input type="text" class="form-control" id="formHeading" value="<%= fm.heading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">First name — label</label>
<input type="text" class="form-control" id="fld_firstName_label" value="<%= fld.firstName?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">First name — placeholder</label>
<input type="text" class="form-control" id="fld_firstName_ph" value="<%= fld.firstName?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Last name — label</label>
<input type="text" class="form-control" id="fld_lastName_label" value="<%= fld.lastName?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Last name — placeholder</label>
<input type="text" class="form-control" id="fld_lastName_ph" value="<%= fld.lastName?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Student ID — label</label>
<input type="text" class="form-control" id="fld_studentId_label" value="<%= fld.studentId?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Student ID — placeholder</label>
<input type="text" class="form-control" id="fld_studentId_ph" value="<%= fld.studentId?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Department — label</label>
<input type="text" class="form-control" id="fld_department_label" value="<%= fld.department?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Message — label</label>
<input type="text" class="form-control" id="fld_message_label" value="<%= fld.message?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Message — placeholder</label>
<input type="text" class="form-control" id="fld_message_ph" value="<%= fld.message?.placeholder || '' %>">
</div>
<div class="col-md-6">
<label class="form-label small">Submit label</label>
<input type="text" class="form-control" id="formSubmitLabel" value="<%= fm.submitLabel || '' %>">
</div>
<div class="col-md-12">
<label class="form-label small">Success message</label>
<input type="text" class="form-control" id="formSuccessMsg" value="<%= fm.successMessage || '' %>">
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-medium">Departments (dropdown)</span>
<button type="button" class="btn btn-sm btn-secondary" id="addDeptBtn"><i class="fas fa-plus me-1"></i>Add row</button>
</div>
<div id="departmentsContainer" class="vstack gap-2">
<% (fm.departments || []).forEach(function(dep) { %>
<div class="input-group dept-row">
<input type="text" class="form-control dept-input" value="<%= dep %>">
<button type="button" class="btn btn-outline-danger remove-dept"><i class="fas fa-times"></i></button>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
<div class="fixed-bottom-buttons">
<button type="button" class="btn btn-secondary" onclick="resetStudentSupportForm()">
<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>
<script>
function resetStudentSupportForm() {
if (confirm('Are you sure you want to reset all changes? Unsaved edits will be lost.')) {
location.reload();
}
}
window.STUDENT_SUPPORT_ICON_OPTIONS = <%- JSON.stringify(iconOptions) %>;
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function iconOptionsHtml(selected) {
var opts = window.STUDENT_SUPPORT_ICON_OPTIONS || [];
var html = '';
for (var i = 0; i < opts.length; i++) {
var o = opts[i];
var sel = (o.value === selected) ? ' selected' : '';
html += '<option value="' + escapeHtml(o.value) + '"' + sel + '>' + escapeHtml(o.label) + '</option>';
}
return html;
}
function buildServiceRow(svc) {
svc = svc || {};
var iconSel = '<select class="form-select svc-icon-select">' + iconOptionsHtml(svc.icon) + '</select>';
var hoursIconSel = '<select class="form-select svc-hours-icon-select">' + iconOptionsHtml(svc.hoursIcon) + '</select>';
return (
'<div class="card service-row border">' +
'<div class="card-body">' +
'<div class="row g-2">' +
'<div class="col-md-4"><label class="form-label small">Icon</label>' + iconSel + '</div>' +
'<div class="col-md-4"><label class="form-label small">Hours icon</label>' + hoursIconSel + '</div>' +
'<div class="col-md-4 d-flex align-items-end justify-content-end">' +
'<button type="button" class="btn btn-outline-danger btn-sm remove-service"><i class="fas fa-trash"></i></button>' +
'</div>' +
'<div class="col-md-12"><label class="form-label small">Title</label>' +
'<input type="text" class="form-control svc-title" value="' + escapeHtml(svc.title) + '"></div>' +
'<div class="col-md-12"><label class="form-label small">Description</label>' +
'<textarea class="form-control svc-description" rows="2">' + escapeHtml(svc.description) + '</textarea></div>' +
'<div class="col-md-6"><label class="form-label small">Hours text</label>' +
'<input type="text" class="form-control svc-hours" value="' + escapeHtml(svc.hours) + '"></div>' +
'<div class="col-md-3"><label class="form-label small">Button label</label>' +
'<input type="text" class="form-control svc-btn-label" value="' + escapeHtml(svc.buttonLabel) + '"></div>' +
'<div class="col-md-3"><label class="form-label small">Button href</label>' +
'<input type="text" class="form-control svc-btn-href" value="' + escapeHtml(svc.buttonHref) + '"></div>' +
'</div></div></div>'
);
}
function buildChannelRow(ch) {
ch = ch || {};
var iconSel = '<select class="form-select ch-icon-select">' + iconOptionsHtml(ch.icon) + '</select>';
return (
'<div class="card channel-row border"><div class="card-body py-2">' +
'<div class="row g-2 align-items-end">' +
'<div class="col-md-3"><label class="form-label small">Icon</label>' + iconSel + '</div>' +
'<div class="col-md-3"><label class="form-label small">Title</label>' +
'<input type="text" class="form-control ch-title" value="' + escapeHtml(ch.title) + '"></div>' +
'<div class="col-md-5"><label class="form-label small">Detail</label>' +
'<input type="text" class="form-control ch-detail" value="' + escapeHtml(ch.detail) + '"></div>' +
'<div class="col-md-1 text-end">' +
'<button type="button" class="btn btn-outline-danger btn-sm remove-channel"><i class="fas fa-trash"></i></button>' +
'</div></div></div></div>'
);
}
function updateHeroPreview(url) {
var el = document.getElementById('heroImagePreview');
if (!el) return;
if (!url) {
el.innerHTML = '<div class="h-100 d-flex align-items-center justify-content-center text-muted"><div><i class="fas fa-image fa-2x mb-2"></i><br>No image</div></div>';
return;
}
el.classList.remove('d-flex', 'align-items-center', 'justify-content-center');
el.style.height = '380px';
el.style.width = '100%';
var src = url;
if (!src.startsWith('http://') && !src.startsWith('https://')) {
src = src.startsWith('/') ? src : '/' + src;
}
el.innerHTML = '<img src="' + escapeHtml(src) + '" alt="" class="w-100 h-100" style="object-fit: cover; object-position: center;" id="heroPreviewImg">';
}
function syncHiddenJson() {
document.getElementById('metadataJson').value = JSON.stringify({
title: document.getElementById('metaTitle').value.trim(),
description: document.getElementById('metaDescription').value.trim(),
});
document.getElementById('heroJson').value = JSON.stringify({
badge: document.getElementById('heroBadge').value.trim(),
title: document.getElementById('heroTitle').value.trim(),
description: document.getElementById('heroDescription').value.trim(),
primaryButton: {
label: document.getElementById('heroPrimaryLabel').value.trim(),
href: document.getElementById('heroPrimaryHref').value.trim(),
},
secondaryButton: {
label: document.getElementById('heroSecondaryLabel').value.trim(),
href: document.getElementById('heroSecondaryHref').value.trim(),
},
image: document.getElementById('heroImage').value.trim(),
imageAlt: document.getElementById('heroImageAlt').value.trim(),
});
var services = [];
document.querySelectorAll('#servicesContainer .service-row').forEach(function(row) {
services.push({
icon: row.querySelector('.svc-icon-select').value,
title: row.querySelector('.svc-title').value.trim(),
description: row.querySelector('.svc-description').value.trim(),
hours: row.querySelector('.svc-hours').value.trim(),
hoursIcon: row.querySelector('.svc-hours-icon-select').value,
buttonLabel: row.querySelector('.svc-btn-label').value.trim(),
buttonHref: row.querySelector('.svc-btn-href').value.trim(),
});
});
document.getElementById('directoryJson').value = JSON.stringify({
title: document.getElementById('dirTitle').value.trim(),
subtitle: document.getElementById('dirSubtitle').value.trim(),
services: services,
});
var channels = [];
document.querySelectorAll('#channelsContainer .channel-row').forEach(function(row) {
channels.push({
icon: row.querySelector('.ch-icon-select').value,
title: row.querySelector('.ch-title').value.trim(),
detail: row.querySelector('.ch-detail').value.trim(),
});
});
var departments = [];
document.querySelectorAll('#departmentsContainer .dept-input').forEach(function(inp) {
var v = inp.value.trim();
if (v) departments.push(v);
});
document.getElementById('contactSectionJson').value = JSON.stringify({
heading: document.getElementById('csHeading').value.trim(),
description: document.getElementById('csDescription').value.trim(),
channels: channels,
form: {
heading: document.getElementById('formHeading').value.trim(),
fields: {
firstName: {
label: document.getElementById('fld_firstName_label').value.trim(),
placeholder: document.getElementById('fld_firstName_ph').value.trim(),
},
lastName: {
label: document.getElementById('fld_lastName_label').value.trim(),
placeholder: document.getElementById('fld_lastName_ph').value.trim(),
},
studentId: {
label: document.getElementById('fld_studentId_label').value.trim(),
placeholder: document.getElementById('fld_studentId_ph').value.trim(),
},
department: {
label: document.getElementById('fld_department_label').value.trim(),
},
message: {
label: document.getElementById('fld_message_label').value.trim(),
placeholder: document.getElementById('fld_message_ph').value.trim(),
},
},
departments: departments,
submitLabel: document.getElementById('formSubmitLabel').value.trim(),
successMessage: document.getElementById('formSuccessMsg').value.trim(),
},
});
}
function openImageUploader(targetInputId, imageType) {
var fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
var file = e.target.files[0];
if (!file) return;
try {
var formData = new FormData();
formData.append('image', file);
var uploadBtn = document.querySelector('[data-target-input="' + targetInputId + '"]');
var original = uploadBtn ? uploadBtn.innerHTML : '';
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>...';
}
var res = await fetch('/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general'), {
method: 'POST',
body: formData,
});
var result = await res.json();
if (!result.success) throw new Error(result.error || 'Upload failed');
var input = document.getElementById(targetInputId);
if (input) input.value = result.path || '';
if (targetInputId === 'heroImage') updateHeroPreview(input.value);
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = original;
}
} catch (err) {
console.error(err);
alert('Upload failed: ' + (err.message || err));
var b = document.querySelector('[data-target-input="' + targetInputId + '"]');
if (b) { b.disabled = false; b.innerHTML = '<i class="fas fa-upload me-1"></i>Upload'; }
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('studentSupportForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
syncHiddenJson();
form.submit();
} catch (err) {
console.error(err);
alert('Could not prepare form data. Please try again.');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
document.querySelectorAll('.btn-upload-image').forEach(function (btn) {
btn.addEventListener('click', function () {
openImageUploader(this.dataset.targetInput, this.dataset.imageType);
});
});
var heroImgInput = document.getElementById('heroImage');
if (heroImgInput) {
heroImgInput.addEventListener('input', function () { updateHeroPreview(this.value); });
}
document.getElementById('addServiceBtn').addEventListener('click', function () {
var wrap = document.getElementById('servicesContainer');
wrap.insertAdjacentHTML('beforeend', buildServiceRow({ icon: 'fa-solid fa-circle-info', hoursIcon: 'fa-regular fa-clock' }));
});
document.getElementById('addChannelBtn').addEventListener('click', function () {
document.getElementById('channelsContainer').insertAdjacentHTML('beforeend', buildChannelRow({ icon: 'fa-solid fa-phone' }));
});
document.getElementById('addDeptBtn').addEventListener('click', function () {
document.getElementById('departmentsContainer').insertAdjacentHTML(
'beforeend',
'<div class="input-group dept-row">' +
'<input type="text" class="form-control dept-input" value="">' +
'<button type="button" class="btn btn-outline-danger remove-dept"><i class="fas fa-times"></i></button>' +
'</div>'
);
});
document.getElementById('servicesContainer').addEventListener('click', function (e) {
if (e.target.closest('.remove-service')) {
e.target.closest('.service-row').remove();
}
});
document.getElementById('channelsContainer').addEventListener('click', function (e) {
if (e.target.closest('.remove-channel')) {
e.target.closest('.channel-row').remove();
}
});
document.getElementById('departmentsContainer').addEventListener('click', function (e) {
if (e.target.closest('.remove-dept')) {
e.target.closest('.dept-row').remove();
}
});
});
</script>
+3
View File
@@ -96,6 +96,9 @@
<li class="nav-item">
<a class="nav-link" href="/admin/upload">Upload</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/programme">Programmes</a>
</li>
</ul>
</div>
</nav>
+7
View File
@@ -919,6 +919,10 @@
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
Us</a>
</li>
<li class="nav-item">
<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>
@@ -927,6 +931,9 @@
<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>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/audit-logs' ? 'active' : '' %>"
href="/admin/audit-logs">Audit Log