forked from UKSOURCE/cms.hailearning.edu.vn
Add CMS support for floating contact widget with Facebook/Zalo quick actions. Includes mongoose schema, admin UI tab, image upload with sharp resize presets, deferred form submission with draft persistence, and upload middleware error handling.
324 lines
9.0 KiB
JavaScript
324 lines
9.0 KiB
JavaScript
const mongoose = require("mongoose");
|
|
|
|
const { Schema } = mongoose;
|
|
|
|
// Reusable small schemas
|
|
const LinkSchema = new Schema(
|
|
{
|
|
label: { type: String, default: "" },
|
|
href: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const FloatingContactBrandSchema = new Schema(
|
|
{
|
|
imageSrc: { type: String, default: "" },
|
|
imageAlt: { type: String, default: "", maxlength: 60 },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const FloatingContactTriggerSchema = new Schema(
|
|
{
|
|
imageSrc: { type: String, default: "" },
|
|
icon: { type: String, default: "fa-comments" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const FloatingContactActionSchema = new Schema(
|
|
{
|
|
id: { type: String, default: "" },
|
|
platform: { type: String, default: "" },
|
|
enabled: { type: Boolean, default: true },
|
|
label: { type: String, default: "", maxlength: 48 },
|
|
subtitle: { type: String, default: "", maxlength: 48 },
|
|
href: { type: String, default: "" },
|
|
iconImage: { type: String, default: "" },
|
|
iconType: { type: String, default: "iconClass" },
|
|
iconClass: { type: String, default: "" },
|
|
iconText: { type: String, default: "", maxlength: 12 },
|
|
order: { type: Number, default: 0 },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const FloatingContactSchema = new Schema(
|
|
{
|
|
enabled: { type: Boolean, default: true },
|
|
position: { type: String, default: "bottom-right" },
|
|
panelTitle: { type: String, default: "", maxlength: 72 },
|
|
brand: { type: FloatingContactBrandSchema, default: () => ({}) },
|
|
trigger: { type: FloatingContactTriggerSchema, default: () => ({}) },
|
|
actions: { type: [FloatingContactActionSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
// Hero slide (for multiple hero items in slider)
|
|
const HeroSlideSchema = new Schema(
|
|
{
|
|
title: { type: String, default: "" },
|
|
subtitle: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
primaryButton: { type: LinkSchema, default: () => ({}) },
|
|
secondaryButton: { type: LinkSchema, default: () => ({}) },
|
|
heroImage: { type: String, default: "" },
|
|
videoUrl: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const HeroSchema = new Schema(
|
|
{
|
|
// Background for whole hero section
|
|
backgroundImage: { type: String, default: "" },
|
|
|
|
// Multiple slides
|
|
slides: { type: [HeroSlideSchema], default: [] },
|
|
|
|
// Legacy single-slide fields (backward compatible)
|
|
title: { type: String, default: "" },
|
|
subtitle: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
primaryButton: { type: LinkSchema, default: () => ({}) },
|
|
secondaryButton: { type: LinkSchema, default: () => ({}) },
|
|
heroImage: { type: String, default: "" },
|
|
videoUrl: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const WhyChooseUsItemSchema = new Schema(
|
|
{
|
|
icon: { type: String, default: "" },
|
|
title: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const WhyChooseUsSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
highlightWord: { type: String, default: "" },
|
|
mainImage: { type: String, default: "" },
|
|
secondaryImage: { type: String, default: "" },
|
|
items: { type: [WhyChooseUsItemSchema], default: [] },
|
|
features: { type: [String], default: [] },
|
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VisaSolutionItemSchema = new Schema(
|
|
{
|
|
number: { type: String, default: "" },
|
|
title: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
link: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VisaSolutionsSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
items: { type: [VisaSolutionItemSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VisaCountrySchema = new Schema(
|
|
{
|
|
name: { type: String, default: "" },
|
|
code: { type: String, default: "" },
|
|
flag: { type: String, default: "" },
|
|
link: { type: String, default: "" },
|
|
visaTypes: { type: [String], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VisaCountriesSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
countries: { type: [VisaCountrySchema], default: [] },
|
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const TestimonialSchema = new Schema(
|
|
{
|
|
name: { type: String, default: "" },
|
|
role: { type: String, default: "" },
|
|
country: { type: String, default: "" },
|
|
rating: { type: Number, default: 5 },
|
|
comment: { type: String, default: "" },
|
|
avatar: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const TestimonialsSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
videoUrl: { type: String, default: "" },
|
|
videoThumbnail: { type: String, default: "" },
|
|
items: { type: [TestimonialSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VideoGallerySchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
videoUrl: { type: String, default: "" },
|
|
thumbnail: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const FaqItemSchema = new Schema(
|
|
{
|
|
question: { type: String, default: "" },
|
|
answer: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const FaqSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
|
items: { type: [FaqItemSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const AchievementItemSchema = new Schema(
|
|
{
|
|
value: { type: String, default: "" },
|
|
suffix: { type: String, default: "" },
|
|
label: { type: String, default: "" },
|
|
description: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const AchievementsSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
items: { type: [AchievementItemSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VisaConsultancyItemSchema = new Schema(
|
|
{
|
|
name: { type: String, default: "" },
|
|
icon: { type: String, default: "" },
|
|
year: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const VisaConsultancySchema = new Schema(
|
|
{
|
|
items: { type: [VisaConsultancyItemSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const BrandItemSchema = new Schema(
|
|
{
|
|
logo: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const BrandsSchema = new Schema(
|
|
{
|
|
items: { type: [BrandItemSchema], default: [] },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const PartnersSchema = new Schema(
|
|
{
|
|
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
|
|
brands: { type: BrandsSchema, default: () => ({}) },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const BlogPreviewItemSchema = new Schema(
|
|
{
|
|
title: { type: String, default: "" },
|
|
excerpt: { type: String, default: "" },
|
|
category: { type: String, default: "" },
|
|
date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20")
|
|
author: {
|
|
name: { type: String, default: "" },
|
|
avatar: { type: String, default: "" },
|
|
},
|
|
comments: { type: Number, default: 0 },
|
|
link: { type: String, default: "" },
|
|
thumbnail: { type: String, default: "" },
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
const BlogPreviewSchema = new Schema(
|
|
{
|
|
heading: { type: String, default: "" },
|
|
subheading: { type: String, default: "" },
|
|
ctaButton: { type: LinkSchema, default: () => ({}) },
|
|
items: { type: [BlogPreviewItemSchema], default: [] },
|
|
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],
|
|
},
|
|
{ _id: false },
|
|
);
|
|
|
|
/**
|
|
* Home page content model
|
|
*
|
|
* NOTE:
|
|
* - This schema is based on `hailearning.edu.vn/app/home.json`.
|
|
* - `strict: false` keeps backward compatibility with any existing CMS-only sections
|
|
* (e.g. about/missionVision/programs/newsletter/latestPosts...) that the admin UI might still send.
|
|
*/
|
|
const HomeSchema = new Schema(
|
|
{
|
|
hero: { type: HeroSchema, default: () => ({}) },
|
|
whyChooseUs: { type: WhyChooseUsSchema, default: () => ({}) },
|
|
visaSolutions: { type: VisaSolutionsSchema, default: () => ({}) },
|
|
visaCountries: { type: VisaCountriesSchema, default: () => ({}) },
|
|
testimonials: { type: TestimonialsSchema, default: () => ({}) },
|
|
videoGallery: { type: VideoGallerySchema, default: () => ({}) },
|
|
faq: { type: FaqSchema, default: () => ({}) },
|
|
achievements: { type: AchievementsSchema, default: () => ({}) },
|
|
partners: { type: PartnersSchema, default: () => ({}) },
|
|
blogPreview: { type: BlogPreviewSchema, default: () => ({}) },
|
|
floatingContact: { type: FloatingContactSchema, default: () => ({}) },
|
|
},
|
|
{
|
|
timestamps: true,
|
|
strict: false,
|
|
},
|
|
);
|
|
|
|
module.exports = mongoose.model("Home", HomeSchema);
|
|
|