Refactor Homepage, About, Footer (Controller,Model, View, Data), Update: Dashboard, Delete old file

This commit is contained in:
2026-04-22 15:30:09 +07:00
parent cb5e4b0ceb
commit 3ede974215
67 changed files with 3003 additions and 14780 deletions
+35 -152
View File
@@ -1,99 +1,63 @@
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
const Home = require("../models/home");
const Blog = require("../models/blog");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
const { addBaseUrlToImages } = require("../utils/imageHelper");
// Các hàm hỗ trợ
// Helpers
const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 });
const getHomeData = async () => (await getHomeDoc())?.toObject() || {};
const getDefaultHomeData = () => ({
hero: {
backgroundImage: "",
slides: [],
badge: "",
title: "",
subtitle: "",
description: "",
heroImage: "",
videoUrl: "",
primaryButton: {},
secondaryButton: {},
searchPlaceholder: "",
buttonLabel: "",
image: "",
imageAlt: "",
floatingBadge: {
icon: "",
value: "",
label: "",
},
},
whyChooseUs: {
heading: "",
subheading: "",
quickLinks: [],
valueProp: {
badge: "",
title: "",
description: "",
highlightWord: "",
mainImage: "",
secondaryImage: "",
items: [],
features: [],
ctaButton: {},
stats: [],
},
visaSolutions: { heading: "", subheading: "", items: [] },
visaCountries: {
programs: {
heading: "",
subheading: "",
description: "",
countries: [],
ctaButton: {},
},
testimonials: {
heading: "",
subheading: "",
videoUrl: "",
videoThumbnail: "",
items: [],
},
videoGallery: { heading: "", videoUrl: "", thumbnail: "" },
faq: {
requestInfo: {
heading: "",
subheading: "",
description: "",
ctaButton: {},
items: [],
},
achievements: { heading: "", subheading: "", items: [] },
partners: { visaConsultancy: { items: [] }, brands: { items: [] } },
blogPreview: {
heading: "Latest Insights & Updates",
subheading: "Visa Tips & Guides",
ctaButton: { label: "View All Articles", href: "/blog" },
items: [],
selectedBlogIds: [], // Array of manually selected blog IDs
phone: "",
email: "",
programs: [],
},
});
// Admin: Xem trang quản lý
// Admin: render management view with data from MongoDB
exports.index = async (req, res) => {
try {
let data = await getHomeData();
const defaults = getDefaultHomeData();
// Merge dữ liệu mặc định cho tất cả các phần
// Merge defaults for any missing sections
const sections = Object.keys(defaults);
sections.forEach((s) => {
data[s] = data[s] || defaults[s];
});
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
// Lấy tất cả blog để chọn trong CMS
const allBlogs = await Blog.find({ status: "published" })
.sort({ createdAt: -1 })
.lean();
return res.render("admin/home/index", {
layout: "layouts/main",
title: "Home Management",
data,
allBlogs,
frontendUrl,
backendUrl,
getFullImageUrl,
currentPath: req.path,
user: req.session.user,
});
@@ -104,43 +68,34 @@ exports.index = async (req, res) => {
}
};
// Admin: Cập nhật dữ liệu (tập trung vào achievements, partners; các phần khác giữ nguyên nếu không có dữ liệu mới)
// Admin: parse req.body sections and save to MongoDB
exports.update = async (req, res) => {
try {
const sections = [
"hero",
"whyChooseUs",
"visaSolutions",
"visaCountries",
"testimonials",
"videoGallery",
"faq",
"achievements",
"partners",
"blogPreview",
"quickLinks",
"valueProp",
"programs",
"requestInfo",
];
let doc = await getHomeDoc();
const beforeData = doc ? JSON.parse(JSON.stringify(doc.toObject())) : {};
if (!doc) {
doc = new Home({});
}
let hasChanges = false;
const updatedSections = [];
for (const section of sections) {
if (req.body[section]) {
try {
const payload = JSON.parse(req.body[section]);
// Gán trực tiếp vào doc, Mongoose sẽ tự check schema
doc[section] = payload;
doc.markModified(section);
hasChanges = true;
updatedSections.push(section);
} catch (e) {
console.error(`Invalid JSON for ${section}:`, e);
console.error(`Invalid JSON for ${section}:`, e.message);
}
}
}
@@ -151,21 +106,6 @@ exports.update = async (req, res) => {
}
await doc.save();
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
// ✅ AUDIT LOGGING - Home Update
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Home",
documentId: doc._id,
action: AUDIT_ACTIONS.UPDATE_HOME,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Home page configuration has been updated!");
return req.session.save(() => res.redirect("/admin/home"));
@@ -176,72 +116,15 @@ exports.update = async (req, res) => {
}
};
// Public API// API lấy danh sách blog cho CMS
exports.apiGetBlogs = async (req, res) => {
try {
const blogs = await Blog.find({ status: "published" })
.sort({ createdAt: -1 })
.select("title slug featuredImage author publishedAt")
.lean();
res.json(blogs);
} catch (err) {
res.status(500).json({ error: err.message });
}
};
// Public API: return JSON data for frontend
exports.api = async (req, res) => {
try {
let data = await getHomeData();
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
// === Xử lý Blog Preview động ===
const blogPreview = data.blogPreview || {};
let blogs = [];
// Nếu có chọn blog cụ thể
if (blogPreview.selectedBlogIds && blogPreview.selectedBlogIds.length > 0) {
blogs = await Blog.find({
_id: { $in: blogPreview.selectedBlogIds },
status: "published",
}).lean();
// Sắp xếp theo thứ tự đã chọn trong selectedBlogIds
blogs.sort((a, b) => {
return (
blogPreview.selectedBlogIds.indexOf(a._id.toString()) -
blogPreview.selectedBlogIds.indexOf(b._id.toString())
);
});
if (!data || Object.keys(data).length === 0) {
data = getDefaultHomeData();
}
// Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất (hoặc bù vào)
if (blogs.length === 0) {
blogs = await Blog.find({ status: "published" })
.sort({ createdAt: -1 })
.limit(3)
.lean();
}
// Map dữ liệu blog sang format mà frontend mong đợi
blogPreview.items = blogs.map((blog) => ({
title: blog.title,
excerpt: blog.excerpt,
category: blog.category && blog.category[0] ? blog.category[0] : "Visa",
date: blog.publishedAt || blog.createdAt,
author: {
name: blog.author || "Admin",
avatar: "", // Frontend đang tự xử lý hoặc dùng logo hệ thống
},
comments: blog.commentsCount || 0,
link: `/blog/${blog.slug}`,
thumbnail: blog.featuredImage,
}));
data.blogPreview = blogPreview;
// ===============================
const processed = addBaseUrlToImages(data, baseUrl);
return res.json(processed);
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
return res.json(addBaseUrlToImages(data, baseUrl));
} catch (err) {
console.error("Home API error:", err);
return res.status(500).json({ error: "Error loading home data" });