feat: Improve home and contact CMS field guidance

This commit is contained in:
Tống Thành Đạt
2026-04-10 01:38:30 +07:00
parent ed09c7fa89
commit 7ce5921fe0
15 changed files with 529 additions and 230 deletions

View File

@@ -7,11 +7,13 @@ const heroSchema = new mongoose.Schema(
type: String, type: String,
required: true, required: true,
trim: true, trim: true,
maxlength: 40,
}, },
backgroundImage: { backgroundImage: {
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 255,
}, },
overlayColor: { overlayColor: {
type: String, type: String,
@@ -62,9 +64,10 @@ const contactCardSchema = new mongoose.Schema(
type: String, type: String,
required: true, required: true,
trim: true, trim: true,
maxlength: 40,
}, },
content: { content: {
type: [String], type: [{ type: String, maxlength: 96 }],
default: [], default: [],
}, },
iconType: { iconType: {
@@ -72,6 +75,7 @@ const contactCardSchema = new mongoose.Schema(
required: false, required: false,
trim: true, trim: true,
default: "", default: "",
maxlength: 255,
}, },
iconSource: { iconSource: {
type: String, type: String,
@@ -139,16 +143,19 @@ const mapSchema = new mongoose.Schema(
type: String, type: String,
required: true, required: true,
trim: true, trim: true,
maxlength: 120,
}, },
markerTitle: { markerTitle: {
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 48,
}, },
embedUrl: { embedUrl: {
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 1000,
}, },
tileLayer: { tileLayer: {
type: tileLayerSchema, type: tileLayerSchema,
@@ -165,11 +172,13 @@ const formFieldSchema = new mongoose.Schema(
type: String, type: String,
required: true, required: true,
trim: true, trim: true,
maxlength: 32,
}, },
label: { label: {
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 32,
}, },
type: { type: {
type: String, type: String,
@@ -181,6 +190,7 @@ const formFieldSchema = new mongoose.Schema(
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 72,
}, },
required: { required: {
type: Boolean, type: Boolean,
@@ -195,6 +205,7 @@ const formFieldSchema = new mongoose.Schema(
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 48,
}, },
}, },
{ _id: false } { _id: false }
@@ -207,6 +218,7 @@ const submitButtonSchema = new mongoose.Schema(
type: String, type: String,
required: true, required: true,
trim: true, trim: true,
maxlength: 24,
}, },
icon: { icon: {
type: String, type: String,
@@ -229,16 +241,19 @@ const formSchema = new mongoose.Schema(
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 32,
}, },
heading: { heading: {
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 48,
}, },
description: { description: {
type: String, type: String,
trim: true, trim: true,
default: "", default: "",
maxlength: 160,
}, },
fields: { fields: {
type: [formFieldSchema], type: [formFieldSchema],

View File

@@ -5,15 +5,15 @@ const { Schema } = mongoose;
// Reusable small schemas // Reusable small schemas
const LinkSchema = new Schema( const LinkSchema = new Schema(
{ {
label: { type: String, default: "" }, label: { type: String, default: "", maxlength: 32 },
href: { type: String, default: "" }, href: { type: String, default: "", maxlength: 255 },
}, },
{ _id: false }, { _id: false },
); );
const FloatingContactBrandSchema = new Schema( const FloatingContactBrandSchema = new Schema(
{ {
imageSrc: { type: String, default: "" }, imageSrc: { type: String, default: "", maxlength: 255 },
imageAlt: { type: String, default: "", maxlength: 60 }, imageAlt: { type: String, default: "", maxlength: 60 },
}, },
{ _id: false }, { _id: false },
@@ -21,8 +21,8 @@ const FloatingContactBrandSchema = new Schema(
const FloatingContactTriggerSchema = new Schema( const FloatingContactTriggerSchema = new Schema(
{ {
imageSrc: { type: String, default: "" }, imageSrc: { type: String, default: "", maxlength: 255 },
icon: { type: String, default: "fa-comments" }, icon: { type: String, default: "fa-comments", maxlength: 64 },
}, },
{ _id: false }, { _id: false },
); );
@@ -34,10 +34,10 @@ const FloatingContactActionSchema = new Schema(
enabled: { type: Boolean, default: true }, enabled: { type: Boolean, default: true },
label: { type: String, default: "", maxlength: 48 }, label: { type: String, default: "", maxlength: 48 },
subtitle: { type: String, default: "", maxlength: 48 }, subtitle: { type: String, default: "", maxlength: 48 },
href: { type: String, default: "" }, href: { type: String, default: "", maxlength: 255 },
iconImage: { type: String, default: "" }, iconImage: { type: String, default: "", maxlength: 255 },
iconType: { type: String, default: "iconClass" }, iconType: { type: String, default: "iconClass" },
iconClass: { type: String, default: "" }, iconClass: { type: String, default: "", maxlength: 120 },
iconText: { type: String, default: "", maxlength: 12 }, iconText: { type: String, default: "", maxlength: 12 },
order: { type: Number, default: 0 }, order: { type: Number, default: 0 },
}, },
@@ -59,13 +59,13 @@ const FloatingContactSchema = new Schema(
// Hero slide (for multiple hero items in slider) // Hero slide (for multiple hero items in slider)
const HeroSlideSchema = new Schema( const HeroSlideSchema = new Schema(
{ {
title: { type: String, default: "" }, title: { type: String, default: "", maxlength: 72 },
subtitle: { type: String, default: "" }, subtitle: { type: String, default: "", maxlength: 48 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 220 },
primaryButton: { type: LinkSchema, default: () => ({}) }, primaryButton: { type: LinkSchema, default: () => ({}) },
secondaryButton: { type: LinkSchema, default: () => ({}) }, secondaryButton: { type: LinkSchema, default: () => ({}) },
heroImage: { type: String, default: "" }, heroImage: { type: String, default: "", maxlength: 255 },
videoUrl: { type: String, default: "" }, videoUrl: { type: String, default: "", maxlength: 255 },
}, },
{ _id: false }, { _id: false },
); );
@@ -73,42 +73,42 @@ const HeroSlideSchema = new Schema(
const HeroSchema = new Schema( const HeroSchema = new Schema(
{ {
// Background for whole hero section // Background for whole hero section
backgroundImage: { type: String, default: "" }, backgroundImage: { type: String, default: "", maxlength: 255 },
// Multiple slides // Multiple slides
slides: { type: [HeroSlideSchema], default: [] }, slides: { type: [HeroSlideSchema], default: [] },
// Legacy single-slide fields (backward compatible) // Legacy single-slide fields (backward compatible)
title: { type: String, default: "" }, title: { type: String, default: "", maxlength: 72 },
subtitle: { type: String, default: "" }, subtitle: { type: String, default: "", maxlength: 48 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 220 },
primaryButton: { type: LinkSchema, default: () => ({}) }, primaryButton: { type: LinkSchema, default: () => ({}) },
secondaryButton: { type: LinkSchema, default: () => ({}) }, secondaryButton: { type: LinkSchema, default: () => ({}) },
heroImage: { type: String, default: "" }, heroImage: { type: String, default: "", maxlength: 255 },
videoUrl: { type: String, default: "" }, videoUrl: { type: String, default: "", maxlength: 255 },
}, },
{ _id: false }, { _id: false },
); );
const WhyChooseUsItemSchema = new Schema( const WhyChooseUsItemSchema = new Schema(
{ {
icon: { type: String, default: "" }, icon: { type: String, default: "", maxlength: 255 },
title: { type: String, default: "" }, title: { type: String, default: "", maxlength: 40 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 72 },
}, },
{ _id: false }, { _id: false },
); );
const WhyChooseUsSchema = new Schema( const WhyChooseUsSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 72 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 48 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 260 },
highlightWord: { type: String, default: "" }, highlightWord: { type: String, default: "", maxlength: 24 },
mainImage: { type: String, default: "" }, mainImage: { type: String, default: "", maxlength: 255 },
secondaryImage: { type: String, default: "" }, secondaryImage: { type: String, default: "", maxlength: 255 },
items: { type: [WhyChooseUsItemSchema], default: [] }, items: { type: [WhyChooseUsItemSchema], default: [] },
features: { type: [String], default: [] }, features: { type: [{ type: String, maxlength: 96 }], default: [] },
ctaButton: { type: LinkSchema, default: () => ({}) }, ctaButton: { type: LinkSchema, default: () => ({}) },
}, },
{ _id: false }, { _id: false },
@@ -116,18 +116,18 @@ const WhyChooseUsSchema = new Schema(
const VisaSolutionItemSchema = new Schema( const VisaSolutionItemSchema = new Schema(
{ {
number: { type: String, default: "" }, number: { type: String, default: "", maxlength: 4 },
title: { type: String, default: "" }, title: { type: String, default: "", maxlength: 56 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 180 },
link: { type: String, default: "" }, link: { type: String, default: "", maxlength: 255 },
}, },
{ _id: false }, { _id: false },
); );
const VisaSolutionsSchema = new Schema( const VisaSolutionsSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 64 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 40 },
items: { type: [VisaSolutionItemSchema], default: [] }, items: { type: [VisaSolutionItemSchema], default: [] },
}, },
{ _id: false }, { _id: false },
@@ -135,20 +135,20 @@ const VisaSolutionsSchema = new Schema(
const VisaCountrySchema = new Schema( const VisaCountrySchema = new Schema(
{ {
name: { type: String, default: "" }, name: { type: String, default: "", maxlength: 40 },
code: { type: String, default: "" }, code: { type: String, default: "", maxlength: 12 },
flag: { type: String, default: "" }, flag: { type: String, default: "", maxlength: 255 },
link: { type: String, default: "" }, link: { type: String, default: "", maxlength: 255 },
visaTypes: { type: [String], default: [] }, visaTypes: { type: [{ type: String, maxlength: 48 }], default: [] },
}, },
{ _id: false }, { _id: false },
); );
const VisaCountriesSchema = new Schema( const VisaCountriesSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 88 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 56 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 240 },
countries: { type: [VisaCountrySchema], default: [] }, countries: { type: [VisaCountrySchema], default: [] },
ctaButton: { type: LinkSchema, default: () => ({}) }, ctaButton: { type: LinkSchema, default: () => ({}) },
}, },
@@ -157,22 +157,22 @@ const VisaCountriesSchema = new Schema(
const TestimonialSchema = new Schema( const TestimonialSchema = new Schema(
{ {
name: { type: String, default: "" }, name: { type: String, default: "", maxlength: 48 },
role: { type: String, default: "" }, role: { type: String, default: "", maxlength: 48 },
country: { type: String, default: "" }, country: { type: String, default: "", maxlength: 48 },
rating: { type: Number, default: 5 }, rating: { type: Number, default: 5 },
comment: { type: String, default: "" }, comment: { type: String, default: "", maxlength: 280 },
avatar: { type: String, default: "" }, avatar: { type: String, default: "", maxlength: 255 },
}, },
{ _id: false }, { _id: false },
); );
const TestimonialsSchema = new Schema( const TestimonialsSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 64 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 40 },
videoUrl: { type: String, default: "" }, videoUrl: { type: String, default: "", maxlength: 255 },
videoThumbnail: { type: String, default: "" }, videoThumbnail: { type: String, default: "", maxlength: 255 },
items: { type: [TestimonialSchema], default: [] }, items: { type: [TestimonialSchema], default: [] },
}, },
{ _id: false }, { _id: false },
@@ -180,26 +180,40 @@ const TestimonialsSchema = new Schema(
const VideoGallerySchema = new Schema( const VideoGallerySchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: {
videoUrl: { type: String, default: "" }, type: String,
thumbnail: { type: String, default: "" }, 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 }, { _id: false },
); );
const FaqItemSchema = new Schema( const FaqItemSchema = new Schema(
{ {
question: { type: String, default: "" }, question: { type: String, default: "", maxlength: 120 },
answer: { type: String, default: "" }, answer: { type: String, default: "", maxlength: 320 },
}, },
{ _id: false }, { _id: false },
); );
const FaqSchema = new Schema( const FaqSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 64 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 40 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 220 },
ctaButton: { type: LinkSchema, default: () => ({}) }, ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [FaqItemSchema], default: [] }, items: { type: [FaqItemSchema], default: [] },
}, },
@@ -208,18 +222,18 @@ const FaqSchema = new Schema(
const AchievementItemSchema = new Schema( const AchievementItemSchema = new Schema(
{ {
value: { type: String, default: "" }, value: { type: String, default: "", maxlength: 6 },
suffix: { type: String, default: "" }, suffix: { type: String, default: "", maxlength: 4 },
label: { type: String, default: "" }, label: { type: String, default: "", maxlength: 40 },
description: { type: String, default: "" }, description: { type: String, default: "", maxlength: 120 },
}, },
{ _id: false }, { _id: false },
); );
const AchievementsSchema = new Schema( const AchievementsSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 56 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 32 },
items: { type: [AchievementItemSchema], default: [] }, items: { type: [AchievementItemSchema], default: [] },
}, },
{ _id: false }, { _id: false },
@@ -227,9 +241,9 @@ const AchievementsSchema = new Schema(
const VisaConsultancyItemSchema = new Schema( const VisaConsultancyItemSchema = new Schema(
{ {
name: { type: String, default: "" }, name: { type: String, default: "", maxlength: 48 },
icon: { type: String, default: "" }, icon: { type: String, default: "", maxlength: 255 },
year: { type: String, default: "" }, year: { type: String, default: "", maxlength: 8 },
}, },
{ _id: false }, { _id: false },
); );
@@ -243,7 +257,7 @@ const VisaConsultancySchema = new Schema(
const BrandItemSchema = new Schema( const BrandItemSchema = new Schema(
{ {
logo: { type: String, default: "" }, logo: { type: String, default: "", maxlength: 255 },
}, },
{ _id: false }, { _id: false },
); );
@@ -265,16 +279,16 @@ const PartnersSchema = new Schema(
const BlogPreviewItemSchema = new Schema( const BlogPreviewItemSchema = new Schema(
{ {
title: { type: String, default: "" }, title: { type: String, default: "", maxlength: 120 },
excerpt: { type: String, default: "" }, excerpt: { type: String, default: "", maxlength: 280 },
category: { type: String, default: "" }, category: { type: String, default: "", maxlength: 48 },
date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20") date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20")
author: { author: {
name: { type: String, default: "" }, name: { type: String, default: "", maxlength: 48 },
avatar: { type: String, default: "" }, avatar: { type: String, default: "" },
}, },
comments: { type: Number, default: 0 }, comments: { type: Number, default: 0 },
link: { type: String, default: "" }, link: { type: String, default: "", maxlength: 255 },
thumbnail: { type: String, default: "" }, thumbnail: { type: String, default: "" },
}, },
{ _id: false }, { _id: false },
@@ -282,8 +296,8 @@ const BlogPreviewItemSchema = new Schema(
const BlogPreviewSchema = new Schema( const BlogPreviewSchema = new Schema(
{ {
heading: { type: String, default: "" }, heading: { type: String, default: "", maxlength: 64 },
subheading: { type: String, default: "" }, subheading: { type: String, default: "", maxlength: 40 },
ctaButton: { type: LinkSchema, default: () => ({}) }, ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [BlogPreviewItemSchema], default: [] }, items: { type: [BlogPreviewItemSchema], default: [] },
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }], selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],

View File

@@ -7,7 +7,7 @@
<p class="text-muted mb-0">Edit content displayed on Contact Us page</p> <p class="text-muted mb-0">Edit content displayed on Contact Us page</p>
</div> </div>
<div> <div>
<a href="<%= frontendUrl %>/contact-us/" class="btn btn-outline-primary" target="_blank"> <a href="<%= frontendUrl %>/contact" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Contact Us Page <i class="fas fa-external-link-alt me-2"></i>View Contact Us Page
</a> </a>
</div> </div>
@@ -66,7 +66,8 @@
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage" <input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage" name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>"> value="<%= data.hero?.backgroundImage || '' %>"
maxlength="255" data-maxlength="255">
<button type="button" <button type="button"
class="btn btn-outline-primary btn-upload-image" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-target-input="heroBackgroundImage"
@@ -74,7 +75,7 @@
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
</button> </button>
</div> </div>
<small class="text-muted">Recommended size: 1920x1080px</small> <small class="text-muted d-block mt-1">The contact hero currently renders at about 1496x544px on desktop. Recommended minimum upload: 1920x700px.</small>
</div> </div>
<div class="col-md-7"> <div class="col-md-7">
<div id="heroImagePreview" style="height: 300px;"> <div id="heroImagePreview" style="height: 300px;">
@@ -106,7 +107,9 @@
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Title</label> <label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle" <input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>"> value="<%= data.hero?.title || '' %>"
maxlength="40" data-maxlength="40">
<small class="text-muted d-block mt-1">Keep the hero title short so the centered breadcrumb stays balanced on tablet and mobile.</small>
</div> </div>
</div> </div>
<!-- Hidden field for overlayColor - keep default value --> <!-- Hidden field for overlayColor - keep default value -->
@@ -176,7 +179,9 @@
<label class="form-label">Title</label> <label class="form-label">Title</label>
<input type="text" class="form-control" <input type="text" class="form-control"
name="cardTitle_<%= index %>" name="cardTitle_<%= index %>"
value="<%= card.title || '' %>"> value="<%= card.title || '' %>"
maxlength="40" data-maxlength="40">
<small class="text-muted d-block mt-1">Recommended maximum: 40 characters.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label">Icon Type</label> <label class="form-label">Icon Type</label>
@@ -324,7 +329,8 @@
class="form-control card-icon-image-input" class="form-control card-icon-image-input"
name="cardIconImage_<%= index %>" name="cardIconImage_<%= index %>"
value="<%= imageIconValue %>" value="<%= imageIconValue %>"
placeholder="/uploads/icon.png"> placeholder="/uploads/icon.png"
maxlength="255" data-maxlength="255">
<button type="button" <button type="button"
class="btn btn-outline-primary btn-upload-image" class="btn btn-outline-primary btn-upload-image"
data-target-input="cardIconImage_<%= index %>" data-target-input="cardIconImage_<%= index %>"
@@ -347,9 +353,7 @@
style="max-height: 100px; width: auto; display: none;" style="max-height: 100px; width: auto; display: none;"
alt="Icon preview"> alt="Icon preview">
<% } %> <% } %>
<small class="text-muted">Upload <small class="text-muted d-block mt-1">Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</small>
a custom icon image for this
contact card</small>
</div> </div>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
@@ -357,9 +361,8 @@
line)</label> line)</label>
<textarea class="form-control" <textarea class="form-control"
name="cardContent_<%= index %>" name="cardContent_<%= index %>"
rows="3"><%= (card.content || []).join('\n') %></textarea> rows="3" maxlength="220" data-maxlength="220"><%= (card.content || []).join('\n') %></textarea>
<small class="text-muted">Enter each content <small class="text-muted d-block mt-1">Each line is shown inside a compact contact card. Keep it to 1-3 short lines.</small>
item on a new line</small>
</div> </div>
</div> </div>
<button type="button" <button type="button"
@@ -386,23 +389,24 @@
<label class="form-label">Marker Title</label> <label class="form-label">Marker Title</label>
<input type="text" class="form-control" id="mapMarkerTitle" <input type="text" class="form-control" id="mapMarkerTitle"
value="<%= data.map?.markerTitle || '' %>" value="<%= data.map?.markerTitle || '' %>"
placeholder="e.g., Our Office"> placeholder="e.g., Our Office"
maxlength="48" data-maxlength="48">
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Location</label> <label class="form-label">Location</label>
<input type="text" class="form-control" id="mapLocation" <input type="text" class="form-control" id="mapLocation"
value="<%= data.map?.location || '' %>" value="<%= data.map?.location || '' %>"
placeholder="e.g., 123 Main St, City, Country"> placeholder="e.g., 123 Main St, City, Country"
<small class="text-muted">Enter address - map will be automatically maxlength="120" data-maxlength="120">
shown</small> <small class="text-muted d-block mt-1">Enter a full address. This text is used for map lookup and should stay concise.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label">Google Map Embed URL</label> <label class="form-label">Google Map Embed URL</label>
<input type="text" class="form-control" id="mapEmbedUrl" <input type="text" class="form-control" id="mapEmbedUrl"
value="<%= data.map?.embedUrl || '' %>" value="<%= data.map?.embedUrl || '' %>"
placeholder="https://www.google.com/maps/embed?..."> placeholder="https://www.google.com/maps/embed?..."
<small class="text-muted">Paste embed URL from Google Maps (Share -> maxlength="1000" data-maxlength="1000">
Embed a map)</small> <small class="text-muted d-block mt-1">Paste the Google Maps embed URL from Share -> Embed a map.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<div id="mapPreview" <div id="mapPreview"
@@ -468,22 +472,29 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Section Label</label> <label class="form-label">Section Label</label>
<input type="text" class="form-control" id="formSectionLabel" <input type="text" class="form-control" id="formSectionLabel"
value="<%= data.form?.sectionLabel || '' %>"> value="<%= data.form?.sectionLabel || '' %>"
maxlength="32" data-maxlength="32">
<small class="text-muted d-block mt-1">Legacy label. Keep it short if you still use it in future templates.</small>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Submit Button Text</label> <label class="form-label">Submit Button Text</label>
<input type="text" class="form-control" id="formSubmitButtonText" <input type="text" class="form-control" id="formSubmitButtonText"
value="<%= data.form?.submitButton?.text || 'Send Message' %>"> value="<%= data.form?.submitButton?.text || 'Send Message' %>"
maxlength="24" data-maxlength="24">
<small class="text-muted d-block mt-1">Recommended maximum: 24 characters.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label">Heading</label> <label class="form-label">Heading</label>
<input type="text" class="form-control" id="formHeading" <input type="text" class="form-control" id="formHeading"
value="<%= data.form?.heading || '' %>"> value="<%= data.form?.heading || '' %>"
maxlength="48" data-maxlength="48">
<small class="text-muted d-block mt-1">The form heading spans the full form width. Recommended maximum: 48 characters.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label">Description</label> <label class="form-label">Description</label>
<textarea class="form-control" id="formDescription" <textarea class="form-control" id="formDescription"
rows="2"><%= data.form?.description || '' %></textarea> rows="2" maxlength="160" data-maxlength="160"><%= data.form?.description || '' %></textarea>
<small class="text-muted d-block mt-1">This line is centered under the form heading. Recommended maximum: 160 characters.</small>
</div> </div>
<!-- Hidden fields for submitButton icon and buttonClass --> <!-- Hidden fields for submitButton icon and buttonClass -->
<input type="hidden" id="formSubmitButtonIcon" <input type="hidden" id="formSubmitButtonIcon"
@@ -510,7 +521,9 @@
<input type="text" class="form-control" <input type="text" class="form-control"
name="fieldName_<%= index %>" name="fieldName_<%= index %>"
value="<%= field.name || '' %>" value="<%= field.name || '' %>"
placeholder="e.g., Your Name"> placeholder="e.g., Your Name"
maxlength="32" data-maxlength="32">
<small class="text-muted d-block mt-1">Keep field labels short for the stacked mobile form.</small>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Field Type</label> <label class="form-label">Field Type</label>
@@ -536,7 +549,8 @@
<label class="form-label">Placeholder</label> <label class="form-label">Placeholder</label>
<input type="text" class="form-control" <input type="text" class="form-control"
name="fieldPlaceholder_<%= index %>" name="fieldPlaceholder_<%= index %>"
value="<%= field.placeholder || '' %>"> value="<%= field.placeholder || '' %>"
maxlength="72" data-maxlength="72">
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">Required</label> <label class="form-label">Required</label>
@@ -552,9 +566,9 @@
<input type="text" class="form-control" <input type="text" class="form-control"
name="fieldProgrammeName_<%= index %>" name="fieldProgrammeName_<%= index %>"
value="<%= field.programmeName || '' %>" value="<%= field.programmeName || '' %>"
placeholder="e.g., Summer Camp 2024"> placeholder="e.g., Summer Camp 2024"
<small class="text-muted">Internal name for the maxlength="48" data-maxlength="48">
programme</small> <small class="text-muted d-block mt-1">Internal programme reference only. Recommended maximum: 48 characters.</small>
</div> </div>
<!-- Hidden fields for label and colClass --> <!-- Hidden fields for label and colClass -->
<input type="hidden" name="fieldLabel_<%= index %>" <input type="hidden" name="fieldLabel_<%= index %>"
@@ -775,6 +789,7 @@
updateAllJsonInputs(originalFormData); updateAllJsonInputs(originalFormData);
initializeFormHandlers(); initializeFormHandlers();
initContactCharacterCounters(document);
}); });
function applyDateFilter() { function applyDateFilter() {
@@ -1195,6 +1210,65 @@
}); });
} }
function ensureContactCounter(input) {
if (!input || !input.dataset.maxlength) {
return null;
}
if (!input.id) {
input.id = `contactField_${Math.random().toString(36).slice(2, 10)}`;
}
const field = input.closest('.col-md-12, .col-md-7, .col-md-6, .col-md-5, .col-md-4, .col-md-3, .col-md-2, .col-12') || input.parentElement;
const anchor = input.closest('.input-group') || input;
const parent = anchor?.parentElement || field;
if (!field || !anchor || !parent) {
return null;
}
let hint = field.querySelector(`[data-counter-for="${input.id}"]`);
if (!hint) {
hint = document.createElement('small');
hint.className = 'form-text contact-limit-counter text-secondary';
hint.dataset.counterFor = input.id;
}
if (hint.previousElementSibling !== anchor) {
parent.insertBefore(hint, anchor.nextSibling);
}
return hint;
}
function updateContactCounter(input) {
const hint = ensureContactCounter(input);
if (!hint) {
return;
}
const max = Number(input.dataset.maxlength);
if (Number.isFinite(max) && max > 0 && (input.value || '').length > max) {
input.value = (input.value || '').slice(0, max);
}
const length = (input.value || '').length;
hint.textContent = `${length}/${max} characters`;
hint.classList.toggle('text-danger', length >= max);
}
function initContactCharacterCounters(scope = document) {
scope.querySelectorAll('input[data-maxlength], textarea[data-maxlength]').forEach((input) => {
updateContactCounter(input);
if (input.dataset.counterBound === 'true') {
return;
}
input.dataset.counterBound = 'true';
input.addEventListener('input', () => updateContactCounter(input));
});
}
function updateAllJsonInputs(data) { function updateAllJsonInputs(data) {
document.getElementById('heroJson').value = JSON.stringify(data.hero || {}); document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
document.getElementById('contactCardsJson').value = JSON.stringify(data.contactCards || []); document.getElementById('contactCardsJson').value = JSON.stringify(data.contactCards || []);
@@ -1252,7 +1326,8 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Title</label> <label class="form-label">Title</label>
<input type="text" class="form-control" name="cardTitle_${index}"> <input type="text" class="form-control" name="cardTitle_${index}" maxlength="40" data-maxlength="40">
<small class="text-muted d-block mt-1">Recommended maximum: 40 characters.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label">Icon Type</label> <label class="form-label">Icon Type</label>
@@ -1306,20 +1381,20 @@
<label class="form-label">Upload Icon Image</label> <label class="form-label">Upload Icon Image</label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control card-icon-image-input" name="cardIconImage_${index}" <input type="text" class="form-control card-icon-image-input" name="cardIconImage_${index}"
placeholder="/uploads/icon.png"> placeholder="/uploads/icon.png" maxlength="255" data-maxlength="255">
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="cardIconImage_${index}" data-image-type="contact"> data-target-input="cardIconImage_${index}" data-image-type="contact">
<i class="fas fa-upload me-1"></i>Upload Icon <i class="fas fa-upload me-1"></i>Upload Icon
</button> </button>
</div> </div>
<img src="" class="img-thumbnail mt-2 icon-image-preview" data-index="${index}" style="max-height: 100px; width: auto; display: none;" alt="Icon preview"> <img src="" class="img-thumbnail mt-2 icon-image-preview" data-index="${index}" style="max-height: 100px; width: auto; display: none;" alt="Icon preview">
<small class="text-muted">Upload a custom icon image for this contact card</small> <small class="text-muted d-block mt-1">Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</small>
</div> </div>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label">Content (one per line)</label> <label class="form-label">Content (one per line)</label>
<textarea class="form-control" name="cardContent_${index}" rows="3"></textarea> <textarea class="form-control" name="cardContent_${index}" rows="3" maxlength="220" data-maxlength="220"></textarea>
<small class="text-muted">Enter each content item on a new line</small> <small class="text-muted d-block mt-1">Each line is shown inside a compact contact card. Keep it to 1-3 short lines.</small>
</div> </div>
</div> </div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeContactCard(this)"> <button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeContactCard(this)">
@@ -1340,6 +1415,8 @@
openImageUploader(targetInput, imageType); openImageUploader(targetInput, imageType);
}); });
} }
initContactCharacterCounters(newCard);
} }
function handleIconSourceChange(radio) { function handleIconSourceChange(radio) {
@@ -1385,7 +1462,8 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Label</label> <label class="form-label">Label</label>
<input type="text" class="form-control" name="fieldName_${index}" placeholder="e.g., Your Name"> <input type="text" class="form-control" name="fieldName_${index}" placeholder="e.g., Your Name" maxlength="32" data-maxlength="32">
<small class="text-muted d-block mt-1">Keep field labels short for the stacked mobile form.</small>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Field Type</label> <label class="form-label">Field Type</label>
@@ -1399,7 +1477,7 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Placeholder</label> <label class="form-label">Placeholder</label>
<input type="text" class="form-control" name="fieldPlaceholder_${index}"> <input type="text" class="form-control" name="fieldPlaceholder_${index}" maxlength="72" data-maxlength="72">
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label">Required</label> <label class="form-label">Required</label>
@@ -1409,8 +1487,8 @@
</div> </div>
<div class="col-md-3 programme-name-field" style="display: none;"> <div class="col-md-3 programme-name-field" style="display: none;">
<label class="form-label">Programme Name</label> <label class="form-label">Programme Name</label>
<input type="text" class="form-control" name="fieldProgrammeName_${index}" placeholder="e.g., Summer Camp 2024"> <input type="text" class="form-control" name="fieldProgrammeName_${index}" placeholder="e.g., Summer Camp 2024" maxlength="48" data-maxlength="48">
<small class="text-muted">Internal name for the programme</small> <small class="text-muted d-block mt-1">Internal programme reference only. Recommended maximum: 48 characters.</small>
</div> </div>
<!-- Hidden fields for label and colClass --> <!-- Hidden fields for label and colClass -->
<input type="hidden" name="fieldLabel_${index}" value=""> <input type="hidden" name="fieldLabel_${index}" value="">
@@ -1423,6 +1501,7 @@
</div> </div>
`; `;
container.insertAdjacentHTML('beforeend', html); container.insertAdjacentHTML('beforeend', html);
initContactCharacterCounters(container.lastElementChild);
} }
function removeFormField(button) { function removeFormField(button) {

View File

@@ -189,6 +189,8 @@
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section) // Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
initImageUploads(); initImageUploads();
initHomeCharacterCounters(document);
initImagePreviewFallbacks(document);
}); });
// --- UTILITIES (Dùng chung) --- // --- UTILITIES (Dùng chung) ---
@@ -223,6 +225,43 @@
); );
} }
function clearImagePreviewError(previewImg) {
if (!previewImg?.parentElement) {
return;
}
previewImg.parentElement.querySelectorAll(".image-preview-missing").forEach((node) => node.remove());
}
function bindImagePreviewFallback(previewImg) {
if (!previewImg || previewImg.dataset.previewFallbackBound === "true") {
return;
}
previewImg.dataset.previewFallbackBound = "true";
previewImg.addEventListener("error", () => {
previewImg.classList.add("d-none");
previewImg.removeAttribute("src");
if (!previewImg.parentElement?.querySelector(".image-preview-missing")) {
const note = document.createElement("small");
note.className = "text-warning d-block mt-2 image-preview-missing";
note.textContent = "Preview unavailable: current image path could not be loaded.";
previewImg.parentElement?.appendChild(note);
}
});
}
function initImagePreviewFallbacks(scope = document) {
scope.querySelectorAll("img.img-thumbnail").forEach((previewImg) => {
bindImagePreviewFallback(previewImg);
if (previewImg.complete && previewImg.getAttribute("src") && previewImg.naturalWidth === 0) {
previewImg.dispatchEvent(new Event("error"));
}
});
}
function revokePendingPreview(targetInput) { function revokePendingPreview(targetInput) {
const previewUrl = pendingPreviewUrls.get(targetInput); const previewUrl = pendingPreviewUrls.get(targetInput);
if (previewUrl) { if (previewUrl) {
@@ -307,6 +346,8 @@
const previewImg = findImagePreview(input); const previewImg = findImagePreview(input);
if (previewImg) { if (previewImg) {
clearImagePreviewError(previewImg);
bindImagePreviewFallback(previewImg);
previewImg.src = new URL(result.path, window.location.origin).toString(); previewImg.src = new URL(result.path, window.location.origin).toString();
previewImg.classList.remove("d-none"); previewImg.classList.remove("d-none");
} }
@@ -364,6 +405,8 @@
const previewImg = findImagePreview(input); const previewImg = findImagePreview(input);
if (previewImg) { if (previewImg) {
clearImagePreviewError(previewImg);
bindImagePreviewFallback(previewImg);
previewImg.src = previewUrl; previewImg.src = previewUrl;
previewImg.classList.remove("d-none"); previewImg.classList.remove("d-none");
} }
@@ -402,4 +445,93 @@
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show(); new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
toast.addEventListener("hidden.bs.toast", () => toast.remove()); toast.addEventListener("hidden.bs.toast", () => toast.remove());
} }
function ensureCharacterHint(input) {
if (!input || (!input.dataset.maxlength && !input.dataset.maxwords)) {
return null;
}
if (!input.id) {
input.id = `homeField_${Math.random().toString(36).slice(2, 10)}`;
}
const field = input.closest(".col-md-12, .col-md-9, .col-md-6, .col-md-4, .col-md-3, .col-lg-12, .col-lg-8, .col-lg-6, .col-lg-4, .col-12") || input.parentElement;
const anchor = input.closest(".input-group") || input;
const parent = anchor?.parentElement || field;
if (!field || !anchor || !parent) {
return null;
}
let hint = field.querySelector(`[data-counter-for="${input.id}"]`);
if (!hint) {
hint = document.createElement("small");
hint.className = "form-text home-limit-counter text-secondary";
hint.dataset.counterFor = input.id;
}
if (hint.previousElementSibling !== anchor) {
parent.insertBefore(hint, anchor.nextSibling);
}
return hint;
}
function updateCharacterHint(input) {
const hint = ensureCharacterHint(input);
if (!hint) {
return;
}
const hasWordLimit = Boolean(input.dataset.maxwords);
const hasCharLimit = Boolean(input.dataset.maxlength);
if (hasWordLimit) {
const maxWords = Number(input.dataset.maxwords);
const normalized = (input.value || "").replace(/\s+/g, " ").trim();
const words = normalized ? normalized.split(" ") : [];
if (Number.isFinite(maxWords) && maxWords > 0 && words.length > maxWords) {
input.value = words.slice(0, maxWords).join(" ");
} else if (normalized !== input.value) {
input.value = normalized;
}
}
if (hasCharLimit) {
const max = Number(input.dataset.maxlength);
if (Number.isFinite(max) && max > 0 && (input.value || "").length > max) {
input.value = (input.value || "").slice(0, max);
}
}
if (hasWordLimit) {
const maxWords = Number(input.dataset.maxwords);
const currentWords = input.value ? input.value.split(" ").filter(Boolean).length : 0;
const currentLength = (input.value || "").length;
const maxLength = Number(input.dataset.maxlength);
hint.textContent = hasCharLimit
? `${currentWords}/${maxWords} words, ${currentLength}/${maxLength} characters`
: `${currentWords}/${maxWords} words`;
hint.classList.toggle("text-danger", currentWords >= maxWords || (hasCharLimit && currentLength >= maxLength));
return;
}
const max = Number(input.dataset.maxlength);
const length = (input.value || "").length;
hint.textContent = `${length}/${max} characters`;
hint.classList.toggle("text-danger", length >= max);
}
function initHomeCharacterCounters(scope = document) {
scope.querySelectorAll("input[data-maxlength], textarea[data-maxlength], input[data-maxwords], textarea[data-maxwords]").forEach((input) => {
updateCharacterHint(input);
if (input.dataset.counterBound === "true") {
return;
}
input.dataset.counterBound = "true";
input.addEventListener("input", () => updateCharacterHint(input));
});
}
</script> </script>

View File

@@ -19,6 +19,8 @@
id="achievementsHeading" id="achievementsHeading"
value="<%= data.achievements?.heading || '' %>" value="<%= data.achievements?.heading || '' %>"
placeholder="e.g., Our Achievements in Numbers" placeholder="e.g., Our Achievements in Numbers"
maxlength="56"
data-maxlength="56"
/> />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@@ -29,6 +31,8 @@
id="achievementsSubheading" id="achievementsSubheading"
value="<%= data.achievements?.subheading || '' %>" value="<%= data.achievements?.subheading || '' %>"
placeholder="e.g., Did You Know" placeholder="e.g., Did You Know"
maxlength="32"
data-maxlength="32"
/> />
</div> </div>
</div> </div>
@@ -59,8 +63,11 @@
<input <input
type="text" type="text"
class="form-control achievement-value" class="form-control achievement-value"
id="achievementValue_<%= i %>"
value="<%= item.value || '' %>" value="<%= item.value || '' %>"
placeholder="e.g., 95" placeholder="e.g., 95"
maxlength="6"
data-maxlength="6"
/> />
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@@ -68,8 +75,11 @@
<input <input
type="text" type="text"
class="form-control achievement-suffix" class="form-control achievement-suffix"
id="achievementSuffix_<%= i %>"
value="<%= item.suffix || '' %>" value="<%= item.suffix || '' %>"
placeholder="e.g., %" placeholder="e.g., %"
maxlength="4"
data-maxlength="4"
/> />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@@ -77,16 +87,22 @@
<input <input
type="text" type="text"
class="form-control achievement-label" class="form-control achievement-label"
id="achievementLabel_<%= i %>"
value="<%= item.label || '' %>" value="<%= item.label || '' %>"
placeholder="e.g., Visa Success Rate" placeholder="e.g., Visa Success Rate"
maxlength="40"
data-maxlength="40"
/> />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<textarea <textarea
class="form-control achievement-description" class="form-control achievement-description"
id="achievementDescription_<%= i %>"
rows="2" rows="2"
placeholder="Short description of this achievement" placeholder="Short description of this achievement"
maxlength="120"
data-maxlength="120"
><%= item.description || '' %></textarea> ><%= item.description || '' %></textarea>
</div> </div>
</div> </div>

View File

@@ -16,12 +16,14 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="blogPreviewHeading" <input type="text" class="form-control" id="blogPreviewHeading"
value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates" /> value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates"
maxlength="64" data-maxlength="64" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subheading</label> <label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="blogPreviewSubheading" <input type="text" class="form-control" id="blogPreviewSubheading"
value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides" /> value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides"
maxlength="40" data-maxlength="40" />
</div> </div>
<div class="col-md-12 mt-4"> <div class="col-md-12 mt-4">
@@ -88,12 +90,14 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Label</label> <label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="blogPreviewCtaLabel" <input type="text" class="form-control" id="blogPreviewCtaLabel"
value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles" /> value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles"
maxlength="32" data-maxlength="32" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="blogPreviewCtaHref" <input type="text" class="form-control" id="blogPreviewCtaHref"
value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog" /> value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog"
maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,17 +14,17 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="faqHeading" value="<%= data.faq?.heading || '' %>" <input type="text" class="form-control" id="faqHeading" value="<%= data.faq?.heading || '' %>"
placeholder="e.g., Got Questions? We've Got Answers" /> placeholder="e.g., Got Questions? We've Got Answers" maxlength="64" data-maxlength="64" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subheading</label> <label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="faqSubheading" value="<%= data.faq?.subheading || '' %>" <input type="text" class="form-control" id="faqSubheading" value="<%= data.faq?.subheading || '' %>"
placeholder="e.g., Visa FAQs" /> placeholder="e.g., Visa FAQs" maxlength="40" data-maxlength="40" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="faqDescription" rows="3" <textarea class="form-control" id="faqDescription" rows="3"
placeholder="Enter description"><%= data.faq?.description || '' %></textarea> placeholder="Enter description" maxlength="220" data-maxlength="220"><%= data.faq?.description || '' %></textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -69,6 +69,8 @@
class="form-control" class="form-control"
id="faqQuestion_<%= index %>" id="faqQuestion_<%= index %>"
value="<%= item.question || '' %>" value="<%= item.question || '' %>"
maxlength="120"
data-maxlength="120"
placeholder="Enter question" placeholder="Enter question"
/> />
</div> </div>
@@ -78,6 +80,8 @@
class="form-control" class="form-control"
id="faqAnswer_<%= index %>" id="faqAnswer_<%= index %>"
rows="3" rows="3"
maxlength="320"
data-maxlength="320"
placeholder="Enter answer" placeholder="Enter answer"
><%= item.answer || '' %></textarea> ><%= item.answer || '' %></textarea>
</div> </div>
@@ -102,12 +106,12 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Label</label> <label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="faqCtaLabel" value="<%= data.faq?.ctaButton?.label || '' %>" <input type="text" class="form-control" id="faqCtaLabel" value="<%= data.faq?.ctaButton?.label || '' %>"
placeholder="e.g., contact us" /> placeholder="e.g., contact us" maxlength="32" data-maxlength="32" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="faqCtaHref" value="<%= data.faq?.ctaButton?.href || '' %>" <input type="text" class="form-control" id="faqCtaHref" value="<%= data.faq?.ctaButton?.href || '' %>"
placeholder="/contact" /> placeholder="/contact" maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -48,7 +48,8 @@
<label class="form-label fw-medium">Brand Image</label> <label class="form-label fw-medium">Brand Image</label>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="floatingContactBrandImage" <input type="text" class="form-control" id="floatingContactBrandImage"
value="<%= data.floatingContact?.brand?.imageSrc || '' %>" placeholder="/assets/img/logo/black-logo.svg" /> value="<%= data.floatingContact?.brand?.imageSrc || '' %>" placeholder="/assets/img/logo/black-logo.svg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="floatingContactBrandImage" data-image-type="home/floating-contact" data-target-input="floatingContactBrandImage" data-image-type="home/floating-contact"
data-resize-preset="floatingContactBrandImage"> data-resize-preset="floatingContactBrandImage">
@@ -63,7 +64,7 @@
style="height: 120px; width: 120px; object-fit: contain; background: #fff;" style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
alt="Brand preview" /> alt="Brand preview" />
</div> </div>
<small class="text-muted d-block mt-2">Raster logo uploads are normalized to 104x104 WebP to match the homepage widget.</small> <small class="text-muted d-block mt-2">Hiển thị thực tế khoảng 35x35px. Raster uploads are normalized to 104x104 WebP to match the homepage widget.</small>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
<label class="form-label fw-medium">Fallback Trigger Image</label> <label class="form-label fw-medium">Fallback Trigger Image</label>
@@ -73,7 +74,8 @@
class="form-control" class="form-control"
id="floatingContactTriggerImage" id="floatingContactTriggerImage"
value="<%= data.floatingContact?.trigger?.imageSrc || '' %>" value="<%= data.floatingContact?.trigger?.imageSrc || '' %>"
placeholder="/uploads/home/floating-contact/floating-trigger-icon.webp" /> placeholder="/uploads/home/floating-contact/floating-trigger-icon.webp"
maxlength="255" data-maxlength="255" />
<button <button
type="button" type="button"
class="btn btn-outline-primary btn-upload-image" class="btn btn-outline-primary btn-upload-image"
@@ -93,7 +95,7 @@
</div> </div>
<input type="hidden" id="floatingContactTriggerIconFallback" <input type="hidden" id="floatingContactTriggerIconFallback"
value="<%= data.floatingContact?.trigger?.icon || 'fa-comments' %>" /> value="<%= data.floatingContact?.trigger?.icon || 'fa-comments' %>" />
<small class="text-muted d-block mt-2">Shown only when the trigger slideshow cannot use the brand image and action icons. Raster uploads are normalized to 96x96 WebP.</small> <small class="text-muted d-block mt-2">Hiển thị thực tế khoảng 26x26px. Shown only when the trigger slideshow cannot use the brand image and action icons. Raster uploads are normalized to 96x96 WebP.</small>
</div> </div>
</div> </div>
</div> </div>
@@ -322,12 +324,12 @@
<div class="col-lg-9 col-md-8"> <div class="col-lg-9 col-md-8">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control floating-contact-action-href" value="${escapeFloatingContactHtml(defaults.href)}" placeholder="https://example.com" /> <input type="text" class="form-control floating-contact-action-href" value="${escapeFloatingContactHtml(defaults.href)}" placeholder="https://example.com" maxlength="255" data-maxlength="255" />
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="form-label fw-medium">Icon Image</label> <label class="form-label fw-medium">Icon Image</label>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control floating-contact-action-icon-image" id="${escapeFloatingContactHtml(iconInputId)}" value="${escapeFloatingContactHtml(normalizedIconImage)}" placeholder="/uploads/home/floating-contact/floating-action-icon.webp" /> <input type="text" class="form-control floating-contact-action-icon-image" id="${escapeFloatingContactHtml(iconInputId)}" value="${escapeFloatingContactHtml(normalizedIconImage)}" placeholder="/uploads/home/floating-contact/floating-action-icon.webp" maxlength="255" data-maxlength="255" />
<button <button
type="button" type="button"
class="btn btn-outline-primary btn-upload-image" class="btn btn-outline-primary btn-upload-image"
@@ -363,38 +365,10 @@
}); });
}; };
const ensureLengthHint = (input) => {
if (!input || !input.dataset.maxlength) {
return;
}
let hint = input.parentElement?.querySelector(".floating-contact-length-hint");
if (!hint) {
hint = document.createElement("small");
hint.className = "text-muted d-block mt-1 floating-contact-length-hint";
input.parentElement?.appendChild(hint);
}
const max = Number(input.dataset.maxlength);
const length = input.value.length;
hint.textContent = `${length}/${max} characters`;
hint.classList.toggle("text-danger", length >= max);
};
const bindLengthHints = (scope = document) => {
scope.querySelectorAll("input[data-maxlength]").forEach((input) => {
ensureLengthHint(input);
if (input.dataset.lengthBound === "true") {
return;
}
input.addEventListener("input", () => ensureLengthHint(input));
input.dataset.lengthBound = "true";
});
};
const bindActionCard = (card) => { const bindActionCard = (card) => {
bindLengthHints(card); if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(card);
}
bindImageField( bindImageField(
card.querySelector(".floating-contact-action-icon-image"), card.querySelector(".floating-contact-action-icon-image"),
card.querySelector(".floating-contact-action-icon-preview"), card.querySelector(".floating-contact-action-icon-preview"),
@@ -475,7 +449,6 @@
} }
renderInitialActions(); renderInitialActions();
bindLengthHints(document);
}); });
window.homeScrapers.floatingContact = () => { window.homeScrapers.floatingContact = () => {

View File

@@ -20,11 +20,12 @@
<div class="col-lg-6"> <div class="col-lg-6">
<label class="form-label fw-medium">Fallback Background Image</label> <label class="form-label fw-medium">Fallback Background Image</label>
<small class="text-muted d-block mb-1"> <small class="text-muted d-block mb-1">
Tùy chọn dự phòng. Homepage hiện ưu tiên ảnh của từng slide. Tùy chọn dự phòng. Khung hero desktop hiện hiển thị khoảng 1512x544px, nên nên upload ảnh ngang ít nhất 1920x700px.
</small> </small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage" <input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/uploads/home/hero-fallback.jpg" /> value="<%= data.hero?.backgroundImage || '' %>" placeholder="/uploads/home/hero-fallback.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="home"> data-target-input="heroBackgroundImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -95,25 +96,28 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Title</label> <label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_title" <input type="text" class="form-control" id="heroSlide_<%= index %>_title"
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa" /> value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa"
maxlength="72" data-maxlength="72" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label> <label class="form-label fw-medium">Subtitle</label>
<small class="text-muted d-block mb-1">Hiện không render ngoài frontend, chỉ giữ để tương thích dữ liệu cũ.</small> <small class="text-muted d-block mb-1">Hiện không render ngoài frontend, chỉ giữ để tương thích dữ liệu cũ.</small>
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle" <input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified" /> value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified"
maxlength="48" data-maxlength="48" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3" <textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3"
placeholder="Enter hero description"><%= slide.description || '' %></textarea> placeholder="Enter hero description" maxlength="220" data-maxlength="220"><%= slide.description || '' %></textarea>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Slide Background Image</label> <label class="form-label fw-medium">Slide Background Image</label>
<small class="text-muted d-block mb-1">Ả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.</small> <small class="text-muted d-block mb-1">Ảnh này phủ toàn bộ hero. Khung desktop hiện khoảng 1512x544px, khuyến nghị upload 1920x700px hoặc lớn hơn.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage" <input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
value="<%= slide.heroImage || '' %>" placeholder="/uploads/home/hero-slide-01.jpg" /> value="<%= slide.heroImage || '' %>" placeholder="/uploads/home/hero-slide-01.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home"> data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -130,7 +134,8 @@
<label class="form-label fw-medium">Video URL</label> <label class="form-label fw-medium">Video URL</label>
<small class="text-muted d-block mb-1">Frontend hiện không render video trong hero. Giữ lại chỉ để tránh mất dữ liệu cũ.</small> <small class="text-muted d-block mb-1">Frontend hiện không render video trong hero. Giữ lại chỉ để tránh mất dữ liệu cũ.</small>
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl" <input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
value="<%= slide.videoUrl || '' %>" placeholder="Không bắt buộc" /> value="<%= slide.videoUrl || '' %>" placeholder="Không bắt buộc"
maxlength="255" data-maxlength="255" />
</div> </div>
<!-- Primary Button --> <!-- Primary Button -->
@@ -143,12 +148,14 @@
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Label</label> <label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel" <input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel"
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now" /> value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now"
maxlength="32" data-maxlength="32" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref" <input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref"
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact" /> value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact"
maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>
@@ -165,12 +172,14 @@
<label class="form-label fw-medium">Label</label> <label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel" <input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel"
value="<%= slide.secondaryButton?.label || '' %>" value="<%= slide.secondaryButton?.label || '' %>"
placeholder="e.g., Book Free Consultation" /> placeholder="e.g., Book Free Consultation"
maxlength="32" data-maxlength="32" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref" <input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref"
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact" /> value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact"
maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>
@@ -306,6 +315,9 @@
container.appendChild(clone); container.appendChild(clone);
updateLabels(); updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(clone);
}
}); });
container.addEventListener("click", (e) => { container.addEventListener("click", (e) => {
@@ -330,5 +342,8 @@
// Initial normalization (in case indices rendered from server are not 0..n) // Initial normalization (in case indices rendered from server are not 0..n)
updateLabels(); updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(container);
}
}); });
</script> </script>

View File

@@ -10,6 +10,7 @@
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<small class="text-muted d-block mb-3">Award icons trên homepage hiển thị khoảng 124x124px. Khuyến nghị upload ảnh vuông hoặc logo nền trong suốt tối thiểu 248x248px.</small>
<div id="visaConsultancyContainer"> <div id="visaConsultancyContainer">
<% for(let i=0; i<4; i++) { <% for(let i=0; i<4; i++) {
const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {}; const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {};
@@ -22,16 +23,17 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-8"> <div class="col-md-8">
<label class="form-label fw-medium">Award Name</label> <label class="form-label fw-medium">Award Name</label>
<input type="text" class="form-control visa-name" value="<%= item.name || '' %>" placeholder="Award Name" /> <input type="text" class="form-control visa-name" id="visaName_<%= i %>" value="<%= item.name || '' %>" placeholder="Award Name" maxlength="48" data-maxlength="48" />
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-medium">Year</label> <label class="form-label fw-medium">Year</label>
<input type="text" class="form-control visa-year" value="<%= item.year || '' %>" placeholder="e.g., 2025" /> <input type="text" class="form-control visa-year" id="visaYear_<%= i %>" value="<%= item.year || '' %>" placeholder="e.g., 2025" maxlength="8" data-maxlength="8" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Icon / Logo</label> <label class="form-label fw-medium">Icon / Logo</label>
<small class="text-muted d-block mb-1">Khuyến nghị ảnh vuông hoặc logo nền trong suốt 248x248px để hiển thị sắc nét.</small>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>" value="<%= item.icon || '' %>" /> <input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>" value="<%= item.icon || '' %>" maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="visaIcon_<%= i %>" data-image-type="home"> data-target-input="visaIcon_<%= i %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -62,6 +64,7 @@
</button> </button>
</div> </div>
<div class="card-body"> <div class="card-body">
<small class="text-muted d-block mb-3">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.</small>
<div id="brandPartnersContainer" class="row g-3"> <div id="brandPartnersContainer" class="row g-3">
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %> <% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
<div class="col-md-4 brand-partner-item"> <div class="col-md-4 brand-partner-item">
@@ -74,12 +77,13 @@
</button> </button>
</div> </div>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>" value="<%= item.logo || '' %>" /> <input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>" value="<%= item.logo || '' %>" maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="brandLogo_<%= index %>" data-image-type="home"> data-target-input="brandLogo_<%= index %>" data-image-type="home">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
</button> </button>
</div> </div>
<small class="text-muted d-block mt-2">Khuyến nghị SVG hoặc ảnh ngang 320x96px để logo không bị mờ trong slider.</small>
<div class="mt-2 text-center preview-container"> <div class="mt-2 text-center preview-container">
<img src="<%= item.logo || '' %>" class="img-thumbnail <%= item.logo ? '' : 'd-none' %>" style="height: 50px; object-fit: contain;"> <img src="<%= item.logo || '' %>" class="img-thumbnail <%= item.logo ? '' : 'd-none' %>" style="height: 50px; object-fit: contain;">
</div> </div>
@@ -134,11 +138,12 @@
</button> </button>
</div> </div>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="${id}"> <input type="text" class="form-control brand-logo-input" id="${id}" maxlength="255" data-maxlength="255">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="${id}" data-image-type="home"> <button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="${id}" data-image-type="home">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
</button> </button>
</div> </div>
<small class="text-muted d-block mt-2">Khuyến nghị SVG hoặc ảnh ngang 320x96px để logo không bị mờ trong slider.</small>
<div class="mt-2 text-center preview-container"> <div class="mt-2 text-center preview-container">
<img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;"> <img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;">
</div> </div>
@@ -146,5 +151,8 @@
</div> </div>
`; `;
container.appendChild(div); container.appendChild(div);
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(div);
}
} }
</script> </script>

View File

@@ -14,23 +14,27 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="testimonialsHeading" <input type="text" class="form-control" id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials" /> value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials"
maxlength="64" data-maxlength="64" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subheading</label> <label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="testimonialsSubheading" <input type="text" class="form-control" id="testimonialsSubheading"
value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say" /> value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say"
maxlength="40" data-maxlength="40" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Video URL</label> <label class="form-label fw-medium">Video URL</label>
<input type="text" class="form-control" id="testimonialsVideoUrl" <input type="text" class="form-control" id="testimonialsVideoUrl"
value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." /> value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..."
maxlength="255" data-maxlength="255" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Video Thumbnail</label> <label class="form-label fw-medium">Video Thumbnail</label>
<small class="text-muted d-block mb-1">Khung thumbnail desktop hiện khoảng 416x370px. Khuyến nghị upload tối thiểu 832x740px.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="testimonialsVideoThumbnail" <input type="text" class="form-control" id="testimonialsVideoThumbnail"
value="<%= data.testimonials?.videoThumbnail || '' %>" /> value="<%= data.testimonials?.videoThumbnail || '' %>" maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsVideoThumbnail" data-image-type="home"> data-target-input="testimonialsVideoThumbnail" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -69,17 +73,20 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Name</label> <label class="form-label fw-medium">Name</label>
<input type="text" class="form-control" id="testimonialsName_<%= index %>" <input type="text" class="form-control" id="testimonialsName_<%= index %>"
value="<%= item.name || '' %>" placeholder="e.g., Sohel Tanvir" /> value="<%= item.name || '' %>" placeholder="e.g., Sohel Tanvir"
maxlength="48" data-maxlength="48" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Role</label> <label class="form-label fw-medium">Role</label>
<input type="text" class="form-control" id="testimonialsRole_<%= index %>" <input type="text" class="form-control" id="testimonialsRole_<%= index %>"
value="<%= item.role || '' %>" placeholder="e.g., Student" /> value="<%= item.role || '' %>" placeholder="e.g., Student"
maxlength="48" data-maxlength="48" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Country</label> <label class="form-label fw-medium">Country</label>
<input type="text" class="form-control" id="testimonialsCountry_<%= index %>" <input type="text" class="form-control" id="testimonialsCountry_<%= index %>"
value="<%= item.country || '' %>" placeholder="e.g., Canada" /> value="<%= item.country || '' %>" placeholder="e.g., Canada"
maxlength="48" data-maxlength="48" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Rating</label> <label class="form-label fw-medium">Rating</label>
@@ -89,13 +96,14 @@
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Comment</label> <label class="form-label fw-medium">Comment</label>
<textarea class="form-control" id="testimonialsComment_<%= index %>" rows="3" <textarea class="form-control" id="testimonialsComment_<%= index %>" rows="3"
placeholder="Enter testimonial comment"><%= item.comment || '' %></textarea> placeholder="Enter testimonial comment" maxlength="280" data-maxlength="280"><%= item.comment || '' %></textarea>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Avatar</label> <label class="form-label fw-medium">Avatar</label>
<small class="text-muted d-block mb-1">Avatar hiển thị ở khoảng 48x48px. Khuyến nghị ảnh vuông 96x96px hoặc 128x128px.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="testimonialsAvatar_<%= index %>" <input type="text" class="form-control" id="testimonialsAvatar_<%= index %>"
value="<%= item.avatar || '' %>" /> value="<%= item.avatar || '' %>" maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsAvatar_<%= index %>" data-image-type="home"> data-target-input="testimonialsAvatar_<%= index %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -188,6 +196,9 @@
container.appendChild(clone); container.appendChild(clone);
updateLabels(); updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(clone);
}
}); });
container.addEventListener("click", (e) => { container.addEventListener("click", (e) => {
@@ -207,5 +218,8 @@
}); });
updateLabels(); updateLabels();
if (typeof initHomeCharacterCounters === "function") {
initHomeCharacterCounters(container);
}
}); });
</script> </script>

View File

@@ -13,18 +13,22 @@
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="videoGalleryHeading" <input type="text" class="form-control" id="videoGalleryHeading"
value="<%= data.videoGallery?.heading || '' %>" placeholder="e.g., VIDEO PLAY GALLERY" /> value="<%= data.videoGallery?.heading || '' %>" placeholder="e.g., VIDEO PLAY GALLERY"
maxlength="32" data-maxlength="32" data-maxwords="4" />
<small class="text-muted d-block mt-1">Limit this title to 4 words and 32 characters so it stays readable on the homepage.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Video URL</label> <label class="form-label fw-medium">Video URL</label>
<input type="text" class="form-control" id="videoGalleryVideoUrl" <input type="text" class="form-control" id="videoGalleryVideoUrl"
value="<%= data.videoGallery?.videoUrl || '' %>" placeholder="https://example.com/video.mp4" /> value="<%= data.videoGallery?.videoUrl || '' %>" placeholder="https://example.com/video.mp4"
maxlength="255" data-maxlength="255" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Thumbnail Image</label> <label class="form-label fw-medium">Thumbnail Image</label>
<small class="text-muted d-block mb-1">If no video is provided, this image fills a desktop area of about 1552x906px. Recommended minimum upload: 1920x1120px.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="videoGalleryThumbnail" <input type="text" class="form-control" id="videoGalleryThumbnail"
value="<%= data.videoGallery?.thumbnail || '' %>" /> value="<%= data.videoGallery?.thumbnail || '' %>" maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="videoGalleryThumbnail" data-image-type="home"> data-target-input="videoGalleryThumbnail" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload

View File

@@ -14,17 +14,19 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="visaCountriesHeading" <input type="text" class="form-control" id="visaCountriesHeading"
value="<%= data.visaCountries?.heading || '' %>" placeholder="e.g., Visa & VISAWAY Services To UK" /> value="<%= data.visaCountries?.heading || '' %>" placeholder="e.g., Visa & VISAWAY Services To UK"
maxlength="88" data-maxlength="88" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subheading</label> <label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="visaCountriesSubheading" <input type="text" class="form-control" id="visaCountriesSubheading"
value="<%= data.visaCountries?.subheading || '' %>" placeholder="e.g., UK. United Kingdom" /> value="<%= data.visaCountries?.subheading || '' %>" placeholder="e.g., UK. United Kingdom"
maxlength="56" data-maxlength="56" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="visaCountriesDescription" rows="3" <textarea class="form-control" id="visaCountriesDescription" rows="3"
placeholder="Enter description"><%= data.visaCountries?.description || '' %></textarea> placeholder="Enter description" maxlength="240" data-maxlength="240"><%= data.visaCountries?.description || '' %></textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -52,18 +54,20 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Country Name</label> <label class="form-label fw-medium">Country Name</label>
<input type="text" class="form-control" id="visaCountriesName_0" value="<%= featured.name || '' %>" <input type="text" class="form-control" id="visaCountriesName_0" value="<%= featured.name || '' %>"
placeholder="e.g., United Kingdom" /> placeholder="e.g., United Kingdom" maxlength="40" data-maxlength="40" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Country Code</label> <label class="form-label fw-medium">Country Code</label>
<input type="text" class="form-control" id="visaCountriesCode_0" value="<%= featured.code || '' %>" <input type="text" class="form-control" id="visaCountriesCode_0" value="<%= featured.code || '' %>"
placeholder="e.g., UK" /> placeholder="e.g., UK" maxlength="12" data-maxlength="12" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Flag / Illustration Image</label> <label class="form-label fw-medium">Flag / Illustration Image</label>
<small class="text-muted d-block mb-1">Khung ảnh desktop hiện khoảng 840x830px. Khuyến nghị ảnh gần vuông, tối thiểu 1000x1000px.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="visaCountriesFlag_0" <input type="text" class="form-control" id="visaCountriesFlag_0"
value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png" /> value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="visaCountriesFlag_0" data-image-type="home"> data-target-input="visaCountriesFlag_0" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -73,12 +77,12 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaCountriesLink_0" value="<%= featured.link || '' %>" <input type="text" class="form-control" id="visaCountriesLink_0" value="<%= featured.link || '' %>"
placeholder="/country-details/uk" /> placeholder="/country-details/uk" maxlength="255" data-maxlength="255" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Visa Types (comma-separated)</label> <label class="form-label fw-medium">Visa Types (comma-separated)</label>
<textarea class="form-control" id="visaCountriesVisaTypes_0" rows="2" <textarea class="form-control" id="visaCountriesVisaTypes_0" rows="2"
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"><%= (featured.visaTypes || []).join(', ') %></textarea> placeholder="e.g., Student Visa, Work Visa, Tourist Visa" maxlength="220" data-maxlength="220"><%= (featured.visaTypes || []).join(', ') %></textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -100,12 +104,14 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Label</label> <label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="visaCountriesCtaLabel" <input type="text" class="form-control" id="visaCountriesCtaLabel"
value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" /> value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started"
maxlength="32" data-maxlength="32" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaCountriesCtaHref" <input type="text" class="form-control" id="visaCountriesCtaHref"
value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact" /> value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact"
maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,12 +14,14 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="visaSolutionsHeading" <input type="text" class="form-control" id="visaSolutionsHeading"
value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions" /> value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions"
maxlength="64" data-maxlength="64" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subheading</label> <label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="visaSolutionsSubheading" <input type="text" class="form-control" id="visaSolutionsSubheading"
value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services" /> value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services"
maxlength="40" data-maxlength="40" />
</div> </div>
</div> </div>
</div> </div>
@@ -53,22 +55,25 @@
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label fw-medium">Number</label> <label class="form-label fw-medium">Number</label>
<input type="text" class="form-control" id="visaSolutionsNumber_<%= index %>" <input type="text" class="form-control" id="visaSolutionsNumber_<%= index %>"
value="<%= item.number || '' %>" placeholder="e.g., 01" /> value="<%= item.number || '' %>" placeholder="e.g., 01"
maxlength="4" data-maxlength="4" />
</div> </div>
<div class="col-md-9"> <div class="col-md-9">
<label class="form-label fw-medium">Title</label> <label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="visaSolutionsTitle_<%= index %>" <input type="text" class="form-control" id="visaSolutionsTitle_<%= index %>"
value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance" /> value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance"
maxlength="56" data-maxlength="56" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="visaSolutionsDescription_<%= index %>" rows="2" <textarea class="form-control" id="visaSolutionsDescription_<%= index %>" rows="2"
placeholder="Enter description"><%= item.description || '' %></textarea> placeholder="Enter description" maxlength="180" data-maxlength="180"><%= item.description || '' %></textarea>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaSolutionsLink_<%= index %>" <input type="text" class="form-control" id="visaSolutionsLink_<%= index %>"
value="<%= item.link || '' %>" placeholder="/service-details" /> value="<%= item.link || '' %>" placeholder="/service-details"
maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -15,23 +15,26 @@
<label class="form-label fw-medium">Heading</label> <label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="whyChooseUsHeading" <input type="text" class="form-control" id="whyChooseUsHeading"
value="<%= data.whyChooseUs?.heading || '' %>" value="<%= data.whyChooseUs?.heading || '' %>"
placeholder="e.g., Turning Study Abroad Dreams Into Reality" /> placeholder="e.g., Turning Study Abroad Dreams Into Reality"
maxlength="72" data-maxlength="72" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Subheading</label> <label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="whyChooseUsSubheading" <input type="text" class="form-control" id="whyChooseUsSubheading"
value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy" /> value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy"
maxlength="48" data-maxlength="48" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Highlight Word (Optional)</label> <label class="form-label fw-medium">Highlight Word (Optional)</label>
<input type="text" class="form-control" id="whyChooseUsHighlightWord" <input type="text" class="form-control" id="whyChooseUsHighlightWord"
value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams" /> value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams"
maxlength="24" data-maxlength="24" />
<small class="text-muted">This word in the heading will be wrapped in a colored span.</small> <small class="text-muted">This word in the heading will be wrapped in a colored span.</small>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="whyChooseUsDescription" rows="3" <textarea class="form-control" id="whyChooseUsDescription" rows="3"
placeholder="Enter description"><%= data.whyChooseUs?.description || '' %></textarea> placeholder="Enter description" maxlength="260" data-maxlength="260"><%= data.whyChooseUs?.description || '' %></textarea>
</div> </div>
</div> </div>
</div> </div>
@@ -50,10 +53,11 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Main Image</label> <label class="form-label fw-medium">Main Image</label>
<small class="text-muted d-block mb-1">Recommended size: 375x419px</small> <small class="text-muted d-block mb-1">Khung desktop hiện khoảng 318x347px. Khuyến nghị upload ít nhất 750x820px, tỉ lệ dọc khoảng 0.91:1.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsMainImage" <input type="text" class="form-control" id="whyChooseUsMainImage"
value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg" /> value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsMainImage" data-image-type="home"> data-target-input="whyChooseUsMainImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -69,11 +73,12 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Secondary Image</label> <label class="form-label fw-medium">Secondary Image</label>
<small class="text-muted d-block mb-1">Recommended size: 376x394px</small> <small class="text-muted d-block mb-1">Khung desktop hiện khoảng 363x380px. Khuyến nghị upload ít nhất 760x800px, tỉ lệ dọc khoảng 0.95:1.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsSecondaryImage" <input type="text" class="form-control" id="whyChooseUsSecondaryImage"
value="<%= data.whyChooseUs?.secondaryImage || '' %>" value="<%= data.whyChooseUs?.secondaryImage || '' %>"
placeholder="/assets/img/home-1/about/about-02.jpg" /> placeholder="/assets/img/home-1/about/about-02.jpg"
maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsSecondaryImage" data-image-type="home"> data-target-input="whyChooseUsSecondaryImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -108,9 +113,10 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Icon URL</label> <label class="form-label fw-medium">Icon URL</label>
<small class="text-muted d-block mb-1">Icon hiển thị ở khoảng 24x24px. Khuyến nghị dùng SVG hoặc ảnh vuông 48x48px.</small>
<div class="input-group mb-2"> <div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsIcon_<%= index %>" <input type="text" class="form-control" id="whyChooseUsIcon_<%= index %>"
value="<%= item.icon || '' %>" /> value="<%= item.icon || '' %>" maxlength="255" data-maxlength="255" />
<button type="button" class="btn btn-outline-primary btn-upload-image" <button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsIcon_<%= index %>" data-image-type="home"> data-target-input="whyChooseUsIcon_<%= index %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload <i class="fas fa-upload me-1"></i>Upload
@@ -120,12 +126,14 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Title</label> <label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="whyChooseUsTitle_<%= index %>" <input type="text" class="form-control" id="whyChooseUsTitle_<%= index %>"
value="<%= item.title || '' %>" placeholder="e.g., Global Reach" /> value="<%= item.title || '' %>" placeholder="e.g., Global Reach"
maxlength="40" data-maxlength="40" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-medium">Description</label> <label class="form-label fw-medium">Description</label>
<input type="text" class="form-control" id="whyChooseUsItemDescription_<%= index %>" <input type="text" class="form-control" id="whyChooseUsItemDescription_<%= index %>"
value="<%= item.description || '' %>" placeholder="e.g., Expanding Opportunities Worldwide" /> value="<%= item.description || '' %>" placeholder="e.g., Expanding Opportunities Worldwide"
maxlength="72" data-maxlength="72" />
</div> </div>
</div> </div>
</div> </div>
@@ -148,7 +156,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-medium">Feature <%= index + 1 %></label> <label class="form-label fw-medium">Feature <%= index + 1 %></label>
<input type="text" class="form-control" id="whyChooseUsFeature_<%= index %>" value="<%= feature %>" <input type="text" class="form-control" id="whyChooseUsFeature_<%= index %>" value="<%= feature %>"
placeholder="Enter feature" /> placeholder="Enter feature" maxlength="96" data-maxlength="96" />
</div> </div>
<% }); %> <% }); %>
</div> </div>
@@ -168,12 +176,14 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Label</label> <label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="whyChooseUsCtaLabel" <input type="text" class="form-control" id="whyChooseUsCtaLabel"
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" /> value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started"
maxlength="32" data-maxlength="32" />
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-medium">Link</label> <label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="whyChooseUsCtaHref" <input type="text" class="form-control" id="whyChooseUsCtaHref"
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about" /> value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about"
maxlength="255" data-maxlength="255" />
</div> </div>
</div> </div>
</div> </div>