From ed09c7fa894290694aeba8a3058badd497caa66a 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: Thu, 9 Apr 2026 22:03:00 +0700 Subject: [PATCH] Align home hero API and CMS hero editor --- controllers/homeController.js | 79 +++++++++++++++++++++++++++++- views/admin/home/sections/hero.ejs | 44 ++++++++++++----- 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/controllers/homeController.js b/controllers/homeController.js index 2040cd9..9b8e2e7 100644 --- a/controllers/homeController.js +++ b/controllers/homeController.js @@ -10,6 +10,7 @@ const AUDIT_ACTIONS = require("../constants/auditAction"); // Các hàm hỗ trợ const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 }); +const getAllHomeDocs = async () => Home.find().sort({ updatedAt: -1 }); const getHomeData = async () => (await getHomeDoc())?.toObject() || {}; const getOrCreateHomeDoc = async () => { let doc = await getHomeDoc(); @@ -21,6 +22,77 @@ const getOrCreateHomeDoc = async () => { return doc; }; +const hasMeaningfulValue = (value) => + typeof value === "string" && value.trim().length > 0; + +const getMeaningfulHeroSlides = (hero = {}) => { + if (!Array.isArray(hero.slides)) return []; + + return hero.slides.filter((slide = {}) => { + return ( + hasMeaningfulValue(slide.title) || + hasMeaningfulValue(slide.subtitle) || + hasMeaningfulValue(slide.description) || + hasMeaningfulValue(slide.heroImage) || + hasMeaningfulValue(slide.videoUrl) || + hasMeaningfulValue(slide.primaryButton?.label) || + hasMeaningfulValue(slide.primaryButton?.href) || + hasMeaningfulValue(slide.secondaryButton?.label) || + hasMeaningfulValue(slide.secondaryButton?.href) + ); + }); +}; + +const scoreHeroSlides = (slides = []) => + slides.reduce((score, slide = {}) => { + return ( + score + + (slide.title || "").trim().length * 3 + + (slide.subtitle || "").trim().length + + (slide.description || "").trim().length * 2 + + (slide.primaryButton?.label || "").trim().length + + (slide.primaryButton?.href || "").trim().length + + (slide.secondaryButton?.label || "").trim().length + + (slide.secondaryButton?.href || "").trim().length + + (hasMeaningfulValue(slide.heroImage) ? 20 : 0) + + (hasMeaningfulValue(slide.videoUrl) ? 12 : 0) + ); + }, 0); + +const scoreHeroData = (hero = {}) => { + const slides = getMeaningfulHeroSlides(hero); + + if (slides.length > 0) { + return scoreHeroSlides(slides) + slides.length * 30; + } + + return ( + (hero.title || "").trim().length * 3 + + (hero.subtitle || "").trim().length + + (hero.description || "").trim().length * 2 + + (hero.primaryButton?.label || "").trim().length + + (hero.primaryButton?.href || "").trim().length + + (hero.secondaryButton?.label || "").trim().length + + (hero.secondaryButton?.href || "").trim().length + + (hasMeaningfulValue(hero.heroImage) ? 20 : 0) + + (hasMeaningfulValue(hero.videoUrl) ? 12 : 0) + ); +}; + +const getPreferredHeroData = (docs = []) => { + const heroes = docs + .map((doc) => doc?.hero) + .filter(Boolean); + + if (!heroes.length) return {}; + + return heroes.reduce((bestHero, currentHero) => { + return scoreHeroData(currentHero) > scoreHeroData(bestHero) + ? currentHero + : bestHero; + }, heroes[0]); +}; + const normalizeStoredImagePath = (imagePath) => { if (!imagePath || typeof imagePath !== "string") return ""; @@ -373,9 +445,14 @@ exports.apiGetBlogs = async (req, res) => { }; exports.api = async (req, res) => { try { - let data = await getHomeData(); + const docs = await getAllHomeDocs(); + let data = docs[0]?.toObject() || {}; const baseUrl = `${req.protocol}://${req.get("host")}`; + if (docs.length > 1) { + data.hero = getPreferredHeroData(docs.map((doc) => doc.toObject())); + } + // === Xử lý Blog Preview động === const blogPreview = data.blogPreview || {}; let blogs = []; diff --git a/views/admin/home/sections/hero.ejs b/views/admin/home/sections/hero.ejs index 92a0e84..b2c78de 100644 --- a/views/admin/home/sections/hero.ejs +++ b/views/admin/home/sections/hero.ejs @@ -1,21 +1,30 @@
-
- Hero Background + Hero Carousel Setup
-
-
- +
+
Current homepage hero behavior
+
+ Mỗi slide dùng ảnh riêng làm nền full-width. Title, description và 2 button chỉ là lớp overlay. + Trường video hiện không còn được hiển thị ở frontend. +
+
+
+
+ + + Tùy chọn dự phòng. Homepage hiện ưu tiên ảnh của từng slide. +
+ value="<%= data.hero?.backgroundImage || '' %>" placeholder="/uploads/home/hero-fallback.jpg" />
<% } %>
+
+
+
Recommended content structure
+
    +
  • Dùng ảnh slide tỉ lệ ngang, ưu tiên ảnh lớn để fit container.
  • +
  • Title ngắn 2-4 dòng để không tràn trên mobile.
  • +
  • Description giữ ở mức 1-3 câu ngắn.
  • +
  • Hai nút nên dùng link nội bộ như /contact.
  • +
+
+
@@ -79,6 +99,7 @@
+ Hiện không render ngoài frontend, chỉ giữ để tương thích dữ liệu cũ.
@@ -88,11 +109,11 @@ placeholder="Enter hero description"><%= slide.description || '' %>
- - Recommended size: 893x848px + + Ảnh này đang được dùng làm nền full hero. Khuyến nghị ảnh ngang lớn hoặc GIF nếu cần.
+ value="<%= slide.heroImage || '' %>" placeholder="/uploads/home/hero-slide-01.jpg" />
+ Frontend hiện không render video trong hero. Giữ lại chỉ để tránh mất dữ liệu cũ. + value="<%= slide.videoUrl || '' %>" placeholder="Không bắt buộc" />
@@ -309,4 +331,4 @@ // Initial normalization (in case indices rendered from server are not 0..n) updateLabels(); }); - \ No newline at end of file +