feat: enhance about us and footer CMS admin panels

- Improve aboutUs controller with better field handling
- Update footer controller with expanded content management
- Refine about admin view templates
- Update appointment and footer admin views
- Add about contract repair migration script
- Update about.json seed data
This commit is contained in:
Tống Thành Đạt
2026-04-10 22:32:51 +07:00
parent 51c6303437
commit c6a2d4a55d
8 changed files with 534 additions and 114 deletions

View File

@@ -0,0 +1,169 @@
const mongoose = require("mongoose");
const connectDB = require("../config/database");
const AboutUs = require("../models/aboutUs");
const jsonHelper = require("../utils/jsonHelper");
const GENERIC_PLACEHOLDER = "7281.jpg";
const ABOUT_DEFAULTS = {
heroBackground: "/uploads/about/breadcrumb.jpg",
featuresBackground: "/assets/img/home-3/choose-us/pricing-bg.jpg",
featuresImage: "/uploads/about/businessman.jpg",
missionIcons: [
"/assets/img/home-3/choose-us/icon-1.png",
"/assets/img/home-3/choose-us/icon-2.png",
"/assets/img/home-3/choose-us/icon-3.png",
],
featureIcons: [
"/assets/img/home-3/choose-us/icon-1.png",
"/assets/img/home-3/choose-us/icon-2.png",
"/assets/img/home-3/choose-us/icon-3.png",
],
};
const trimString = (value) =>
typeof value === "string" ? value.trim() : "";
const normalizePath = (value) => {
const trimmed = trimString(value);
if (!trimmed) {
return "";
}
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
return trimmed;
}
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
};
const isPlaceholderLike = (value) => {
const normalized = normalizePath(value).toLowerCase();
return !normalized || normalized.endsWith(`/${GENERIC_PLACEHOLDER}`);
};
const normalizeButton = (value = {}) => ({
label: trimString(value?.label),
href: trimString(value?.href),
});
const repairIconItems = (items, fallbacks) =>
Array.isArray(items)
? items.map((item, index) => ({
...item,
icon: isPlaceholderLike(item?.icon)
? fallbacks[index % fallbacks.length]
: normalizePath(item?.icon),
}))
: [];
const buildCanonicalAboutData = (source = {}) => ({
hero: {
title: trimString(source?.hero?.title),
breadcrumb: Array.isArray(source?.hero?.breadcrumb)
? source.hero.breadcrumb.map(trimString).filter(Boolean)
: [],
backgroundImage: isPlaceholderLike(source?.hero?.backgroundImage)
? ABOUT_DEFAULTS.heroBackground
: normalizePath(source?.hero?.backgroundImage),
},
intro: {
subheading: trimString(source?.intro?.subheading),
heading: trimString(source?.intro?.heading),
description: trimString(source?.intro?.description),
image: normalizePath(source?.intro?.image),
},
mission: {
subheading: trimString(source?.mission?.subheading),
heading: trimString(source?.mission?.heading),
description: trimString(source?.mission?.description),
images: {
main: normalizePath(source?.mission?.images?.main),
secondary: normalizePath(source?.mission?.images?.secondary),
},
items: repairIconItems(source?.mission?.items, ABOUT_DEFAULTS.missionIcons)
.map((item) => ({
icon: item.icon,
label: trimString(item?.label),
description: trimString(item?.description),
}))
.filter((item) => item.icon || item.label || item.description),
features: Array.isArray(source?.mission?.features)
? source.mission.features.map(trimString).filter(Boolean)
: [],
ctaButton: normalizeButton(source?.mission?.ctaButton),
},
features: {
backgroundImage: isPlaceholderLike(source?.features?.backgroundImage)
? ABOUT_DEFAULTS.featuresBackground
: normalizePath(source?.features?.backgroundImage),
subheading: trimString(source?.features?.subheading),
heading: trimString(source?.features?.heading),
description: trimString(source?.features?.description),
image: isPlaceholderLike(source?.features?.image)
? ABOUT_DEFAULTS.featuresImage
: normalizePath(source?.features?.image),
items: repairIconItems(source?.features?.items, ABOUT_DEFAULTS.featureIcons)
.map((item) => ({
icon: item.icon,
title: trimString(item?.title),
description: trimString(item?.description),
}))
.filter((item) => item.icon || item.title || item.description),
ctaButton: normalizeButton(source?.features?.ctaButton),
},
news: {
subheading: trimString(source?.news?.subheading),
heading: trimString(source?.news?.heading),
ctaButton: normalizeButton(source?.news?.ctaButton),
selectedBlogIds: Array.isArray(source?.news?.selectedBlogIds)
? source.news.selectedBlogIds.filter(Boolean)
: [],
items: [],
},
});
async function up() {
await connectDB();
try {
const doc = await AboutUs.getSingle();
const repaired = buildCanonicalAboutData(doc.toObject());
doc.set(repaired);
await doc.save();
jsonHelper.writeJsonFile("about", repaired);
console.log("✓ Repaired About CMS contract");
console.log(` - Database: ${mongoose.connection.db.databaseName}`);
console.log(" - Canonicalized About singleton fields");
console.log(" - Backfilled hero/features/images/icons when placeholder-like");
} catch (error) {
console.error("✗ Failed to repair About contract:", error);
throw error;
} finally {
await mongoose.disconnect();
}
}
async function down() {
console.log(
" Rollback skipped for 2026_04_10_210000_repair_about_contract because the migration normalizes live content in place.",
);
}
if (require.main === module) {
up()
.then(() => {
console.log("\n✓ Migration script completed");
process.exit(0);
})
.catch((error) => {
console.error("\n✗ Migration script failed:", error);
process.exit(1);
});
}
module.exports = { up, down };