From 7ce5921fe03fb8250e79977d23dfb22d39a276a8 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: Fri, 10 Apr 2026 01:38:30 +0700 Subject: [PATCH] feat: Improve home and contact CMS field guidance --- models/contact.js | 17 +- models/home.js | 168 ++++++++++-------- views/admin/contact/index.ejs | 155 ++++++++++++---- views/admin/home/index.ejs | 132 ++++++++++++++ views/admin/home/sections/achievements.ejs | 16 ++ views/admin/home/sections/blogPreview.ejs | 14 +- views/admin/home/sections/faq.ejs | 16 +- views/admin/home/sections/floatingContact.ejs | 49 ++--- views/admin/home/sections/hero.ejs | 39 ++-- views/admin/home/sections/partners.ejs | 20 ++- views/admin/home/sections/testimonials.ejs | 34 ++-- views/admin/home/sections/videoGallery.ejs | 12 +- views/admin/home/sections/visaCountries.ejs | 28 +-- views/admin/home/sections/visaSolutions.ejs | 19 +- views/admin/home/sections/whyChooseUs.ejs | 40 +++-- 15 files changed, 529 insertions(+), 230 deletions(-) diff --git a/models/contact.js b/models/contact.js index 26df236..26c9d3f 100644 --- a/models/contact.js +++ b/models/contact.js @@ -7,11 +7,13 @@ const heroSchema = new mongoose.Schema( type: String, required: true, trim: true, + maxlength: 40, }, backgroundImage: { type: String, trim: true, default: "", + maxlength: 255, }, overlayColor: { type: String, @@ -62,9 +64,10 @@ const contactCardSchema = new mongoose.Schema( type: String, required: true, trim: true, + maxlength: 40, }, content: { - type: [String], + type: [{ type: String, maxlength: 96 }], default: [], }, iconType: { @@ -72,6 +75,7 @@ const contactCardSchema = new mongoose.Schema( required: false, trim: true, default: "", + maxlength: 255, }, iconSource: { type: String, @@ -139,16 +143,19 @@ const mapSchema = new mongoose.Schema( type: String, required: true, trim: true, + maxlength: 120, }, markerTitle: { type: String, trim: true, default: "", + maxlength: 48, }, embedUrl: { type: String, trim: true, default: "", + maxlength: 1000, }, tileLayer: { type: tileLayerSchema, @@ -165,11 +172,13 @@ const formFieldSchema = new mongoose.Schema( type: String, required: true, trim: true, + maxlength: 32, }, label: { type: String, trim: true, default: "", + maxlength: 32, }, type: { type: String, @@ -181,6 +190,7 @@ const formFieldSchema = new mongoose.Schema( type: String, trim: true, default: "", + maxlength: 72, }, required: { type: Boolean, @@ -195,6 +205,7 @@ const formFieldSchema = new mongoose.Schema( type: String, trim: true, default: "", + maxlength: 48, }, }, { _id: false } @@ -207,6 +218,7 @@ const submitButtonSchema = new mongoose.Schema( type: String, required: true, trim: true, + maxlength: 24, }, icon: { type: String, @@ -229,16 +241,19 @@ const formSchema = new mongoose.Schema( type: String, trim: true, default: "", + maxlength: 32, }, heading: { type: String, trim: true, default: "", + maxlength: 48, }, description: { type: String, trim: true, default: "", + maxlength: 160, }, fields: { type: [formFieldSchema], diff --git a/models/home.js b/models/home.js index d2bc0ee..7d5edfd 100644 --- a/models/home.js +++ b/models/home.js @@ -5,15 +5,15 @@ const { Schema } = mongoose; // Reusable small schemas const LinkSchema = new Schema( { - label: { type: String, default: "" }, - href: { type: String, default: "" }, + label: { type: String, default: "", maxlength: 32 }, + href: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); const FloatingContactBrandSchema = new Schema( { - imageSrc: { type: String, default: "" }, + imageSrc: { type: String, default: "", maxlength: 255 }, imageAlt: { type: String, default: "", maxlength: 60 }, }, { _id: false }, @@ -21,8 +21,8 @@ const FloatingContactBrandSchema = new Schema( const FloatingContactTriggerSchema = new Schema( { - imageSrc: { type: String, default: "" }, - icon: { type: String, default: "fa-comments" }, + imageSrc: { type: String, default: "", maxlength: 255 }, + icon: { type: String, default: "fa-comments", maxlength: 64 }, }, { _id: false }, ); @@ -34,10 +34,10 @@ const FloatingContactActionSchema = new Schema( enabled: { type: Boolean, default: true }, label: { type: String, default: "", maxlength: 48 }, subtitle: { type: String, default: "", maxlength: 48 }, - href: { type: String, default: "" }, - iconImage: { type: String, default: "" }, + href: { type: String, default: "", maxlength: 255 }, + iconImage: { type: String, default: "", maxlength: 255 }, iconType: { type: String, default: "iconClass" }, - iconClass: { type: String, default: "" }, + iconClass: { type: String, default: "", maxlength: 120 }, iconText: { type: String, default: "", maxlength: 12 }, order: { type: Number, default: 0 }, }, @@ -59,13 +59,13 @@ const FloatingContactSchema = 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: "" }, + title: { type: String, default: "", maxlength: 72 }, + subtitle: { type: String, default: "", maxlength: 48 }, + description: { type: String, default: "", maxlength: 220 }, primaryButton: { type: LinkSchema, default: () => ({}) }, secondaryButton: { type: LinkSchema, default: () => ({}) }, - heroImage: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, + heroImage: { type: String, default: "", maxlength: 255 }, + videoUrl: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); @@ -73,42 +73,42 @@ const HeroSlideSchema = new Schema( const HeroSchema = new Schema( { // Background for whole hero section - backgroundImage: { type: String, default: "" }, + backgroundImage: { type: String, default: "", maxlength: 255 }, // Multiple slides slides: { type: [HeroSlideSchema], default: [] }, // Legacy single-slide fields (backward compatible) - title: { type: String, default: "" }, - subtitle: { type: String, default: "" }, - description: { type: String, default: "" }, + title: { type: String, default: "", maxlength: 72 }, + subtitle: { type: String, default: "", maxlength: 48 }, + description: { type: String, default: "", maxlength: 220 }, primaryButton: { type: LinkSchema, default: () => ({}) }, secondaryButton: { type: LinkSchema, default: () => ({}) }, - heroImage: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, + heroImage: { type: String, default: "", maxlength: 255 }, + videoUrl: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); const WhyChooseUsItemSchema = new Schema( { - icon: { type: String, default: "" }, - title: { type: String, default: "" }, - description: { type: String, default: "" }, + icon: { type: String, default: "", maxlength: 255 }, + title: { type: String, default: "", maxlength: 40 }, + description: { type: String, default: "", maxlength: 72 }, }, { _id: false }, ); 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: "" }, + heading: { type: String, default: "", maxlength: 72 }, + subheading: { type: String, default: "", maxlength: 48 }, + description: { type: String, default: "", maxlength: 260 }, + highlightWord: { type: String, default: "", maxlength: 24 }, + mainImage: { type: String, default: "", maxlength: 255 }, + secondaryImage: { type: String, default: "", maxlength: 255 }, items: { type: [WhyChooseUsItemSchema], default: [] }, - features: { type: [String], default: [] }, + features: { type: [{ type: String, maxlength: 96 }], default: [] }, ctaButton: { type: LinkSchema, default: () => ({}) }, }, { _id: false }, @@ -116,18 +116,18 @@ const WhyChooseUsSchema = new Schema( const VisaSolutionItemSchema = new Schema( { - number: { type: String, default: "" }, - title: { type: String, default: "" }, - description: { type: String, default: "" }, - link: { type: String, default: "" }, + number: { type: String, default: "", maxlength: 4 }, + title: { type: String, default: "", maxlength: 56 }, + description: { type: String, default: "", maxlength: 180 }, + link: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); const VisaSolutionsSchema = new Schema( { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, + heading: { type: String, default: "", maxlength: 64 }, + subheading: { type: String, default: "", maxlength: 40 }, items: { type: [VisaSolutionItemSchema], default: [] }, }, { _id: false }, @@ -135,20 +135,20 @@ const VisaSolutionsSchema = new Schema( const VisaCountrySchema = new Schema( { - name: { type: String, default: "" }, - code: { type: String, default: "" }, - flag: { type: String, default: "" }, - link: { type: String, default: "" }, - visaTypes: { type: [String], default: [] }, + name: { type: String, default: "", maxlength: 40 }, + code: { type: String, default: "", maxlength: 12 }, + flag: { type: String, default: "", maxlength: 255 }, + link: { type: String, default: "", maxlength: 255 }, + visaTypes: { type: [{ type: String, maxlength: 48 }], default: [] }, }, { _id: false }, ); const VisaCountriesSchema = new Schema( { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - description: { type: String, default: "" }, + heading: { type: String, default: "", maxlength: 88 }, + subheading: { type: String, default: "", maxlength: 56 }, + description: { type: String, default: "", maxlength: 240 }, countries: { type: [VisaCountrySchema], default: [] }, ctaButton: { type: LinkSchema, default: () => ({}) }, }, @@ -157,22 +157,22 @@ const VisaCountriesSchema = new Schema( const TestimonialSchema = new Schema( { - name: { type: String, default: "" }, - role: { type: String, default: "" }, - country: { type: String, default: "" }, + name: { type: String, default: "", maxlength: 48 }, + role: { type: String, default: "", maxlength: 48 }, + country: { type: String, default: "", maxlength: 48 }, rating: { type: Number, default: 5 }, - comment: { type: String, default: "" }, - avatar: { type: String, default: "" }, + comment: { type: String, default: "", maxlength: 280 }, + avatar: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); const TestimonialsSchema = new Schema( { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, - videoThumbnail: { type: String, default: "" }, + heading: { type: String, default: "", maxlength: 64 }, + subheading: { type: String, default: "", maxlength: 40 }, + videoUrl: { type: String, default: "", maxlength: 255 }, + videoThumbnail: { type: String, default: "", maxlength: 255 }, items: { type: [TestimonialSchema], default: [] }, }, { _id: false }, @@ -180,26 +180,40 @@ const TestimonialsSchema = new Schema( const VideoGallerySchema = new Schema( { - heading: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, - thumbnail: { type: String, default: "" }, + heading: { + type: String, + default: "", + maxlength: 32, + validate: { + validator(value) { + const words = String(value || "") + .trim() + .split(/\s+/) + .filter(Boolean); + return words.length <= 4; + }, + message: "Video gallery heading must be 4 words or fewer.", + }, + }, + videoUrl: { type: String, default: "", maxlength: 255 }, + thumbnail: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); const FaqItemSchema = new Schema( { - question: { type: String, default: "" }, - answer: { type: String, default: "" }, + question: { type: String, default: "", maxlength: 120 }, + answer: { type: String, default: "", maxlength: 320 }, }, { _id: false }, ); const FaqSchema = new Schema( { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - description: { type: String, default: "" }, + heading: { type: String, default: "", maxlength: 64 }, + subheading: { type: String, default: "", maxlength: 40 }, + description: { type: String, default: "", maxlength: 220 }, ctaButton: { type: LinkSchema, default: () => ({}) }, items: { type: [FaqItemSchema], default: [] }, }, @@ -208,18 +222,18 @@ const FaqSchema = new Schema( const AchievementItemSchema = new Schema( { - value: { type: String, default: "" }, - suffix: { type: String, default: "" }, - label: { type: String, default: "" }, - description: { type: String, default: "" }, + value: { type: String, default: "", maxlength: 6 }, + suffix: { type: String, default: "", maxlength: 4 }, + label: { type: String, default: "", maxlength: 40 }, + description: { type: String, default: "", maxlength: 120 }, }, { _id: false }, ); const AchievementsSchema = new Schema( { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, + heading: { type: String, default: "", maxlength: 56 }, + subheading: { type: String, default: "", maxlength: 32 }, items: { type: [AchievementItemSchema], default: [] }, }, { _id: false }, @@ -227,9 +241,9 @@ const AchievementsSchema = new Schema( const VisaConsultancyItemSchema = new Schema( { - name: { type: String, default: "" }, - icon: { type: String, default: "" }, - year: { type: String, default: "" }, + name: { type: String, default: "", maxlength: 48 }, + icon: { type: String, default: "", maxlength: 255 }, + year: { type: String, default: "", maxlength: 8 }, }, { _id: false }, ); @@ -243,7 +257,7 @@ const VisaConsultancySchema = new Schema( const BrandItemSchema = new Schema( { - logo: { type: String, default: "" }, + logo: { type: String, default: "", maxlength: 255 }, }, { _id: false }, ); @@ -265,16 +279,16 @@ const PartnersSchema = new Schema( const BlogPreviewItemSchema = new Schema( { - title: { type: String, default: "" }, - excerpt: { type: String, default: "" }, - category: { type: String, default: "" }, + title: { type: String, default: "", maxlength: 120 }, + excerpt: { type: String, default: "", maxlength: 280 }, + category: { type: String, default: "", maxlength: 48 }, date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20") author: { - name: { type: String, default: "" }, + name: { type: String, default: "", maxlength: 48 }, avatar: { type: String, default: "" }, }, comments: { type: Number, default: 0 }, - link: { type: String, default: "" }, + link: { type: String, default: "", maxlength: 255 }, thumbnail: { type: String, default: "" }, }, { _id: false }, @@ -282,8 +296,8 @@ const BlogPreviewItemSchema = new Schema( const BlogPreviewSchema = new Schema( { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, + heading: { type: String, default: "", maxlength: 64 }, + subheading: { type: String, default: "", maxlength: 40 }, ctaButton: { type: LinkSchema, default: () => ({}) }, items: { type: [BlogPreviewItemSchema], default: [] }, selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }], diff --git a/views/admin/contact/index.ejs b/views/admin/contact/index.ejs index ba96344..6dce90c 100644 --- a/views/admin/contact/index.ejs +++ b/views/admin/contact/index.ejs @@ -7,7 +7,7 @@

Edit content displayed on Contact Us page

- + View Contact Us Page
@@ -66,7 +66,8 @@
+ value="<%= data.hero?.backgroundImage || '' %>" + maxlength="255" data-maxlength="255">
- Recommended size: 1920x1080px + The contact hero currently renders at about 1496x544px on desktop. Recommended minimum upload: 1920x700px.
@@ -106,7 +107,9 @@
+ value="<%= data.hero?.title || '' %>" + maxlength="40" data-maxlength="40"> + Keep the hero title short so the centered breadcrumb stays balanced on tablet and mobile.
@@ -176,7 +179,9 @@ + value="<%= card.title || '' %>" + maxlength="40" data-maxlength="40"> + Recommended maximum: 40 characters.
@@ -324,7 +329,8 @@ class="form-control card-icon-image-input" name="cardIconImage_<%= index %>" value="<%= imageIconValue %>" - placeholder="/uploads/icon.png"> + placeholder="/uploads/icon.png" + maxlength="255" data-maxlength="255">
@@ -357,9 +361,8 @@ line) - Enter each content - item on a new line + rows="3" maxlength="220" data-maxlength="220"><%= (card.content || []).join('\n') %> + Each line is shown inside a compact contact card. Keep it to 1-3 short lines.
- Upload a custom icon image for this contact card + Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.
- - Enter each content item on a new line + + Each line is shown inside a compact contact card. Keep it to 1-3 short lines.
+ Brand logo trong slider hiện hiển thị khoảng 159x48px. Khuyến nghị dùng SVG hoặc logo ngang nền trong suốt tối thiểu 320x96px.
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
@@ -74,12 +77,13 @@
- +
+ Khuyến nghị SVG hoặc ảnh ngang 320x96px để logo không bị mờ trong slider.
@@ -134,11 +138,12 @@
- +
+ Khuyến nghị SVG hoặc ảnh ngang 320x96px để logo không bị mờ trong slider.
@@ -146,5 +151,8 @@
`; container.appendChild(div); + if (typeof initHomeCharacterCounters === "function") { + initHomeCharacterCounters(div); + } } - \ No newline at end of file + diff --git a/views/admin/home/sections/testimonials.ejs b/views/admin/home/sections/testimonials.ejs index 650af05..7bf9ab4 100644 --- a/views/admin/home/sections/testimonials.ejs +++ b/views/admin/home/sections/testimonials.ejs @@ -14,23 +14,27 @@
+ value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials" + maxlength="64" data-maxlength="64" />
+ value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say" + maxlength="40" data-maxlength="40" />
+ value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." + maxlength="255" data-maxlength="255" />
+ Khung thumbnail desktop hiện khoảng 416x370px. Khuyến nghị upload tối thiểu 832x740px.
+ value="<%= data.testimonials?.videoThumbnail || '' %>" maxlength="255" data-maxlength="255" />
@@ -52,18 +54,20 @@
+ placeholder="e.g., United Kingdom" maxlength="40" data-maxlength="40" />
+ placeholder="e.g., UK" maxlength="12" data-maxlength="12" />
+ Khung ảnh desktop hiện khoảng 840x830px. Khuyến nghị ảnh gần vuông, tối thiểu 1000x1000px.
+ value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png" + maxlength="255" data-maxlength="255" />
@@ -100,12 +104,14 @@
+ value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" + maxlength="32" data-maxlength="32" />
+ value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact" + maxlength="255" data-maxlength="255" />
@@ -142,4 +148,4 @@ } }; }; - \ No newline at end of file + diff --git a/views/admin/home/sections/visaSolutions.ejs b/views/admin/home/sections/visaSolutions.ejs index 1fefdcb..aa6d3cb 100644 --- a/views/admin/home/sections/visaSolutions.ejs +++ b/views/admin/home/sections/visaSolutions.ejs @@ -14,12 +14,14 @@
+ value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions" + maxlength="64" data-maxlength="64" />
+ value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services" + maxlength="40" data-maxlength="40" />
@@ -53,22 +55,25 @@
+ value="<%= item.number || '' %>" placeholder="e.g., 01" + maxlength="4" data-maxlength="4" />
+ value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance" + maxlength="56" data-maxlength="56" />
+ placeholder="Enter description" maxlength="180" data-maxlength="180"><%= item.description || '' %>
+ value="<%= item.link || '' %>" placeholder="/service-details" + maxlength="255" data-maxlength="255" />
@@ -134,4 +139,4 @@ updateLabels(); }); - \ No newline at end of file + diff --git a/views/admin/home/sections/whyChooseUs.ejs b/views/admin/home/sections/whyChooseUs.ejs index fd98ef3..af13e5b 100644 --- a/views/admin/home/sections/whyChooseUs.ejs +++ b/views/admin/home/sections/whyChooseUs.ejs @@ -15,23 +15,26 @@ + placeholder="e.g., Turning Study Abroad Dreams Into Reality" + maxlength="72" data-maxlength="72" />
+ value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy" + maxlength="48" data-maxlength="48" />
+ value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams" + maxlength="24" data-maxlength="24" /> This word in the heading will be wrapped in a colored span.
+ placeholder="Enter description" maxlength="260" data-maxlength="260"><%= data.whyChooseUs?.description || '' %>
@@ -50,10 +53,11 @@
- Recommended size: 375x419px + Khung desktop hiện khoảng 318x347px. Khuyến nghị upload ít nhất 750x820px, tỉ lệ dọc khoảng 0.91:1.
+ value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg" + maxlength="255" data-maxlength="255" />
@@ -148,7 +156,7 @@
+ placeholder="Enter feature" maxlength="96" data-maxlength="96" />
<% }); %>
@@ -168,12 +176,14 @@
+ value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" + maxlength="32" data-maxlength="32" />
+ value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about" + maxlength="255" data-maxlength="255" />
@@ -230,4 +240,4 @@ }, }; }; - \ No newline at end of file +