From 7ec9bccad58082c85767a970a8ab4114e271a65d Mon Sep 17 00:00:00 2001 From: Wini_Fy Date: Thu, 5 Feb 2026 21:19:51 +0700 Subject: [PATCH 1/2] refactor: enhance home page structure and content management --- controllers/homeController.js | 52 ++- data/home.json | 11 +- models/home.js | 26 +- views/admin/home/index.ejs | 24 +- views/admin/home/sections/faq.ejs | 102 +++-- views/admin/home/sections/hero.ejs | 425 +++++++++++++------- views/admin/home/sections/testimonials.ejs | 268 +++++++----- views/admin/home/sections/videoGallery.ejs | 62 ++- views/admin/home/sections/visaCountries.ejs | 180 ++++----- views/admin/home/sections/visaSolutions.ejs | 163 +++++--- views/admin/home/sections/whyChooseUs.ejs | 240 +++++++---- 11 files changed, 951 insertions(+), 602 deletions(-) diff --git a/controllers/homeController.js b/controllers/homeController.js index d4eac73..fbc55e8 100644 --- a/controllers/homeController.js +++ b/controllers/homeController.js @@ -7,8 +7,28 @@ const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 }); const getHomeData = async () => (await getHomeDoc())?.toObject() || {}; const getDefaultHomeData = () => ({ - hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} }, - whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} }, + hero: { + backgroundImage: "", + slides: [], + title: "", + subtitle: "", + description: "", + heroImage: "", + videoUrl: "", + primaryButton: {}, + secondaryButton: {}, + }, + whyChooseUs: { + heading: "", + subheading: "", + description: "", + highlightWord: "", + mainImage: "", + secondaryImage: "", + items: [], + features: [], + ctaButton: {}, + }, visaSolutions: { heading: "", subheading: "", items: [] }, visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} }, testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] }, @@ -16,10 +36,10 @@ const getDefaultHomeData = () => ({ 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" }, + blogPreview: { + heading: "Latest Insights & Updates", + subheading: "Visa Tips & Guides", + ctaButton: { label: "View All Articles", href: "/blog" }, items: [], selectedBlogIds: [] // Array of manually selected blog IDs }, @@ -30,7 +50,7 @@ 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 const sections = Object.keys(defaults); sections.forEach(s => { @@ -39,7 +59,7 @@ exports.index = async (req, res) => { 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(); @@ -65,8 +85,8 @@ exports.index = async (req, res) => { exports.update = async (req, res) => { try { const sections = [ - "hero", "whyChooseUs", "visaSolutions", "visaCountries", - "testimonials", "videoGallery", "faq", "achievements", + "hero", "whyChooseUs", "visaSolutions", "visaCountries", + "testimonials", "videoGallery", "faq", "achievements", "partners", "blogPreview" ]; @@ -122,20 +142,20 @@ exports.api = async (req, res) => { // === 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({ + blogs = await Blog.find({ _id: { $in: blogPreview.selectedBlogIds }, - status: "published" + 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" }) diff --git a/data/home.json b/data/home.json index 92bd9ab..0d4df4a 100644 --- a/data/home.json +++ b/data/home.json @@ -1,7 +1,6 @@ { - "hero": { - "title": "From Application to Visa – We've Got You Covered", + "title": "From Application to Visa – We’ve Got You Covered", "subtitle": "Global Education Simplified", "description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.", "primaryButton": { @@ -48,25 +47,25 @@ "number": "01", "title": "Student Visa Guidance", "description": "Assistance with admission, documentation, and visa application.Assistance", - "link": "/service-details" + "link": "/services/student-visa" }, { "number": "02", "title": "PTE Exam Preparation", "description": "We provide expert guidance and personalized support throughout the education visa process,", - "link": "/service-details" + "link": "/services/pte-exam" }, { "number": "03", "title": "University Selection Assistance", "description": "We provide expert guidance and personalized support throughout the education visa process,", - "link": "/service-details" + "link": "/services/university-selection" }, { "number": "04", "title": "IELTS Exam Preparation", "description": "We provide expert guidance and personalized support throughout the education visa process,", - "link": "/service-details" + "link": "/services/ielts-exam" } ] }, diff --git a/models/home.js b/models/home.js index 429d3be..09d3d52 100644 --- a/models/home.js +++ b/models/home.js @@ -11,14 +11,35 @@ const LinkSchema = new Schema( { _id: false }, ); -const HeroSchema = new Schema( +// Hero slide (for multiple hero items in slider) +const HeroSlideSchema = new Schema( { title: { type: String, default: "" }, subtitle: { type: String, default: "" }, description: { type: String, default: "" }, primaryButton: { type: LinkSchema, default: () => ({}) }, secondaryButton: { type: LinkSchema, default: () => ({}) }, + heroImage: { type: String, default: "" }, + videoUrl: { type: String, default: "" }, + }, + { _id: false }, +); + +const HeroSchema = new Schema( + { + // Background for whole hero section backgroundImage: { type: String, default: "" }, + + // Multiple slides + slides: { type: [HeroSlideSchema], default: [] }, + + // Legacy single-slide fields (backward compatible) + title: { type: String, default: "" }, + subtitle: { type: String, default: "" }, + description: { type: String, default: "" }, + primaryButton: { type: LinkSchema, default: () => ({}) }, + secondaryButton: { type: LinkSchema, default: () => ({}) }, + heroImage: { type: String, default: "" }, videoUrl: { type: String, default: "" }, }, { _id: false }, @@ -38,6 +59,9 @@ const WhyChooseUsSchema = new Schema( heading: { type: String, default: "" }, subheading: { type: String, default: "" }, description: { type: String, default: "" }, + highlightWord: { type: String, default: "" }, + mainImage: { type: String, default: "" }, + secondaryImage: { type: String, default: "" }, items: { type: [WhyChooseUsItemSchema], default: [] }, features: { type: [String], default: [] }, ctaButton: { type: LinkSchema, default: () => ({}) }, diff --git a/views/admin/home/index.ejs b/views/admin/home/index.ejs index 4296913..f094b65 100644 --- a/views/admin/home/index.ejs +++ b/views/admin/home/index.ejs @@ -88,15 +88,15 @@
<%- include('sections/hero') %> - <%- include('sections/whyChooseUs') %> - <%- include('sections/visaSolutions') %> - <%- include('sections/visaCountries') %> - <%- include('sections/testimonials') %> - <%- include('sections/videoGallery') %> - <%- include('sections/faq') %> - <%- include('sections/achievements') %> - <%- include('sections/partners') %> - <%- include('sections/blogPreview') %> + <%- include('sections/whyChooseUs') %> + <%- include('sections/visaSolutions') %> + <%- include('sections/visaCountries') %> + <%- include('sections/testimonials') %> + <%- include('sections/videoGallery') %> + <%- include('sections/faq') %> + <%- include('sections/achievements') %> + <%- include('sections/partners') %> + <%- include('sections/blogPreview') %>
@@ -136,9 +136,9 @@ document.addEventListener("DOMContentLoaded", function () { const form = document.querySelector("form"); if (form) { - form.addEventListener("submit", function(e) { + form.addEventListener("submit", function (e) { console.log("Form submitting, collecting data from scrapers..."); - + // Tự động thu gom dữ liệu từ các section đã đăng ký Object.keys(window.homeScrapers).forEach(section => { const input = document.getElementById(section + 'Json'); @@ -164,7 +164,7 @@ // --- UTILITIES (Dùng chung) --- function initImageUploads() { - document.addEventListener("click", function(e) { + document.addEventListener("click", function (e) { const btn = e.target.closest(".btn-upload-image"); if (btn) { document.getElementById("currentImageType").value = btn.dataset.imageType; diff --git a/views/admin/home/sections/faq.ejs b/views/admin/home/sections/faq.ejs index 021c06a..7176faf 100644 --- a/views/admin/home/sections/faq.ejs +++ b/views/admin/home/sections/faq.ejs @@ -13,32 +13,18 @@
- +
- +
- +
@@ -48,16 +34,33 @@
-
+
FAQ Items
-
- <% (data.faq?.items || []).forEach(function(item, index) { %> -
+
+ <% + const faqItems = (data.faq && Array.isArray(data.faq.items) && data.faq.items.length === 5) + ? data.faq.items + : (data.faq && Array.isArray(data.faq.items) && data.faq.items.length > 0) + ? (function () { + const clone = data.faq.items.slice(0, 5); + while (clone.length < 5) clone.push({}); + return clone; + })() + : [{}, {}, {}, {}, {}]; + %> + <% faqItems.forEach(function(item, index) { %> +
+
+
FAQ + + <%= index + 1 %> + +
+
-
FAQ <%= index + 1 %>
@@ -98,23 +101,13 @@
- +
- +
@@ -122,3 +115,34 @@
+ + \ No newline at end of file diff --git a/views/admin/home/sections/hero.ejs b/views/admin/home/sections/hero.ejs index c1f333f..92a0e84 100644 --- a/views/admin/home/sections/hero.ejs +++ b/views/admin/home/sections/hero.ejs @@ -1,157 +1,312 @@
- +
-
-
+
+
- Basic Information + Hero Background
-
- - -
-
- - -
-
- - -
- -
<% if (data.hero?.backgroundImage) { %> -
- Background preview +
+ Background preview +
+ <% } %> +
+
+
+
+
+ + +
+
+
+
+ Hero Slides +
+ +
+
+ <% const existingSlides=(data.hero && Array.isArray(data.hero.slides) && data.hero.slides.length> 0) + ? data.hero.slides + : [{ + title: data.hero?.title || '', + subtitle: data.hero?.subtitle || '', + description: data.hero?.description || '', + heroImage: data.hero?.heroImage || '', + videoUrl: data.hero?.videoUrl || '', + primaryButton: data.hero?.primaryButton || {}, + secondaryButton: data.hero?.secondaryButton || {}, + }]; + %> + + <% existingSlides.forEach(function(slide, index) { %> +
+
+ Slide + + <%= index + 1 %> + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + Recommended size: 893x848px +
+ + +
+ <% if (slide.heroImage) { %> +
+ Hero image preview +
+ <% } %> +
+
+ + +
+ + +
+
+
+ Primary Button +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ Secondary Button +
+
+
+ + +
+
+ + +
+
+
+
+
+
- <% } %> -
-
- - -
-
-
-
-
- - -
-
-
-
- Primary Button -
-
-
-
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
- Secondary Button -
-
-
-
-
- - -
-
- - -
-
+ <% }); %>
+ + \ No newline at end of file diff --git a/views/admin/home/sections/testimonials.ejs b/views/admin/home/sections/testimonials.ejs index 55f91ac..650af05 100644 --- a/views/admin/home/sections/testimonials.ejs +++ b/views/admin/home/sections/testimonials.ejs @@ -13,49 +13,26 @@
- +
- +
- +
- -
@@ -68,92 +45,167 @@
-
+
Testimonials
+
-
+
<% (data.testimonials?.items || []).forEach(function(item, index) { %> -
-
-
Testimonial <%= index + 1 %>
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - +
+
+
Testimonial + <%= index + 1 %> +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
-
- <% }); %> + <% }); %>
+ + \ No newline at end of file diff --git a/views/admin/home/sections/videoGallery.ejs b/views/admin/home/sections/videoGallery.ejs index 4398814..3ceb355 100644 --- a/views/admin/home/sections/videoGallery.ejs +++ b/views/admin/home/sections/videoGallery.ejs @@ -12,52 +12,30 @@
- +
- +
- -
<% if (data.videoGallery?.thumbnail) { %> -
- Thumbnail preview -
- <% } %> +
+ Thumbnail preview +
+ <% } %>
@@ -65,3 +43,17 @@
+ + \ No newline at end of file diff --git a/views/admin/home/sections/visaCountries.ejs b/views/admin/home/sections/visaCountries.ejs index 73472f8..67012ad 100644 --- a/views/admin/home/sections/visaCountries.ejs +++ b/views/admin/home/sections/visaCountries.ejs @@ -13,114 +13,76 @@
- +
- +
- +
- +
- Countries + Featured Country
+ This country is used in the home page feature section.
- <% (data.visaCountries?.countries || []).forEach(function(country, index) { %> -
-
-
Country <%= index + 1 %>
-
-
- - -
-
- - -
-
- -
- - + <% const featured=(data.visaCountries && Array.isArray(data.visaCountries.countries) && + data.visaCountries.countries.length> 0) + ? data.visaCountries.countries[0] + : {}; + %> +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ +
-
-
- - -
-
- -
-
- <% }); %>
@@ -137,23 +99,13 @@
- +
- +
@@ -161,3 +113,33 @@
+ + \ No newline at end of file diff --git a/views/admin/home/sections/visaSolutions.ejs b/views/admin/home/sections/visaSolutions.ejs index bfad6f1..1fefdcb 100644 --- a/views/admin/home/sections/visaSolutions.ejs +++ b/views/admin/home/sections/visaSolutions.ejs @@ -13,23 +13,13 @@
- +
- +
@@ -39,62 +29,109 @@
-
+
Visa Solutions Items
-
- <% (data.visaSolutions?.items || []).forEach(function(item, index) { %> -
-
-
Service <%= index + 1 %>
-
-
- - +
+ <% const vsItems=(data.visaSolutions && Array.isArray(data.visaSolutions.items) && + data.visaSolutions.items.length===4) ? data.visaSolutions.items : (data.visaSolutions && + Array.isArray(data.visaSolutions.items) && data.visaSolutions.items.length> 0) + ? (function () { + const clone = data.visaSolutions.items.slice(0, 4); + while (clone.length < 4) clone.push({}); return clone; })() : [{}, {}, {}, {}]; %> + <% vsItems.forEach(function(item, index) { %> +
+
+
Service + <%= index + 1 %> +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
-
- - -
-
- - -
-
- - -
-
-
-
- <% }); %> + <% }); %>
+ + \ No newline at end of file diff --git a/views/admin/home/sections/whyChooseUs.ejs b/views/admin/home/sections/whyChooseUs.ejs index 2a1a24b..fd98ef3 100644 --- a/views/admin/home/sections/whyChooseUs.ejs +++ b/views/admin/home/sections/whyChooseUs.ejs @@ -3,7 +3,7 @@
-
+
Basic Information @@ -13,32 +13,78 @@
- + placeholder="e.g., Turning Study Abroad Dreams Into Reality" />
- + +
+
+ + + This word in the heading will be wrapped in a colored span.
- + +
+
+
+
+
+ + +
+
+
+
+ About Images (Left side) +
+
+
+
+
+ + Recommended size: 375x419px +
+ + +
+ <% if (data.whyChooseUs?.mainImage) { %> +
+ Main about image preview +
+ <% } %> +
+ +
+ + Recommended size: 376x394px +
+ + +
+ <% if (data.whyChooseUs?.secondaryImage) { %> +
+ Secondary about image preview +
+ <% } %>
@@ -55,53 +101,36 @@
<% (data.whyChooseUs?.items || []).forEach(function(item, index) { %> -
-
-
Item <%= index + 1 %>
-
-
- -
- - +
+
+
Item <%= index + 1 %> +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
-
-
- - -
-
- -
-
- <% }); %> + <% }); %>
@@ -116,17 +145,12 @@
<% (data.whyChooseUs?.features || []).forEach(function(feature, index) { %> -
- - -
- <% }); %> +
+ + +
+ <% }); %>
@@ -143,23 +167,13 @@
- +
- +
@@ -167,3 +181,53 @@
+ + \ No newline at end of file From ade8fc7e08c3caf3f6d017f30be3d70d22601727 Mon Sep 17 00:00:00 2001 From: Wini_Fy Date: Thu, 5 Feb 2026 21:22:06 +0700 Subject: [PATCH 2/2] Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/thanh-05022026-home