forked from UKSOURCE/cms.hailearning.edu.vn
fea/nhat-dat-11042026-merge #1
@@ -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],
|
||||||
|
|||||||
168
models/home.js
168
models/home.js
@@ -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' }],
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -1662,4 +1741,4 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -172,4 +176,4 @@
|
|||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -145,4 +149,4 @@
|
|||||||
items
|
items
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -56,4 +60,4 @@
|
|||||||
thumbnail: getVal("videoGalleryThumbnail"),
|
thumbnail: getVal("videoGalleryThumbnail"),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -142,4 +148,4 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -134,4 +139,4 @@
|
|||||||
|
|
||||||
updateLabels();
|
updateLabels();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -230,4 +240,4 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user