forked from UKSOURCE/cms.hailearning.edu.vn
- 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
170 lines
5.3 KiB
JavaScript
170 lines
5.3 KiB
JavaScript
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 };
|