diff --git a/controllers/homeController.js b/controllers/homeController.js index e651588..d4eac73 100644 --- a/controllers/homeController.js +++ b/controllers/homeController.js @@ -1,138 +1,56 @@ -const { addBaseUrlToImages } = require("../utils/imageHelper"); +const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper"); const Home = require("../models/home"); +const Blog = require("../models/blog"); -// -------------------- Helpers -------------------- +// Các hàm hỗ trợ +const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 }); +const getHomeData = async () => (await getHomeDoc())?.toObject() || {}; -const getHomeDoc = async () => { - // Keep newest document as the source of truth - return await Home.findOne().sort({ updatedAt: -1 }); -}; - -const getHomeData = async () => { - const doc = await Home.findOne().sort({ updatedAt: -1 }).lean(); - return doc || {}; -}; - -/** - * Default structure used by the current CMS Home admin UI (`views/admin/home/index.ejs`). - * This is intentionally permissive; the Home model itself also supports the Next.js - * structure from `hailearning.edu.vn/app/home.json`. - */ const getDefaultHomeData = () => ({ - hero: { - title: "", - description: "", - backgroundImage: "", - button: { label: "Book Your Adventure", href: "/booking" }, - contactBox: { - welcomeText: "", - phone: { label: "Call us", number: "", href: "" }, - email: { label: "Email", address: "", href: "" }, - workingHours: { label: "Working Hours", hours: "" }, - }, - }, - about: { - title: "", - subtitle: "", - description: "", - images: { mainImage1: "", mainImage2: "", avatars: [] }, - features: [], - quote: "", - button: { label: "", href: "" }, - stats: { customerCount: 0, customerLabel: "" }, - }, - missionVision: { - title: "", - subtitle: "", - backgroundImage: "", - cards: [], - }, - whyChooseUs: { - title: "", - subtitle: "", - description: "", - button: { label: "", href: "" }, - features: [], - tags: [], - cta: { text: "", linkText: "", linkHref: "" }, - }, - activities: { cards: [] }, - faq: { - title: "", - subtitle: "", - description: "", - image: "", - contact: { title: "", info: "" }, - questions: [], - }, - partners: { - title: "", - subtitle: "", - backgroundImage: "", - logos: [], - cta: { badge: "", text: "", linkText: "", linkHref: "" }, - }, - programs: { - title: "", - subtitle: "", - button: { label: "", href: "" }, - card: { - pricePrefix: "from", - priceSuffix: "USD", - buttonLabel: "Camp Detail", - buttonHref: "/camp-profiles", - }, + hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} }, + whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} }, + visaSolutions: { heading: "", subheading: "", items: [] }, + visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} }, + testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] }, + videoGallery: { heading: "", videoUrl: "", thumbnail: "" }, + faq: { 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: [], - }, - newsletter: { - title: "", - subtitle: "", - description: "", - image: "", - decorativeImage: "", - button: { label: "", placeholder: "", href: "" }, - }, - latestPosts: { - title: "", - subtitle: "", - searchPlaceholder: "", - sidebarTitle: "", - blogPosts: [], - sidebarPosts: [], - featuredCard: { image: "", title: "", description: "" }, + selectedBlogIds: [] // Array of manually selected blog IDs }, }); -// -------------------- Admin -------------------- - +// Admin: Xem trang quản lý exports.index = async (req, res) => { try { let data = await getHomeData(); - - if (!data || Object.keys(data).length === 0) { - data = getDefaultHomeData(); - } else { - // Merge minimal defaults to keep the view safe - const defaults = getDefaultHomeData(); - data.hero = data.hero || defaults.hero; - data.about = data.about || defaults.about; - data.missionVision = data.missionVision || defaults.missionVision; - data.whyChooseUs = data.whyChooseUs || defaults.whyChooseUs; - data.activities = data.activities || defaults.activities; - data.faq = data.faq || defaults.faq; - data.partners = data.partners || defaults.partners; - data.programs = data.programs || defaults.programs; - data.newsletter = data.newsletter || defaults.newsletter; - data.latestPosts = data.latestPosts || defaults.latestPosts; - } + const defaults = getDefaultHomeData(); + + // Merge dữ liệu mặc định cho tất cả các phần + 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, }); @@ -143,73 +61,32 @@ 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) exports.update = async (req, res) => { try { - const currentDoc = await getHomeDoc(); - const currentData = currentDoc ? currentDoc.toObject() : {}; - const updatedData = { ...currentData }; - - // Quick fields (Hero) from classic form fields - if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) { - updatedData.hero = { - title: req.body.heroTitle || "", - description: req.body.heroDescription || "", - backgroundImage: req.body.heroBackgroundImage || "", - button: { - label: req.body.heroButtonLabel || "Book Your Adventure", - href: req.body.heroButtonHref || "/booking", - }, - contactBox: { - welcomeText: req.body.heroContactWelcome || "", - phone: { - label: "Call us", - number: req.body.heroContactPhone || "", - href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : "", - }, - email: { - label: "Email", - address: req.body.heroContactEmail || "", - href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : "", - }, - workingHours: { - label: "Working Hours", - hours: req.body.heroContactHours || "", - }, - }, - }; - } - - // Handle sections sent as JSON payloads const sections = [ - "hero", - "about", - "missionVision", - "whyChooseUs", - "activities", - "faq", - "partners", - "programs", - "newsletter", - "latestPosts", + "hero", "whyChooseUs", "visaSolutions", "visaCountries", + "testimonials", "videoGallery", "faq", "achievements", + "partners", "blogPreview" ]; + let doc = await getHomeDoc(); + if (!doc) { + doc = new Home({}); + } + let hasChanges = false; - for (const section of sections) { - if (!req.body[section]) continue; - - try { - const newSectionData = JSON.parse(req.body[section]); - const currentSectionData = currentData?.[section]; - - if (JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData)) { - updatedData[section] = newSectionData; + 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; + } catch (e) { + console.error(`Invalid JSON for ${section}:`, e); } - } catch (e) { - console.error(`Error processing section "${section}":`, e); - req.flash("error_msg", `Invalid JSON for section "${section}": ${e.message}`); - return req.session.save(() => res.redirect("/admin/home")); } } @@ -218,29 +95,75 @@ exports.update = async (req, res) => { return req.session.save(() => res.redirect("/admin/home")); } - if (currentDoc?._id) { - await Home.findByIdAndUpdate(currentDoc._id, updatedData, { new: true }); - } else { - await Home.create(updatedData); - } - - req.flash("success_msg", "Home data updated successfully"); + await doc.save(); + req.flash("success_msg", "Home page configuration has been updated!"); return req.session.save(() => res.redirect("/admin/home")); } catch (err) { console.error("Home update error:", err); - req.flash("error_msg", `Update error: ${err.message || "Unknown"}`); + req.flash("error_msg", `Update error: ${err.message}`); return req.session.save(() => res.redirect("/admin/home")); } }; -// -------------------- Public API -------------------- - +// 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 }); + } +}; exports.api = async (req, res) => { try { - const homeData = await getHomeData(); + let data = await getHomeData(); const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; - const processedData = addBaseUrlToImages(homeData, baseUrl); - return res.json(processedData); + + // === 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()); + }); + } + + // 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); } catch (err) { console.error("Home API error:", err); return res.status(500).json({ error: "Error loading home data" }); diff --git a/data/header-menu.json b/data/header-menu.json index 496291b..af22c79 100644 --- a/data/header-menu.json +++ b/data/header-menu.json @@ -132,36 +132,11 @@ { "label": "Blog", "slug": "blog", - "href": "#", + "href": "/blog", "type": "internal", "order": 5, "isActive": true, - "children": [ - { - "label": "Blog Grid", - "slug": "blog-grid", - "href": "/blog-grid", - "type": "internal", - "order": 1, - "isActive": true - }, - { - "label": "Blog Standard", - "slug": "blog-standard", - "href": "/blog", - "type": "internal", - "order": 2, - "isActive": true - }, - { - "label": "Blog Details", - "slug": "blog-details", - "href": "/blog-details", - "type": "internal", - "order": 3, - "isActive": true - } - ] + "children": [] }, { "label": "Contact Us", diff --git a/data/home.json b/data/home.json index 4ecd14f..92bd9ab 100644 --- a/data/home.json +++ b/data/home.json @@ -238,54 +238,50 @@ ] }, "partners": { - "heading": "Our Trusted Partners", - "items": [ - { - "name": "Best Visa Consultancy", - "logo": "/assets/img/home-1/feature/icon-1.png", - "year": "2025" - }, - { - "name": "Visa Success Award", - "logo": "/assets/img/home-1/feature/icon-2.png", - "year": "2025" - }, - { - "name": "Innovation Award", - "logo": "/assets/img/home-1/feature/icon-3.png", - "year": "2025" - }, - { - "name": "Global Education Partner", - "logo": "/assets/img/home-1/feature/icon-4.png", - "year": "2025" - }, - { - "name": "University Partner 1", - "logo": "/assets/img/home-1/brand/01.png", - "year": "2025" - }, - { - "name": "University Partner 2", - "logo": "/assets/img/home-1/brand/02.png", - "year": "2025" - }, - { - "name": "University Partner 3", - "logo": "/assets/img/home-1/brand/03.png", - "year": "2025" - }, - { - "name": "University Partner 4", - "logo": "/assets/img/home-1/brand/04.png", - "year": "2025" - }, - { - "name": "University Partner 5", - "logo": "/assets/img/home-1/brand/05.png", - "year": "2025" - } - ] + "visaConsultancy": { + "heading": "Our Achievements & Awards", + "items": [ + { + "name": "Best Visa Consultancy", + "icon": "/assets/img/home-1/feature/icon-1.png", + "year": "2025" + }, + { + "name": "Visa Success Award", + "icon": "/assets/img/home-1/feature/icon-2.png", + "year": "2025" + }, + { + "name": "Innovation Award", + "icon": "/assets/img/home-1/feature/icon-3.png", + "year": "2025" + }, + { + "name": "Global Education Partner", + "icon": "/assets/img/home-1/feature/icon-4.png", + "year": "2025" + } + ] + }, + "brands": { + "items": [ + { + "logo": "/assets/img/home-1/brand/01.png" + }, + { + "logo": "/assets/img/home-1/brand/02.png" + }, + { + "logo": "/assets/img/home-1/brand/03.png" + }, + { + "logo": "/assets/img/home-1/brand/04.png" + }, + { + "logo": "/assets/img/home-1/brand/05.png" + } + ] + } }, "blogPreview": { "heading": "Latest Insights & Updates", diff --git a/models/home.js b/models/home.js index cc2f48d..429d3be 100644 --- a/models/home.js +++ b/models/home.js @@ -156,19 +156,40 @@ const AchievementsSchema = new Schema( { _id: false }, ); -const PartnerItemSchema = new Schema( +const VisaConsultancyItemSchema = new Schema( { name: { type: String, default: "" }, - logo: { type: String, default: "" }, + icon: { type: String, default: "" }, year: { type: String, default: "" }, }, { _id: false }, ); +const VisaConsultancySchema = new Schema( + { + items: { type: [VisaConsultancyItemSchema], default: [] }, + }, + { _id: false }, +); + +const BrandItemSchema = new Schema( + { + logo: { type: String, default: "" }, + }, + { _id: false }, +); + +const BrandsSchema = new Schema( + { + items: { type: [BrandItemSchema], default: [] }, + }, + { _id: false }, +); + const PartnersSchema = new Schema( { - heading: { type: String, default: "" }, - items: { type: [PartnerItemSchema], default: [] }, + visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) }, + brands: { type: BrandsSchema, default: () => ({}) }, }, { _id: false }, ); @@ -196,6 +217,7 @@ const BlogPreviewSchema = new Schema( subheading: { type: String, default: "" }, ctaButton: { type: LinkSchema, default: () => ({}) }, items: { type: [BlogPreviewItemSchema], default: [] }, + selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }], }, { _id: false }, ); diff --git a/routes/admin.js b/routes/admin.js index d292f3f..8a7b199 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -37,6 +37,7 @@ router.get("/dashboard", ensureAuthenticated, dashboardController.getDashboard); // Home router.get("/home", ensureAuthenticated, homeController.index); router.post("/home/update", ensureAuthenticated, homeController.update); +router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs); // Middleware chuẩn hóa code router.param("code", (req, res, next, code) => { diff --git a/views/admin/home/index.ejs b/views/admin/home/index.ejs index dbf860d..4296913 100644 --- a/views/admin/home/index.ejs +++ b/views/admin/home/index.ejs @@ -7,11 +7,7 @@
Edit content displayed on homepage
@@ -19,75 +15,51 @@