From 5615c494cf5e851af580b03e9be5e7c253f769a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=E1=BB=91ng=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= <84076965+tongthanhdat009@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:10:27 +0700 Subject: [PATCH] style(cms): improve editor ui and implement collapsible item cards Enhance the CMS administration interface by introducing a more refined design system for content editors. This update replaces generic Bootstrap utility classes with dedicated CMS styling to improve visual hierarchy and usability. Key changes: - Introduce `cms-editor-group` and `cms-item-card` classes for consistent grouping and item styling. - Implement collapsible functionality for item cards to manage long lists of content more effectively. - Standardize editor buttons, including new styles for drag handles, remove buttons, and add buttons. - Update the fixed bottom action bar with improved backdrop filters, shadows, and spacing. - Refactor editor scripts across Accreditation, Admissions, History, Partnerships, and Policies to utilize the new UI components and collapse logic. - Add comprehensive CSS to `main.ejs` to support the new CMS design tokens and layout behaviors. --- .../accreditation/partials/editor-script.ejs | 47 +++-- .../admissions/partials/editor-script.ejs | 47 +++-- .../admin/history/partials/editor-script.ejs | 47 +++-- .../partnerships/partials/directory-tab.ejs | 20 +- .../partnerships/partials/editor-script.ejs | 9 + .../partials/inquiry-form-tab.ejs | 10 +- .../admin/partnerships/partials/templates.ejs | 65 ++++--- .../admin/policies/partials/editor-script.ejs | 47 +++-- .../partials/sections-editor-script.ejs | 49 +++-- views/layouts/main.ejs | 183 +++++++++++++++++- 10 files changed, 387 insertions(+), 137 deletions(-) diff --git a/views/admin/accreditation/partials/editor-script.ejs b/views/admin/accreditation/partials/editor-script.ejs index f0c79db..ba01854 100644 --- a/views/admin/accreditation/partials/editor-script.ejs +++ b/views/admin/accreditation/partials/editor-script.ejs @@ -111,26 +111,17 @@ const col = createCol(schema.colClass || "col-12"); const card = document.createElement("div"); - card.className = "border rounded-3 bg-light-subtle p-3"; + card.className = "cms-editor-group"; const header = document.createElement("div"); - header.className = "d-flex justify-content-between align-items-center mb-3 gap-3"; + header.className = "mb-3"; header.innerHTML = `
${schema.helpText ? `
${escapeHtml(schema.helpText)}
` : ""}
- `; - header.querySelector("button").addEventListener("click", function () { - parent[key].push(createDefaultValue(schema.itemSchema)); - applyAutoSequenceToArray(schema, parent[key]); - renderSection(tabKey); - }); - card.appendChild(header); if (parent[key].length === 0) { @@ -145,11 +136,11 @@ parent[key].forEach((item, index) => { const itemCard = document.createElement("div"); - itemCard.className = "card shadow-sm border-0 mb-3"; + itemCard.className = "card cms-item-card mb-3"; itemCard.dataset.index = String(index); const itemHeader = document.createElement("div"); - itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap"; + itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap"; const title = getArrayItemTitle(schema, item, index); const subtitle = getArrayItemSubtitle(schema, item); @@ -157,7 +148,7 @@
${ schema.sortable - ? '' + ? '' : "" }
@@ -166,13 +157,22 @@
+ ${renderItemActions(schema.itemActions, item)} -
`; + itemHeader + .querySelector('[data-toggle-item="true"]') + .addEventListener("click", function () { + itemCard.classList.toggle("is-collapsed"); + }); + itemHeader .querySelector('[data-remove-item="true"]') .addEventListener("click", function () { @@ -224,6 +224,17 @@ list.appendChild(itemCard); }); + const addButton = document.createElement("button"); + addButton.type = "button"; + addButton.className = "cms-add-button mt-2"; + addButton.innerHTML = `${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`; + addButton.addEventListener("click", function () { + parent[key].push(createDefaultValue(schema.itemSchema)); + applyAutoSequenceToArray(schema, parent[key]); + renderSection(tabKey); + }); + card.appendChild(addButton); + if (schema.sortable && window.Sortable) { window.Sortable.create(list, { animation: 150, @@ -598,7 +609,7 @@ function appendHelp(col, schema, value, extraHint) { const wrapper = document.createElement("div"); - wrapper.className = "d-flex justify-content-between gap-3"; + wrapper.className = "field-meta-row"; const help = document.createElement("div"); help.className = "form-text"; @@ -608,7 +619,7 @@ let counter = null; if (schema.maxLength) { counter = document.createElement("div"); - counter.className = "form-text text-end ms-auto"; + counter.className = "field-char-count"; updateCounter(counter, String(value || "").length, schema.maxLength); wrapper.appendChild(counter); } diff --git a/views/admin/admissions/partials/editor-script.ejs b/views/admin/admissions/partials/editor-script.ejs index f0c79db..ba01854 100644 --- a/views/admin/admissions/partials/editor-script.ejs +++ b/views/admin/admissions/partials/editor-script.ejs @@ -111,26 +111,17 @@ const col = createCol(schema.colClass || "col-12"); const card = document.createElement("div"); - card.className = "border rounded-3 bg-light-subtle p-3"; + card.className = "cms-editor-group"; const header = document.createElement("div"); - header.className = "d-flex justify-content-between align-items-center mb-3 gap-3"; + header.className = "mb-3"; header.innerHTML = `
${schema.helpText ? `
${escapeHtml(schema.helpText)}
` : ""}
- `; - header.querySelector("button").addEventListener("click", function () { - parent[key].push(createDefaultValue(schema.itemSchema)); - applyAutoSequenceToArray(schema, parent[key]); - renderSection(tabKey); - }); - card.appendChild(header); if (parent[key].length === 0) { @@ -145,11 +136,11 @@ parent[key].forEach((item, index) => { const itemCard = document.createElement("div"); - itemCard.className = "card shadow-sm border-0 mb-3"; + itemCard.className = "card cms-item-card mb-3"; itemCard.dataset.index = String(index); const itemHeader = document.createElement("div"); - itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap"; + itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap"; const title = getArrayItemTitle(schema, item, index); const subtitle = getArrayItemSubtitle(schema, item); @@ -157,7 +148,7 @@
${ schema.sortable - ? '' + ? '' : "" }
@@ -166,13 +157,22 @@
+ ${renderItemActions(schema.itemActions, item)} -
`; + itemHeader + .querySelector('[data-toggle-item="true"]') + .addEventListener("click", function () { + itemCard.classList.toggle("is-collapsed"); + }); + itemHeader .querySelector('[data-remove-item="true"]') .addEventListener("click", function () { @@ -224,6 +224,17 @@ list.appendChild(itemCard); }); + const addButton = document.createElement("button"); + addButton.type = "button"; + addButton.className = "cms-add-button mt-2"; + addButton.innerHTML = `${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`; + addButton.addEventListener("click", function () { + parent[key].push(createDefaultValue(schema.itemSchema)); + applyAutoSequenceToArray(schema, parent[key]); + renderSection(tabKey); + }); + card.appendChild(addButton); + if (schema.sortable && window.Sortable) { window.Sortable.create(list, { animation: 150, @@ -598,7 +609,7 @@ function appendHelp(col, schema, value, extraHint) { const wrapper = document.createElement("div"); - wrapper.className = "d-flex justify-content-between gap-3"; + wrapper.className = "field-meta-row"; const help = document.createElement("div"); help.className = "form-text"; @@ -608,7 +619,7 @@ let counter = null; if (schema.maxLength) { counter = document.createElement("div"); - counter.className = "form-text text-end ms-auto"; + counter.className = "field-char-count"; updateCounter(counter, String(value || "").length, schema.maxLength); wrapper.appendChild(counter); } diff --git a/views/admin/history/partials/editor-script.ejs b/views/admin/history/partials/editor-script.ejs index f0c79db..ba01854 100644 --- a/views/admin/history/partials/editor-script.ejs +++ b/views/admin/history/partials/editor-script.ejs @@ -111,26 +111,17 @@ const col = createCol(schema.colClass || "col-12"); const card = document.createElement("div"); - card.className = "border rounded-3 bg-light-subtle p-3"; + card.className = "cms-editor-group"; const header = document.createElement("div"); - header.className = "d-flex justify-content-between align-items-center mb-3 gap-3"; + header.className = "mb-3"; header.innerHTML = `
${schema.helpText ? `
${escapeHtml(schema.helpText)}
` : ""}
- `; - header.querySelector("button").addEventListener("click", function () { - parent[key].push(createDefaultValue(schema.itemSchema)); - applyAutoSequenceToArray(schema, parent[key]); - renderSection(tabKey); - }); - card.appendChild(header); if (parent[key].length === 0) { @@ -145,11 +136,11 @@ parent[key].forEach((item, index) => { const itemCard = document.createElement("div"); - itemCard.className = "card shadow-sm border-0 mb-3"; + itemCard.className = "card cms-item-card mb-3"; itemCard.dataset.index = String(index); const itemHeader = document.createElement("div"); - itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap"; + itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap"; const title = getArrayItemTitle(schema, item, index); const subtitle = getArrayItemSubtitle(schema, item); @@ -157,7 +148,7 @@
${ schema.sortable - ? '' + ? '' : "" }
@@ -166,13 +157,22 @@
+ ${renderItemActions(schema.itemActions, item)} -
`; + itemHeader + .querySelector('[data-toggle-item="true"]') + .addEventListener("click", function () { + itemCard.classList.toggle("is-collapsed"); + }); + itemHeader .querySelector('[data-remove-item="true"]') .addEventListener("click", function () { @@ -224,6 +224,17 @@ list.appendChild(itemCard); }); + const addButton = document.createElement("button"); + addButton.type = "button"; + addButton.className = "cms-add-button mt-2"; + addButton.innerHTML = `${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`; + addButton.addEventListener("click", function () { + parent[key].push(createDefaultValue(schema.itemSchema)); + applyAutoSequenceToArray(schema, parent[key]); + renderSection(tabKey); + }); + card.appendChild(addButton); + if (schema.sortable && window.Sortable) { window.Sortable.create(list, { animation: 150, @@ -598,7 +609,7 @@ function appendHelp(col, schema, value, extraHint) { const wrapper = document.createElement("div"); - wrapper.className = "d-flex justify-content-between gap-3"; + wrapper.className = "field-meta-row"; const help = document.createElement("div"); help.className = "form-text"; @@ -608,7 +619,7 @@ let counter = null; if (schema.maxLength) { counter = document.createElement("div"); - counter.className = "form-text text-end ms-auto"; + counter.className = "field-char-count"; updateCounter(counter, String(value || "").length, schema.maxLength); wrapper.appendChild(counter); } diff --git a/views/admin/partnerships/partials/directory-tab.ejs b/views/admin/partnerships/partials/directory-tab.ejs index 6e9abf0..ff00505 100644 --- a/views/admin/partnerships/partials/directory-tab.ejs +++ b/views/admin/partnerships/partials/directory-tab.ejs @@ -19,30 +19,30 @@ -
-
+
+
The frontend shows up to 3 direct tabs. Additional tabs move into a More dropdown.
-
+
-
-
+
+
Use a short unique partner key so card state stays stable.
-
+
diff --git a/views/admin/partnerships/partials/editor-script.ejs b/views/admin/partnerships/partials/editor-script.ejs index 7dcb48b..02dc400 100644 --- a/views/admin/partnerships/partials/editor-script.ejs +++ b/views/admin/partnerships/partials/editor-script.ejs @@ -140,6 +140,9 @@ renderDirectoryTabs(); renderPartners(); }); + node.querySelector("[data-toggle-item]")?.addEventListener("click", function () { + node.classList.toggle("is-collapsed"); + }); directoryTabsList.appendChild(node); }); @@ -204,6 +207,9 @@ state.directory.partners.splice(index, 1); renderPartners(); }); + node.querySelector("[data-toggle-item]")?.addEventListener("click", function () { + node.classList.toggle("is-collapsed"); + }); partnersList.appendChild(node); }); @@ -277,6 +283,9 @@ field.options.push(""); renderInquiryFields(); }); + node.querySelector("[data-toggle-item]")?.addEventListener("click", function () { + node.classList.toggle("is-collapsed"); + }); function toggleOptions() { optionsWrap.classList.toggle("d-none", typeSelect.value !== "select"); diff --git a/views/admin/partnerships/partials/inquiry-form-tab.ejs b/views/admin/partnerships/partials/inquiry-form-tab.ejs index d4290d8..36b468a 100644 --- a/views/admin/partnerships/partials/inquiry-form-tab.ejs +++ b/views/admin/partnerships/partials/inquiry-form-tab.ejs @@ -15,17 +15,17 @@
-
-
+
+
Manage labels, placeholders, type, width, and dropdown options.
-
+
diff --git a/views/admin/partnerships/partials/templates.ejs b/views/admin/partnerships/partials/templates.ejs index 89eadba..6d51aa8 100644 --- a/views/admin/partnerships/partials/templates.ejs +++ b/views/admin/partnerships/partials/templates.ejs @@ -1,15 +1,20 @@