Adding backend of Student Support, Contact, Request

This commit is contained in:
2026-04-20 16:22:37 +07:00
parent fab72e86a6
commit ac5fa14c97
23 changed files with 3167 additions and 2324 deletions
+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>