Merge branch 'develop' into fea/dat-20042026-CMS-Partnerships-Accreditation-History-Admissions-Policies

This commit is contained in:
Tống Thành Đạt
2026-04-22 18:08:53 +07:00
80 changed files with 3334 additions and 19058 deletions
+161
View File
@@ -0,0 +1,161 @@
const mongoose = require("mongoose");
const { Schema } = mongoose;
// Hero
const HeroSchema = new Schema(
{
badge: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
studentCount: { type: String, default: "" },
image: { type: String, default: "" },
imageAlt: { type: String, default: "" },
coreValues: { type: [String], default: [] },
},
{ _id: false },
);
// Leadership
const SocialSchema = new Schema(
{
linkedin: { type: String, default: "" },
twitter: { type: String, default: "" },
},
{ _id: false },
);
const LeadershipMemberSchema = new Schema(
{
name: { type: String, default: "" },
role: { type: String, default: "" },
bio: { type: String, default: "" },
avatar: { type: String, default: "" },
social: { type: SocialSchema, default: () => ({}) },
},
{ _id: false },
);
const LeadershipSchema = new Schema(
{
heading: { type: String, default: "" },
description: { type: String, default: "" },
members: { type: [LeadershipMemberSchema], default: [] },
},
{ _id: false },
);
// Learning Model
const LearningFeatureSchema = new Schema(
{
icon: { type: String, default: "" },
title: { type: String, default: "" },
desc: { type: String, default: "" },
},
{ _id: false },
);
const LearningModeSchema = new Schema(
{
title: { type: String, default: "" },
description: { type: String, default: "" },
features: { type: [LearningFeatureSchema], default: [] },
},
{ _id: false },
);
const LearningModelSchema = new Schema(
{
heading: { type: String, default: "" },
description: { type: String, default: "" },
async: { type: LearningModeSchema, default: () => ({}) },
sync: { type: LearningModeSchema, default: () => ({}) },
},
{ _id: false },
);
// Accreditation
const AccreditationBadgeSchema = new Schema(
{
icon: { type: String, default: "" },
title: { type: String, default: "" },
desc: { type: String, default: "" },
},
{ _id: false },
);
const AccreditationStatSchema = new Schema(
{
value: { type: String, default: "" },
label: { type: String, default: "" },
isPrimary: { type: Boolean, default: false },
},
{ _id: false },
);
const AccreditationSchema = new Schema(
{
heading: { type: String, default: "" },
description: { type: String, default: "" },
badges: { type: [AccreditationBadgeSchema], default: [] },
stats: { type: [AccreditationStatSchema], default: [] },
},
{ _id: false },
);
// Success Stories
const StorySchema = new Schema(
{
quote: { type: String, default: "" },
name: { type: String, default: "" },
program: { type: String, default: "" },
avatar: { type: String, default: "" },
},
{ _id: false },
);
const SuccessStoriesSchema = new Schema(
{
heading: { type: String, default: "" },
description: { type: String, default: "" },
stories: { type: [StorySchema], default: [] },
},
{ _id: false },
);
// CTA
const CtaButtonSchema = new Schema(
{
label: { type: String, default: "" },
href: { type: String, default: "" },
},
{ _id: false },
);
const CtaSchema = new Schema(
{
heading: { type: String, default: "" },
description: { type: String, default: "" },
primaryButton: { type: CtaButtonSchema, default: () => ({}) },
secondaryButton: { type: CtaButtonSchema, default: () => ({}) },
},
{ _id: false },
);
// Root schema
const AboutSchema = new Schema(
{
hero: { type: HeroSchema, default: () => ({}) },
leadership: { type: LeadershipSchema, default: () => ({}) },
learningModel: { type: LearningModelSchema, default: () => ({}) },
accreditation: { type: AccreditationSchema, default: () => ({}) },
successStories: { type: SuccessStoriesSchema, default: () => ({}) },
cta: { type: CtaSchema, default: () => ({}) },
},
{
timestamps: true,
strict: false,
},
);
module.exports = mongoose.model("About", AboutSchema);
-206
View File
@@ -1,206 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Appointment) {
delete mongoose.models.Appointment;
}
if (mongoose.connection.models.Appointment) {
delete mongoose.connection.models.Appointment;
}
// Schema cho hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Make Appointment",
},
backgroundImage: {
type: String,
trim: true,
default: "",
},
subtitle: {
type: String,
trim: true,
default: "",
},
heading: {
type: String,
trim: true,
default: "",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema cho form field
const formFieldSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
label: {
type: String,
trim: true,
default: "",
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "date", "select"],
},
placeholder: {
type: String,
trim: true,
default: "",
},
required: {
type: Boolean,
default: false,
},
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
},
{ _id: false }
);
// Schema cho submit button
const submitButtonSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
default: "Request Appointment",
},
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn",
},
},
{ _id: false }
);
// Schema cho form
const formSchema = new mongoose.Schema(
{
heading: {
type: String,
trim: true,
default: "Request Appointment",
},
fields: {
type: [formFieldSchema],
default: [],
},
submitButton: {
type: submitButtonSchema,
default: () => ({}),
},
},
{ _id: false }
);
// Main Appointment Schema
const appointmentSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
visaOptions: {
type: [String],
default: [],
},
form: {
type: formSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
appointmentSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default appointment exists
const existingAppointment = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Make Appointment",
backgroundImage: jsonData.hero?.backgroundImage || "",
subtitle: jsonData.hero?.subtitle || "",
heading: jsonData.hero?.heading || "",
description: jsonData.hero?.description || "",
},
visaOptions: Array.isArray(jsonData.visaOptions) ? jsonData.visaOptions : [],
form: {
heading: jsonData.form?.heading || "Request Appointment",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
label: field.label || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
colClass: field.colClass || "col-lg-12",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Request Appointment",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn",
},
},
};
if (existingAppointment) {
// Update existing appointment
existingAppointment.hero = processedData.hero;
existingAppointment.visaOptions = processedData.visaOptions;
existingAppointment.form = processedData.form;
await existingAppointment.save();
console.log("Appointment data updated successfully");
return existingAppointment;
} else {
// Create new appointment
const newAppointment = await this.create({
name: "default",
...processedData,
});
console.log("Appointment data imported successfully");
return newAppointment;
}
} catch (error) {
console.error("Error migrating appointment data:", error);
throw error;
}
};
module.exports = mongoose.model("Appointment", appointmentSchema);
-83
View File
@@ -1,83 +0,0 @@
const mongoose = require("mongoose");
/**
* Schema for Appointment Submissions
* Stores appointment requests from users
*/
const appointmentSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
appointmentDate: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
visaTypes: {
type: [String],
default: [],
},
status: {
type: String,
enum: ["pending", "confirmed", "completed", "cancelled"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
confirmedAt: {
type: Date,
default: null,
},
completedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
appointmentSubmissionSchema.index({ status: 1, createdAt: -1 });
appointmentSubmissionSchema.index({ email: 1 });
appointmentSubmissionSchema.index({ appointmentDate: 1 });
module.exports = mongoose.model("AppointmentSubmission", appointmentSubmissionSchema);
-106
View File
@@ -1,106 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Booking) {
delete mongoose.models.Booking;
}
if (mongoose.connection.models.Booking) {
delete mongoose.connection.models.Booking;
}
const bookingSchema = new mongoose.Schema(
{
hero: {
title: String,
backgroundImage: String,
},
searchBar: {
locationLabel: String,
holidaySeasonLabel: String,
searchButtonText: String,
},
filterPanel: {
title: String,
priceTitle: String,
priceLabel: String,
pricePlaceholder: String,
priceMin: Number,
priceMax: Number,
activitiesTitle: String,
ageTitle: String,
ageSelectPlaceholder: String,
ageMin: Number,
ageMax: Number,
ratingTitle: String,
ratingOptions: [
{
value: String,
label: String,
},
],
resetButtonText: String,
},
programs: [
{
value: String,
label: String,
},
],
holidays: [
{
value: String,
label: String,
},
],
locations: [
{
value: String,
label: String,
},
],
camps: [
{
name: String,
price: Number,
priceText: String,
season: [String],
age: [Number],
locations: [String],
image: String,
link: String,
program: String,
rating: Number,
},
],
// Configuration - Dùng Mixed type để chấp nhận bất kỳ structure nào
configuration: mongoose.Schema.Types.Mixed,
formSteps: [
{
step: Number,
title: String,
sections: [
{
id: String,
fields: [mongoose.Schema.Types.Mixed],
},
],
},
],
validation: mongoose.Schema.Types.Mixed,
},
{
timestamps: true,
strict: false
}
);
module.exports = mongoose.model("Booking", bookingSchema);
-200
View File
@@ -1,200 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.BookingSubmission) {
delete mongoose.models.BookingSubmission;
}
if (mongoose.connection.models.BookingSubmission) {
delete mongoose.connection.models.BookingSubmission;
}
const bookingSubmissionSchema = new mongoose.Schema(
{
// Liên kết với activity và session
activityId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Activity',
required: true
},
sessionId: {
type: String,
required: true
},
// Thông tin người đăng ký
parentFirstName: {
type: String,
required: true,
trim: true
},
parentLastName: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true,
lowercase: true
},
phone: {
type: String,
required: true,
trim: true
},
// Thông tin địa chỉ
address: {
type: String,
required: true,
trim: true
},
city: {
type: String,
required: true,
trim: true
},
country: {
type: String,
required: true,
trim: true
},
postalCode: {
type: String,
required: true,
trim: true
},
// Thông tin người tham gia
participantFirstName: {
type: String,
required: true,
trim: true
},
participantLastName: {
type: String,
required: true,
trim: true
},
participantBirthDate: {
type: Date,
required: true
},
participantGender: {
type: String,
enum: ['male', 'female', 'other'],
required: true
},
numberOfParticipants: {
type: Number,
required: true,
min: 1
},
// Thông tin y tế và đặc biệt
medicalConditions: {
type: String,
trim: true,
default: ''
},
dietaryRestrictions: {
type: String,
enum: ['none', 'vegetarian', 'vegan', 'halal', 'kosher', 'gluten-free', 'other'],
default: 'none'
},
specialRequests: {
type: String,
trim: true,
default: ''
},
// Thông tin liên hệ khẩn cấp
emergencyContact: {
type: String,
required: true,
trim: true
},
emergencyPhone: {
type: String,
required: true,
trim: true
},
// Điều khoản và thông báo
agreeTerms: {
type: Boolean,
required: true,
default: false
},
agreeNewsletter: {
type: Boolean,
default: false
},
// Trạng thái đăng ký
status: {
type: String,
enum: ['pending', 'confirmed', 'cancelled', 'completed'],
default: 'pending'
},
// Ghi chú admin
adminNotes: {
type: String,
trim: true,
default: ''
},
// Thông tin thanh toán
paymentStatus: {
type: String,
enum: ['pending', 'partial', 'paid', 'refunded'],
default: 'pending'
},
totalAmount: {
type: Number,
default: 0
},
paidAmount: {
type: Number,
default: 0
}
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
// Virtual để tính tuổi của participant
bookingSubmissionSchema.virtual('participantAge').get(function() {
if (this.participantBirthDate) {
const today = new Date();
const birthDate = new Date(this.participantBirthDate);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
return 0;
});
// Virtual để lấy thông tin activity
bookingSubmissionSchema.virtual('activity', {
ref: 'Activity',
localField: 'activityId',
foreignField: '_id',
justOne: true
});
// Index for better performance
bookingSubmissionSchema.index({ activityId: 1, sessionId: 1 });
bookingSubmissionSchema.index({ email: 1 });
bookingSubmissionSchema.index({ status: 1 });
bookingSubmissionSchema.index({ createdAt: -1 });
module.exports = mongoose.model("BookingSubmission", bookingSubmissionSchema);
+58 -169
View File
@@ -1,213 +1,102 @@
const mongoose = require("mongoose");
// Schema cho menu links
const menuLinkSchema = new mongoose.Schema(
const { Schema } = mongoose;
// Brand
const LogoSchema = new Schema(
{
label: {
type: String,
required: true,
trim: true,
},
href: {
type: String,
required: true,
trim: true,
},
order: {
type: Number,
required: false,
default: 0,
},
image: { type: String, default: "" },
href: { type: String, default: "/" },
},
{ _id: false },
);
// Schema cho social links
const socialLinkSchema = new mongoose.Schema(
const SocialItemSchema = new Schema(
{
icon: {
type: String,
required: true,
trim: true,
},
href: {
type: String,
required: true,
trim: true,
},
icon: { type: String, default: "" },
href: { type: String, default: "#" },
},
{ _id: false },
);
// Schema cho phone
const phoneSchema = new mongoose.Schema(
const BrandSchema = new Schema(
{
display: {
type: String,
required: false,
trim: true,
default: "",
},
href: {
type: String,
required: false,
trim: true,
default: "",
},
logo: { type: LogoSchema, default: () => ({}) },
description: { type: String, default: "" },
social: { type: [SocialItemSchema], default: [] },
},
{ _id: false },
);
// Schema cho logo
const logoSchema = new mongoose.Schema(
// Explore
const ExploreLinkSchema = new Schema(
{
src: {
type: String,
required: false,
trim: true,
default: "",
},
alt: {
type: String,
required: false,
trim: true,
default: "",
},
href: {
type: String,
required: false,
trim: true,
default: "/",
},
label: { type: String, default: "" },
href: { type: String, default: "#" },
},
{ _id: false },
);
// Schema cho copyright
const copyrightSchema = new mongoose.Schema(
const ExploreSchema = new Schema(
{
text: {
type: String,
required: false,
trim: true,
default: "Copyright©",
},
brand: {
type: String,
required: false,
trim: true,
default: "",
},
rights: {
type: String,
required: false,
trim: true,
default: "All Rights Reserved.",
},
heading: { type: String, default: "" },
links: { type: [ExploreLinkSchema], default: [] },
},
{ _id: false },
);
// Schema cho top section
const topSchema = new mongoose.Schema(
// Contact
const ContactSchema = new Schema(
{
bgImage: {
type: String,
required: false,
trim: true,
default: "",
},
phone: {
type: phoneSchema,
default: () => ({ display: "", href: "" }),
},
address: {
type: String,
required: false,
trim: true,
default: "",
},
logo: {
type: logoSchema,
default: () => ({ src: "", alt: "", href: "/" }),
},
menuLinks: {
type: [menuLinkSchema],
default: [],
},
socialLinks: {
type: [socialLinkSchema],
default: [],
},
heading: { type: String, default: "" },
address: { type: String, default: "" },
phone: { type: String, default: "" },
email: { type: String, default: "" },
},
{ _id: false },
);
// Schema cho bottom section
const bottomSchema = new mongoose.Schema(
// Newsletter
const NewsletterSchema = new Schema(
{
copyright: {
type: copyrightSchema,
default: () => ({ text: "Copyright©", brand: "", rights: "All Rights Reserved." }),
},
menuLinks: {
type: [menuLinkSchema],
default: [],
},
heading: { type: String, default: "" },
description: { type: String, default: "" },
placeholder: { type: String, default: "" },
buttonText: { type: String, default: "" },
},
{ _id: false },
);
// Main Footer Schema - khớp 100% với footer.json
const footerSchema = new mongoose.Schema(
// Bottom
const BottomLinkSchema = new Schema(
{
top: {
type: topSchema,
default: () => ({
bgImage: "",
phone: { display: "", href: "" },
address: "",
logo: { src: "", alt: "", href: "/" },
menuLinks: [],
socialLinks: [],
}),
},
bottom: {
type: bottomSchema,
default: () => ({
copyright: { text: "Copyright©", brand: "", rights: "All Rights Reserved." },
menuLinks: [],
}),
},
label: { type: String, default: "" },
href: { type: String, default: "#" },
},
{ _id: false },
);
const BottomSchema = new Schema(
{
copyright: { type: String, default: "" },
links: { type: [BottomLinkSchema], default: [] },
},
{ _id: false },
);
// Root
const FooterSchema = new Schema(
{
brand: { type: BrandSchema, default: () => ({}) },
explore: { type: ExploreSchema, default: () => ({}) },
contact: { type: ContactSchema, default: () => ({}) },
newsletter: { type: NewsletterSchema, default: () => ({}) },
bottom: { type: BottomSchema, default: () => ({}) },
},
{
timestamps: true,
strict: false,
},
);
// Static method để lấy hoặc tạo footer duy nhất
footerSchema.statics.getSingle = async function () {
let footer = await this.findOne();
if (!footer) {
footer = await this.create({});
}
return footer;
};
// Migration method để import từ JSON hiện tại
footerSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Xóa tất cả documents hiện có
await this.deleteMany({});
// Tạo document mới
const footer = await this.create(jsonData);
console.log("Footer data migrated successfully");
return footer;
} catch (error) {
console.error("Error migrating footer data:", error);
throw error;
}
};
module.exports = mongoose.model("Footer", footerSchema);
module.exports = mongoose.model("Footer", FooterSchema);
+12 -77
View File
@@ -1,84 +1,7 @@
const mongoose = require("mongoose");
const socialLinkSchema = new mongoose.Schema(
{
platform: {
type: String,
required: true,
enum: ["linkedin", "twitter", "instagram", "youtube", "facebook"],
},
url: {
type: String,
required: true,
},
icon: String,
order: {
type: Number,
default: 0,
},
},
{ _id: false },
);
const languageSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
},
{ _id: false },
);
const menuItemSchema = new mongoose.Schema(
{
label: {
type: String,
required: true,
},
href: {
type: String,
required: true,
},
icon: String,
order: {
type: Number,
default: 0,
},
children: [this],
},
{ _id: false },
);
const headerSchema = new mongoose.Schema(
{
// Top bar
top: {
phone: String,
email: String,
location: String,
socialLinks: [socialLinkSchema],
languages: [languageSchema],
},
// Offcanvas
offcanvas: {
description: String,
contactInfo: {
address: String,
email: String,
workingHours: String,
phone: String,
},
},
// Menu
menu: [menuItemSchema],
// Logo
logo: {
light: String,
@@ -86,6 +9,18 @@ const headerSchema = new mongoose.Schema(
alt: String,
},
// Sign In Button
signInButton: {
label: {
type: String,
default: "Sign In",
},
href: {
type: String,
default: "/signin",
},
},
// CTA Button
ctaButton: {
label: String,
+61 -220
View File
@@ -3,49 +3,41 @@ const mongoose = require("mongoose");
const { Schema } = mongoose;
// Reusable small schemas
const LinkSchema = new Schema(
const FloatingBadgeSchema = new Schema(
{
icon: { type: String, default: "" },
value: { type: String, default: "" },
label: { type: String, default: "" },
href: { type: String, 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)
badge: { type: String, default: "" },
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: "" },
searchPlaceholder: { type: String, default: "" },
buttonLabel: { type: String, default: "" },
image: { type: String, default: "" },
imageAlt: { type: String, default: "" },
floatingBadge: { type: FloatingBadgeSchema, default: () => ({}) },
},
{ _id: false },
);
const WhyChooseUsItemSchema = new Schema(
const QuickLinkItemSchema = new Schema(
{
icon: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
linkText: { type: String, default: "" },
href: { type: String, default: "" },
},
{ _id: false },
);
const ValuePropFeatureSchema = new Schema(
{
icon: { type: String, default: "" },
title: { type: String, default: "" },
@@ -54,218 +46,68 @@ const WhyChooseUsItemSchema = new Schema(
{ _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(
const ValuePropStatSchema = new Schema(
{
value: { type: String, default: "" },
suffix: { type: String, default: "" },
label: { type: String, default: "" },
description: { type: String, default: "" },
image: { type: String, default: "" },
imageAlt: { 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(
const ValuePropSchema = new Schema(
{
badge: { type: String, default: "" },
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: "" },
description: { type: String, default: "" },
features: { type: [ValuePropFeatureSchema], default: [] },
stats: { type: [ValuePropStatSchema], default: [] },
},
{ _id: false },
);
const BlogPreviewSchema = new Schema(
const ProgramItemSchema = new Schema(
{
category: { type: String, default: "" },
image: { type: String, default: "" },
duration: { type: String, default: "" },
rating: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
studentCount: { type: String, default: "" },
href: { type: String, default: "" },
},
{ _id: false },
);
const ProgramsSchema = 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' }],
description: { type: String, default: "" },
items: { type: [ProgramItemSchema], default: [] },
},
{ _id: false },
);
const RequestInfoSchema = new Schema(
{
heading: { type: String, default: "" },
description: { type: String, default: "" },
phone: { type: String, default: "" },
email: { type: String, default: "" },
programs: { type: [String], default: [] },
},
{ _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: () => ({}) },
quickLinks: { type: [QuickLinkItemSchema], default: [] },
valueProp: { type: ValuePropSchema, default: () => ({}) },
programs: { type: ProgramsSchema, default: () => ({}) },
requestInfo: { type: RequestInfoSchema, default: () => ({}) },
},
{
timestamps: true,
@@ -274,4 +116,3 @@ const HomeSchema = new Schema(
);
module.exports = mongoose.model("Home", HomeSchema);
-328
View File
@@ -1,328 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Pricing) {
delete mongoose.models.Pricing;
}
if (mongoose.connection.models.Pricing) {
delete mongoose.connection.models.Pricing;
}
// Schema for breadcrumb item
const breadcrumbItemSchema = new mongoose.Schema(
{
text: {
type: String,
trim: true,
default: "",
},
link: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Pricing Plan",
},
backgroundImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/breadcrumb.jpg",
},
shapeImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/shape.png",
},
breadcrumb: {
type: [breadcrumbItemSchema],
default: [],
},
},
{ _id: false }
);
// Schema for pricing section header
const pricingSectionSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "pricing plan",
},
heading: {
type: String,
trim: true,
default: "Flexible Plans to Suit Every Traveler",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for individual plan
const planSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: true,
},
price: {
type: String,
trim: true,
default: "0",
},
period: {
type: String,
trim: true,
default: "mo",
},
currency: {
type: String,
trim: true,
default: "$",
},
buttonText: {
type: String,
trim: true,
default: "Get Started Today",
},
buttonLink: {
type: String,
trim: true,
default: "/pricing",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
style: {
type: String,
trim: true,
enum: ["default", "style-2"],
default: "default",
},
features: {
type: [String],
default: [],
},
},
{ _id: false }
);
// Schema for plans container
const plansSchema = new mongoose.Schema(
{
monthly: {
type: [planSchema],
default: [],
},
yearly: {
type: [planSchema],
default: [],
},
},
{ _id: false }
);
// Schema for testimonial item
const testimonialItemSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
default: "",
},
role: {
type: String,
trim: true,
default: "",
},
rating: {
type: Number,
min: 1,
max: 5,
default: 5,
},
content: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for testimonials section
const testimonialsSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "What Our Clients Say",
},
heading: {
type: String,
trim: true,
default: "Immigration Success Stories",
},
buttonText: {
type: String,
trim: true,
default: "View All Review",
},
buttonLink: {
type: String,
trim: true,
default: "/contact",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
image: {
type: String,
trim: true,
default: "",
},
items: {
type: [testimonialItemSchema],
default: [],
},
},
{ _id: false }
);
// Main Pricing Schema
const pricingSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
pricingSection: {
type: pricingSectionSchema,
default: () => ({}),
},
plans: {
type: plansSchema,
default: () => ({}),
},
testimonials: {
type: testimonialsSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
pricingSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default pricing exists
const existingPricing = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Pricing Plan",
backgroundImage: jsonData.hero?.backgroundImage || "/assets/img/inner-page/breadcrumb.jpg",
shapeImage: jsonData.hero?.shapeImage || "/assets/img/inner-page/shape.png",
breadcrumb: (jsonData.hero?.breadcrumb || []).map((item) => ({
text: item.text || "",
link: item.link || "",
})),
},
pricingSection: {
subtitle: jsonData.pricingSection?.subtitle || "pricing plan",
heading: jsonData.pricingSection?.heading || "Flexible Plans to Suit Every Traveler",
description: jsonData.pricingSection?.description || "",
},
plans: {
monthly: (jsonData.plans?.monthly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
yearly: (jsonData.plans?.yearly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
},
testimonials: {
subtitle: jsonData.testimonials?.subtitle || "What Our Clients Say",
heading: jsonData.testimonials?.heading || "Immigration Success Stories",
buttonText: jsonData.testimonials?.buttonText || "View All Review",
buttonLink: jsonData.testimonials?.buttonLink || "/contact",
buttonIcon: jsonData.testimonials?.buttonIcon || "fa-solid fa-arrow-right",
image: jsonData.testimonials?.image || "",
items: (jsonData.testimonials?.items || []).map((item) => ({
name: item.name || "",
role: item.role || "",
rating: item.rating || 5,
content: item.content || "",
})),
},
};
if (existingPricing) {
// Update existing pricing
existingPricing.hero = processedData.hero;
existingPricing.pricingSection = processedData.pricingSection;
existingPricing.plans = processedData.plans;
existingPricing.testimonials = processedData.testimonials;
await existingPricing.save();
console.log("Pricing data updated successfully");
return existingPricing;
} else {
// Create new pricing
const newPricing = await this.create({
name: "default",
...processedData,
});
console.log("Pricing data imported successfully");
return newPricing;
}
} catch (error) {
console.error("Error migrating pricing data:", error);
throw error;
}
};
module.exports = mongoose.model("Pricing", pricingSchema);
-118
View File
@@ -1,118 +0,0 @@
const mongoose = require("mongoose");
// Define sub-schemas first
const authorSchema = new mongoose.Schema(
{
name: String,
type: String,
},
{ _id: false },
);
const clientReviewSchema = new mongoose.Schema(
{
id: String,
rating: Number,
content: String,
author: authorSchema,
icon: String,
},
{ _id: false },
);
const featureSchema = new mongoose.Schema(
{
title: String,
description: String,
},
{ _id: false },
);
const faqSchema = new mongoose.Schema(
{
id: String,
question: String,
answer: String,
isExpanded: { type: Boolean, default: false },
},
{ _id: false },
);
const serviceDetailsSchema = new mongoose.Schema(
{
title: String,
description: String,
mainImage: String,
overviewTitle: String,
overviewDescription: String,
additionalDescription: String,
keyFeaturesTitle: String,
keyFeaturesImage: String,
features: [featureSchema],
faqTitle: String,
faqImage: String,
faq: [faqSchema],
},
{ _id: false },
);
// Main service page schema
const serviceSchema = new mongoose.Schema(
{
pageTitle: String,
// Main services section
services: {
title: {
subTitle: String,
mainTitle: String,
},
items: [
{
slug: String,
name: String,
description: String,
image: String,
layout: String,
details: serviceDetailsSchema,
},
],
},
// Destination countries section
destinations: {
backgroundImage: String,
title: {
subTitle: String,
mainTitle: String,
},
},
// Visa types section
visas: {
items: [
{
id: String,
number: String,
name: String,
description: String,
buttonText: String,
buttonLink: String,
},
],
},
// Client reviews section
reviews: {
title: {
subTitle: String,
mainTitle: String,
},
thumb: String,
items: [clientReviewSchema],
},
},
{ timestamps: true },
);
module.exports = mongoose.model("Service", serviceSchema);
-234
View File
@@ -1,234 +0,0 @@
// models/visa.js
const mongoose = require("mongoose");
// ==================== SCHEMAS ====================
// VisaItem Schema
const VisaItemSchema = new mongoose.Schema(
{
title: { type: String, default: "" },
description: { type: String, default: "" },
},
{ _id: false },
);
// VisaTypeCategory Schema
const VisaTypeCategorySchema = new mongoose.Schema(
{
category: { type: String, default: "" },
items: [VisaItemSchema],
},
{ _id: false },
);
// VisaProcessStep Schema
const VisaProcessStepSchema = new mongoose.Schema(
{
number: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
},
{ _id: false },
);
// VisaProcess Schema
const VisaProcessSchema = new mongoose.Schema(
{
title: { type: String, default: "" },
steps: [VisaProcessStepSchema],
},
{ _id: false },
);
// VisaCategory Schema
const VisaCategorySchema = new mongoose.Schema(
{
title: { type: String, default: "" },
steps: {
type: [[String]],
default: [],
},
},
{ _id: false },
);
// VisaService Schema
const VisaServiceSchema = new mongoose.Schema(
{
title: { type: String, default: "" },
steps: [VisaProcessStepSchema],
},
{ _id: false },
);
// RelatedCountry Schema
const RelatedCountrySchema = new mongoose.Schema(
{
id: { type: Number, default: 0 },
name: { type: String, default: "" },
icon: { type: String, default: "" },
},
{ _id: false },
);
// Phone Schema
const PhoneSchema = new mongoose.Schema(
{
label: { type: String, default: "" },
value: { type: String, default: "" },
link: { type: String, default: "" },
},
{ _id: false },
);
// Email Schema
const EmailSchema = new mongoose.Schema(
{
label: { type: String, default: "" },
value: { type: String, default: "" },
link: { type: String, default: "" },
},
{ _id: false },
);
// Location Schema
const LocationSchema = new mongoose.Schema(
{
label: { type: String, default: "" },
address: { type: String, default: "" },
},
{ _id: false },
);
// ContactInfo Schema
const ContactInfoSchema = new mongoose.Schema(
{
img: { type: String, default: "" },
sectionTitle: { type: String, default: "" },
helpText: { type: String, default: "" },
phone: {
type: PhoneSchema,
default: () => ({}),
},
email: {
type: EmailSchema,
default: () => ({}),
},
location: {
type: LocationSchema,
default: () => ({}),
},
},
{ _id: false },
);
// ActiveCountry Schema
const ActiveCountrySchema = new mongoose.Schema(
{
id: { type: Number, default: 0 },
name: { type: String, default: "" },
title: { type: String, default: "" },
mainImage: { type: String, default: "" },
description: { type: String, default: "" },
additionalInfo: { type: String, default: "" },
tagline: { type: String, default: "" },
visaTypes: [VisaTypeCategorySchema],
visaProcess: {
type: VisaProcessSchema,
default: null,
},
gallery: {
type: [String],
default: [],
},
visaCategories: {
type: VisaCategorySchema,
default: null,
},
visaService: {
type: VisaServiceSchema,
default: null,
},
},
{ _id: false },
);
// DetailedView Schema
const DetailedViewSchema = new mongoose.Schema(
{
activeCountry: {
type: ActiveCountrySchema,
default: null,
},
relatedCountries: {
type: [RelatedCountrySchema],
default: [],
},
contactInfo: {
type: ContactInfoSchema,
default: null,
},
},
{ _id: false },
);
// ==================== MAIN VISA COUNTRY SCHEMA ====================
// Main VisaCountry Schema (Individual country object)
const VisaCountrySchema = new mongoose.Schema(
{
// Không dùng `index: true` ở đây vì đã tạo index riêng cho hero.summaryList.* bên dưới
id: { type: Number, required: true },
name: { type: String, required: true },
slug: { type: String, required: true },
icon: { type: String, default: "" },
services: {
type: [String],
default: [],
},
detailedView: {
type: DetailedViewSchema,
default: null,
},
},
{ _id: false },
);
// ==================== HERO SCHEMA ====================
const HeroSchema = new mongoose.Schema(
{
title: { type: String, default: "Visa" },
summaryList: {
type: [VisaCountrySchema],
default: [],
},
},
{ _id: false },
);
// ==================== MAIN VISA SCHEMA ====================
const visaDataSchema = new mongoose.Schema(
{
hero: {
type: HeroSchema,
default: () => ({ title: "Visa", summaryList: [] }),
},
},
{
timestamps: true,
},
);
// ==================== INDEXES ====================
visaDataSchema.index({ "hero.summaryList.slug": 1 });
visaDataSchema.index({ "hero.summaryList.id": 1 });
visaDataSchema.index({ "hero.summaryList.name": 1 });
// ==================== MODEL ====================
module.exports = mongoose.models.Visa || mongoose.model("Visa", visaDataSchema);