feat(cms): enhance content editors and implement automatic ID generation

Improve the CMS administration interface across multiple pages (Accreditation, Admissions, History, Partnerships, and Policies) with a focus on usability and data integrity.
Key changes include:
- Implement `ensureUniqueIds` utility to automatically generate and maintain unique slugs for content items, removing the need for manual ID entry in the UI.
- Refactor the Admissions calculator to support detailed per-option editing via a new dedicated view and routes.
- Replace basic datalists with a custom, searchable icon combobox component for better visual selection.
- Update `_renderSingletonPageView` to handle active tab persistence via query parameters.
- Streamline editor configurations by removing redundant fields and improving help text.
- Enhance the Admissions "Key Dates" editor with a dynamic table interface for managing columns and rows.
- Normalize data payloads in controllers to ensure consistent API responses and internal linking.
This commit is contained in:
Tống Thành Đạt
2026-04-21 12:37:19 +07:00
parent 122df1a46e
commit 5d0fae6d51
30 changed files with 2174 additions and 477 deletions
+5 -40
View File
@@ -3,7 +3,6 @@ const {
textarea,
image,
icon,
url,
select,
combobox,
object,
@@ -19,40 +18,13 @@ const statusOptions = [
module.exports = {
key: "accreditation",
title: "Accreditation Management",
subtitle: "Edit content displayed on the accreditation page",
subtitle: "Manage the content for the accreditation page",
routeBase: "/admin/accreditation",
apiPath: "/api/accreditation",
previewPath: "/about/accreditation",
dataFile: "accreditation",
imageType: "accreditation",
tabs: [
{
key: "trustBanner",
label: "Trust Banner",
icon: "fas fa-shield-check",
schema: object("trustBanner", "Trust banner", [
icon("icon", "Banner icon"),
text("text", "Banner message", {
maxLength: 120,
helpText:
"This is the slim credibility strip shown above the accreditation content.",
}),
objectList(
"links",
"Banner links",
[
text("label", "Link label", { maxLength: 40 }),
url("href", "Link URL", { maxLength: 255 }),
icon("icon", "Link icon"),
],
{
itemLabel: "Link",
sortable: true,
emptyText: "No trust banner links yet.",
},
),
]),
},
{
key: "hero",
label: "Hero",
@@ -72,8 +44,7 @@ module.exports = {
itemLabel: "Tab",
maxLength: 30,
sortable: true,
helpText:
"The frontend shows up to 3 direct tabs. Additional tabs move into a More dropdown.",
helpText: "Add and reorder the category tabs shown on the page.",
}),
objectList(
"items",
@@ -82,7 +53,7 @@ module.exports = {
icon("icon", "Fallback icon"),
image("image", "Card image", {
imageHint: "Recommended 118x58 px minimum visible ratio",
helpText: "Logo or badge used at the top of the card.",
helpText: "Logo or badge shown at the top of the card.",
}),
select("status", "Status", statusOptions),
combobox("category", "Category", {
@@ -91,19 +62,13 @@ module.exports = {
}),
text("title", "Card title", {
maxLength: 17,
helpText: "Frontend title space is capped at 17 characters.",
helpText: "Keep this very short. Around 17 characters fits best in the card title area.",
}),
textarea("description", "Card description", {
maxLength: 300,
rows: 5,
helpText: "Frontend description preview is capped at 300 characters.",
helpText: "Keep this concise. Around 300 characters fits best in the card description area.",
}),
text("scopeLabel", "Scope label", { maxLength: 20 }),
text("scope", "Scope text", { maxLength: 60 }),
text("validUntilLabel", "Validity label", { maxLength: 24 }),
text("validUntil", "Validity text", { maxLength: 40 }),
text("buttonLabel", "Certificate button label", { maxLength: 30 }),
url("certificateHref", "Certificate URL", { maxLength: 255 }),
],
{
itemLabel: "Accreditation",