forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
64
models/about.js
Normal file
64
models/about.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const aboutSchema = new mongoose.Schema({
|
||||
banner: {
|
||||
image: String,
|
||||
title: String,
|
||||
text: String
|
||||
},
|
||||
about: {
|
||||
title: String,
|
||||
paragraphs: [String],
|
||||
list_items: [String],
|
||||
button: {
|
||||
text: String,
|
||||
url: String
|
||||
},
|
||||
image: String,
|
||||
quote: {
|
||||
mark_image: String,
|
||||
title: String,
|
||||
text: String,
|
||||
author: String
|
||||
}
|
||||
},
|
||||
values: {
|
||||
background_image: String,
|
||||
items: [{
|
||||
icon: String,
|
||||
title: String,
|
||||
text: String
|
||||
}]
|
||||
},
|
||||
education: {
|
||||
images: {
|
||||
student1: String,
|
||||
student2: String
|
||||
},
|
||||
subtitle: String,
|
||||
title: String,
|
||||
text: String
|
||||
},
|
||||
advantages: {
|
||||
title: String,
|
||||
items: [{
|
||||
number: String,
|
||||
title: String,
|
||||
text: String
|
||||
}]
|
||||
},
|
||||
academic_board: {
|
||||
title: String,
|
||||
members: [{
|
||||
image: String,
|
||||
title: String,
|
||||
name: String,
|
||||
color: String
|
||||
}]
|
||||
},
|
||||
updatedAt: Date
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('About', aboutSchema);
|
||||
88
models/aboutUs.js
Normal file
88
models/aboutUs.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const aboutUsSchema = new mongoose.Schema(
|
||||
{
|
||||
// Hero section
|
||||
hero: {
|
||||
title: String,
|
||||
backgroundImage: String,
|
||||
},
|
||||
|
||||
// Introduction section with nested services
|
||||
introduction: {
|
||||
subtitle: String,
|
||||
title: String,
|
||||
description: String,
|
||||
mainImage: String,
|
||||
services: [
|
||||
{
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Statistics with nested items
|
||||
statistics: {
|
||||
items: [
|
||||
{
|
||||
number: String,
|
||||
description: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Accommodation section with nested features
|
||||
accommodation: {
|
||||
subtitle: String,
|
||||
title: String,
|
||||
description: String,
|
||||
features: [
|
||||
{
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Activities section with nested gallery
|
||||
activities: {
|
||||
subtitle: String,
|
||||
title: String,
|
||||
description: String,
|
||||
gallery: [
|
||||
{
|
||||
image: String,
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Newsletter
|
||||
newsletter: {
|
||||
imagePath: String,
|
||||
title: String,
|
||||
description: String,
|
||||
buttonText: String,
|
||||
},
|
||||
|
||||
// Events with nested items
|
||||
events: {
|
||||
title: String,
|
||||
items: [
|
||||
{
|
||||
imageUrl: String,
|
||||
date: String,
|
||||
title: String,
|
||||
description: String,
|
||||
age: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{timestamps: true}
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("AboutUs", aboutUsSchema);
|
||||
194
models/activity.js
Normal file
194
models/activity.js
Normal file
@@ -0,0 +1,194 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const activitySchema = new mongoose.Schema(
|
||||
{
|
||||
// Hero section for activity page header (supports Activities and Booking variants)
|
||||
hero: {
|
||||
titleActivities: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
titleBooking: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
bannerImageActivities: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
bannerImageBooking: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
priceText: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
season: [
|
||||
{
|
||||
type: String,
|
||||
enum: ["spring", "summer", "autumn", "winter"],
|
||||
},
|
||||
],
|
||||
age: {
|
||||
type: [Number],
|
||||
validate: {
|
||||
validator: function (v) {
|
||||
return v.length === 2 && v[0] <= v[1];
|
||||
},
|
||||
message: "Age must be an array of [minAge, maxAge]",
|
||||
},
|
||||
},
|
||||
locations: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
image: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
// Global filters document (single document in Activity collection)
|
||||
filters: [
|
||||
{
|
||||
label: { type: String, required: true, trim: true },
|
||||
value: { type: String, required: true, trim: true },
|
||||
items: [
|
||||
{
|
||||
value: { type: String, required: true },
|
||||
label: { type: String, required: true },
|
||||
},
|
||||
],
|
||||
order: { type: Number, default: 0 },
|
||||
},
|
||||
],
|
||||
program: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
default: 4,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// marker for the single document that stores global filters
|
||||
isFiltersDoc: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Rich camp details from camp-detail field in activities.json
|
||||
campDetail: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: {},
|
||||
},
|
||||
// Booking sessions - các đợt booking với thông số riêng
|
||||
bookingSessions: [
|
||||
{
|
||||
sessionId: { type: String, required: true },
|
||||
startDate: { type: Date, required: true },
|
||||
endDate: { type: Date, required: true },
|
||||
overnightStays: { type: Number, required: true, default: 14 },
|
||||
// Spots theo giới tính
|
||||
totalMaleSpots: { type: Number, default: 25 },
|
||||
totalFemaleSpots: { type: Number, default: 25 },
|
||||
bookedMaleSpots: { type: Number, default: 0 },
|
||||
bookedFemaleSpots: { type: Number, default: 0 },
|
||||
price: { type: Number },
|
||||
isActive: { type: Boolean, default: true },
|
||||
// Danh sách booking cho session này
|
||||
bookingList: [
|
||||
{
|
||||
address: { type: String, required: true },
|
||||
agreeNewsletter: { type: Boolean, default: false },
|
||||
agreeTerms: { type: Boolean, required: true },
|
||||
city: { type: String, required: true },
|
||||
country: { type: String, required: true },
|
||||
dietaryRestrictions: {
|
||||
type: String,
|
||||
enum: ['none', 'vegetarian', 'vegan', 'halal', 'kosher', 'gluten-free', 'other'],
|
||||
default: 'none'
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true
|
||||
},
|
||||
emergencyContact: { type: String, required: true },
|
||||
emergencyPhone: { type: String, required: true },
|
||||
medicalConditions: { type: String, default: '' },
|
||||
numberOfParticipants: { type: Number, required: true, min: 1 },
|
||||
parentFirstName: { type: String, required: true, trim: true },
|
||||
parentLastName: { type: String, required: true, trim: true },
|
||||
participantBirthDate: { type: Date, required: true },
|
||||
participantFirstName: { type: String, required: true, trim: true },
|
||||
participantGender: {
|
||||
type: String,
|
||||
enum: ['male', 'female', 'other'],
|
||||
required: true
|
||||
},
|
||||
participantLastName: { type: String, required: true, trim: true },
|
||||
phone: { type: String, required: true },
|
||||
postalCode: { type: String, required: true },
|
||||
sessionDate: { type: String, required: true }, // sessionId reference
|
||||
specialRequests: { type: String, default: '' },
|
||||
// Thêm các trường quản lý
|
||||
bookingStatus: {
|
||||
type: String,
|
||||
enum: ['pending', 'confirmed', 'cancelled', 'completed'],
|
||||
default: 'pending'
|
||||
},
|
||||
paymentStatus: {
|
||||
type: String,
|
||||
enum: ['pending', 'partial', 'paid', 'refunded'],
|
||||
default: 'pending'
|
||||
},
|
||||
totalAmount: { type: Number, default: 0 },
|
||||
paidAmount: { type: Number, default: 0 },
|
||||
bookingDate: { type: Date, default: Date.now },
|
||||
confirmationCode: { type: String, unique: true },
|
||||
adminNotes: { type: String, default: '' }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
{timestamps: true}
|
||||
);
|
||||
|
||||
// Add index for better query performance
|
||||
activitySchema.index({name: 1});
|
||||
activitySchema.index({isActive: 1, order: 1});
|
||||
activitySchema.index({season: 1});
|
||||
activitySchema.index({locations: 1});
|
||||
|
||||
module.exports = mongoose.model("Activity", activitySchema);
|
||||
106
models/booking.js
Normal file
106
models/booking.js
Normal file
@@ -0,0 +1,106 @@
|
||||
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
models/bookingSubmission.js
Normal file
200
models/bookingSubmission.js
Normal file
@@ -0,0 +1,200 @@
|
||||
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);
|
||||
245
models/campLocation.js
Normal file
245
models/campLocation.js
Normal file
@@ -0,0 +1,245 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Sub-schemas for better organization
|
||||
const programOptionSchema = new mongoose.Schema({
|
||||
_comment: String,
|
||||
href: String,
|
||||
imageSrc: String,
|
||||
title: String,
|
||||
description: String
|
||||
}, { _id: false });
|
||||
|
||||
const locationSchema = new mongoose.Schema({
|
||||
_comment: String,
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
country: String,
|
||||
title: String,
|
||||
imageSrc: String,
|
||||
imageAlt: String,
|
||||
readMoreLink: String,
|
||||
imagePosition: {
|
||||
type: String,
|
||||
enum: ['left', 'right']
|
||||
},
|
||||
cardSize: {
|
||||
type: String,
|
||||
enum: ['default', 'large']
|
||||
},
|
||||
scrollspyClass: String,
|
||||
programOptions: [programOptionSchema]
|
||||
}, { _id: false });
|
||||
|
||||
const campMarkerSchema = new mongoose.Schema({
|
||||
_comment: String,
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lat: {
|
||||
type: Number,
|
||||
required: false // Changed to false since coordinates are optional
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: false // Changed to false since coordinates are optional
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
image: String
|
||||
}, { _id: false });
|
||||
|
||||
const faqItemSchema = new mongoose.Schema({
|
||||
_comment: String,
|
||||
question: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
answer: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const securityItemSchema = new mongoose.Schema({
|
||||
_comment: String,
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Map schemas (similar to contact model)
|
||||
const coordinatesSchema = new mongoose.Schema({
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const tileLayerSchema = new mongoose.Schema({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
attribution: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
maxZoom: {
|
||||
type: Number,
|
||||
default: 18
|
||||
},
|
||||
minZoom: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const mapSchema = new mongoose.Schema({
|
||||
_comment: String,
|
||||
coordinates: {
|
||||
type: coordinatesSchema,
|
||||
required: true
|
||||
},
|
||||
zoom: {
|
||||
type: Number,
|
||||
default: 15
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
markerTitle: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
tileLayer: {
|
||||
type: tileLayerSchema,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Main schema
|
||||
const campLocationSchema = new mongoose.Schema({
|
||||
metadata: {
|
||||
_comment: String,
|
||||
title: String,
|
||||
description: String
|
||||
},
|
||||
hero: {
|
||||
_comment: String,
|
||||
title: String,
|
||||
backgroundImage: String,
|
||||
overlayColor: String
|
||||
},
|
||||
camps: [campMarkerSchema],
|
||||
locationsSection: {
|
||||
_comment: String,
|
||||
title: String,
|
||||
readMoreButtonText: {
|
||||
type: String,
|
||||
default: 'read more'
|
||||
}
|
||||
},
|
||||
locations: [locationSchema],
|
||||
intro: {
|
||||
_comment: String,
|
||||
content: String
|
||||
},
|
||||
map: {
|
||||
coordinates: {
|
||||
lat: { type: Number, default: 0 },
|
||||
lng: { type: Number, default: 0 }
|
||||
},
|
||||
zoom: { type: Number, default: 5 },
|
||||
location: { type: String, default: '' },
|
||||
markerTitle: { type: String, default: '' },
|
||||
tileLayer: {
|
||||
url: { type: String, default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
|
||||
attribution: { type: String, default: '' },
|
||||
maxZoom: { type: Number, default: 18 },
|
||||
minZoom: { type: Number, default: 0 }
|
||||
}
|
||||
},
|
||||
faqSection: {
|
||||
_comment: String,
|
||||
title: String,
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: 'More questions'
|
||||
},
|
||||
buttonIcon: {
|
||||
type: String,
|
||||
default: 'comments'
|
||||
},
|
||||
buttonLink: {
|
||||
type: String,
|
||||
default: '/info/faq'
|
||||
}
|
||||
},
|
||||
faq: [faqItemSchema],
|
||||
welcomeQuote: {
|
||||
_comment: String,
|
||||
title: String,
|
||||
quote: String,
|
||||
author: String
|
||||
},
|
||||
securityConcept: {
|
||||
_comment: String,
|
||||
title: String,
|
||||
introduction: String,
|
||||
items: [securityItemSchema]
|
||||
},
|
||||
updatedAt: Date
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes for better query performance
|
||||
campLocationSchema.index({ 'camps.id': 1 });
|
||||
campLocationSchema.index({ 'locations.id': 1 });
|
||||
campLocationSchema.index({ 'locations.country': 1 });
|
||||
|
||||
// Virtual to get camp by ID
|
||||
campLocationSchema.virtual('getCampById').get(function() {
|
||||
return (id) => this.camps.find(camp => camp.id === id);
|
||||
});
|
||||
|
||||
// Virtual to get location by ID
|
||||
campLocationSchema.virtual('getLocationById').get(function() {
|
||||
return (id) => this.locations.find(location => location.id === id);
|
||||
});
|
||||
|
||||
// Method to get locations by country
|
||||
campLocationSchema.methods.getLocationsByCountry = function(country) {
|
||||
return this.locations.filter(location => location.country === country);
|
||||
};
|
||||
|
||||
// Method to get all countries
|
||||
campLocationSchema.methods.getAllCountries = function() {
|
||||
return [...new Set(this.locations.map(location => location.country))];
|
||||
};
|
||||
|
||||
// Static method to get the latest camp location data
|
||||
campLocationSchema.statics.getLatest = function() {
|
||||
return this.findOne().sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('CampLocation', campLocationSchema);
|
||||
387
models/contact.js
Normal file
387
models/contact.js
Normal file
@@ -0,0 +1,387 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho hero section
|
||||
const heroSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
overlayColor: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
enableScrollspy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
backgroundPosition: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "center",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho contact card
|
||||
const contactCardSchema = new mongoose.Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
enum: [
|
||||
"phone",
|
||||
"email",
|
||||
"location",
|
||||
"hours",
|
||||
"website",
|
||||
"social",
|
||||
"custom",
|
||||
],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
content: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
iconType: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
iconSource: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
enum: ["fontawesome", "image"],
|
||||
default: "fontawesome",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho map coordinates
|
||||
const coordinatesSchema = new mongoose.Schema(
|
||||
{
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho tile layer
|
||||
const tileLayerSchema = new mongoose.Schema(
|
||||
{
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
attribution: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
maxZoom: {
|
||||
type: Number,
|
||||
default: 18,
|
||||
},
|
||||
minZoom: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho map
|
||||
const mapSchema = new mongoose.Schema(
|
||||
{
|
||||
coordinates: {
|
||||
type: coordinatesSchema,
|
||||
required: true,
|
||||
},
|
||||
zoom: {
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
markerTitle: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
tileLayer: {
|
||||
type: tileLayerSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho form field
|
||||
const formFieldSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
enum: ["text", "email", "tel", "textarea", "programme"],
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
programmeName: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho submit button
|
||||
const submitButtonSchema = new mongoose.Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho form
|
||||
const formSchema = new mongoose.Schema(
|
||||
{
|
||||
sectionLabel: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
heading: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
fields: {
|
||||
type: [formFieldSchema],
|
||||
default: [],
|
||||
},
|
||||
submitButton: {
|
||||
type: submitButtonSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Main Contact Schema
|
||||
const contactSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
default: "default",
|
||||
unique: true,
|
||||
},
|
||||
hero: {
|
||||
type: heroSchema,
|
||||
required: true,
|
||||
},
|
||||
contactCards: {
|
||||
type: [contactCardSchema],
|
||||
default: [],
|
||||
},
|
||||
map: {
|
||||
type: mapSchema,
|
||||
required: true,
|
||||
},
|
||||
form: {
|
||||
type: formSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Mapping iconType cũ sang Font Awesome icon mới
|
||||
const iconTypeMapping = {
|
||||
phone: "fas fa-phone",
|
||||
email: "fas fa-envelope",
|
||||
location: "fas fa-map-marker-alt",
|
||||
clock: "fas fa-clock",
|
||||
hours: "fas fa-clock",
|
||||
};
|
||||
|
||||
// Tạo migration script để import dữ liệu từ contact-data.json
|
||||
contactSchema.statics.migrateFromJson = async function (jsonData) {
|
||||
try {
|
||||
// Kiểm tra xem đã có contact mặc định chưa
|
||||
const existingContact = await this.findOne({ name: "default" });
|
||||
|
||||
// Xử lý và chuẩn hóa dữ liệu từ JSON
|
||||
const processedData = {
|
||||
hero: {
|
||||
title: jsonData.hero?.title || "Contact Us",
|
||||
backgroundImage: jsonData.hero?.backgroundImage || "",
|
||||
overlayColor: jsonData.hero?.overlayColor || "rgba(0, 0, 0, 0)",
|
||||
sectionClass: jsonData.hero?.sectionClass || "",
|
||||
titleClass: jsonData.hero?.titleClass || "",
|
||||
enableScrollspy: jsonData.hero?.enableScrollspy || false,
|
||||
backgroundPosition: jsonData.hero?.backgroundPosition || "center",
|
||||
},
|
||||
contactCards: (jsonData.contactCards || []).map((card) => {
|
||||
let iconType = card.iconType || "";
|
||||
let iconSource = card.iconSource;
|
||||
|
||||
// Nếu không có iconSource, tự động detect từ iconType
|
||||
if (!iconSource) {
|
||||
// Nếu iconType là image path (bắt đầu bằng /uploads/ hoặc http)
|
||||
if (
|
||||
iconType.startsWith("/uploads/") ||
|
||||
iconType.startsWith("http://") ||
|
||||
iconType.startsWith("https://")
|
||||
) {
|
||||
iconSource = "image";
|
||||
} else {
|
||||
// Nếu iconType là string cũ (phone, email, location, clock)
|
||||
iconSource = "fontawesome";
|
||||
// Map iconType cũ sang Font Awesome icon mới
|
||||
if (iconTypeMapping[iconType]) {
|
||||
iconType = iconTypeMapping[iconType];
|
||||
} else if (
|
||||
iconType &&
|
||||
!iconType.startsWith("fas ") &&
|
||||
!iconType.startsWith("fab ")
|
||||
) {
|
||||
// Nếu iconType không phải là Font Awesome class hợp lệ, thử map
|
||||
iconType = iconTypeMapping[iconType] || iconType;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nếu đã có iconSource nhưng iconType là string cũ, map sang Font Awesome
|
||||
if (
|
||||
iconSource === "fontawesome" &&
|
||||
iconType &&
|
||||
!iconType.startsWith("fas ") &&
|
||||
!iconType.startsWith("fab ") &&
|
||||
iconTypeMapping[iconType]
|
||||
) {
|
||||
iconType = iconTypeMapping[iconType];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: card.type || "custom",
|
||||
title: card.title || "",
|
||||
content: Array.isArray(card.content) ? card.content : [],
|
||||
iconType: iconType,
|
||||
iconSource: iconSource || "fontawesome",
|
||||
};
|
||||
}),
|
||||
map: {
|
||||
coordinates: {
|
||||
lat: jsonData.map?.coordinates?.lat || 0,
|
||||
lng: jsonData.map?.coordinates?.lng || 0,
|
||||
},
|
||||
zoom: jsonData.map?.zoom || 15,
|
||||
location: jsonData.map?.location || "",
|
||||
markerTitle: jsonData.map?.markerTitle || "",
|
||||
tileLayer: {
|
||||
url:
|
||||
jsonData.map?.tileLayer?.url ||
|
||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: jsonData.map?.tileLayer?.attribution || "",
|
||||
maxZoom: jsonData.map?.tileLayer?.maxZoom || 18,
|
||||
minZoom: jsonData.map?.tileLayer?.minZoom || 0,
|
||||
},
|
||||
},
|
||||
form: {
|
||||
sectionLabel: jsonData.form?.sectionLabel || "",
|
||||
heading: jsonData.form?.heading || "",
|
||||
fields: (jsonData.form?.fields || []).map((field) => ({
|
||||
name: field.name || "",
|
||||
type: field.type || "text",
|
||||
placeholder: field.placeholder || "",
|
||||
required: field.required || false,
|
||||
programmeName: field.programmeName || "",
|
||||
})),
|
||||
submitButton: {
|
||||
text: jsonData.form?.submitButton?.text || "Send Message",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (existingContact) {
|
||||
// Cập nhật contact hiện có với dữ liệu đã xử lý
|
||||
existingContact.hero = processedData.hero;
|
||||
existingContact.contactCards = processedData.contactCards;
|
||||
existingContact.map = processedData.map;
|
||||
existingContact.form = processedData.form;
|
||||
await existingContact.save();
|
||||
console.log("Contact data updated successfully");
|
||||
return existingContact;
|
||||
} else {
|
||||
// Tạo contact mới với dữ liệu đã xử lý
|
||||
const newContact = await this.create({
|
||||
name: "default",
|
||||
...processedData,
|
||||
});
|
||||
console.log("Contact data imported successfully");
|
||||
return newContact;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error migrating contact data:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("Contact", contactSchema);
|
||||
32
models/department.js
Normal file
32
models/department.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Helper function to generate slug
|
||||
function generateSlug(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/-+/g, '-'); // Replace multiple - with single -
|
||||
}
|
||||
|
||||
const departmentSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
unique: true,
|
||||
lowercase: true
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
index: true
|
||||
}
|
||||
}, {
|
||||
timestamps: false // Thêm timestamps để theo dõi thời gian tạo/cập nhật
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Department', departmentSchema);
|
||||
222
models/faq.js
Normal file
222
models/faq.js
Normal file
@@ -0,0 +1,222 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const faqItemSchema = new mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}, { _id: true });
|
||||
|
||||
const faqSectionSchema = new mongoose.Schema({
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
faqs: [faqItemSchema]
|
||||
}, { _id: true });
|
||||
|
||||
const sidebarNavSchema = new mongoose.Schema({
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const heroSchema = new mongoose.Schema({
|
||||
title: String,
|
||||
backgroundImage: String,
|
||||
overlayColor: String,
|
||||
sectionClass: String,
|
||||
titleClass: String,
|
||||
enableScrollspy: Boolean,
|
||||
backgroundPosition: String
|
||||
});
|
||||
|
||||
const contactBoxSchema = new mongoose.Schema({
|
||||
title: String,
|
||||
phone: {
|
||||
icon: String,
|
||||
text: String
|
||||
},
|
||||
email: {
|
||||
icon: String,
|
||||
text: String
|
||||
}
|
||||
});
|
||||
|
||||
const videoSchema = new mongoose.Schema({
|
||||
url: String,
|
||||
title: String
|
||||
});
|
||||
|
||||
const faqSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
|
||||
},
|
||||
hero: heroSchema,
|
||||
sidebarNav: [sidebarNavSchema],
|
||||
contactBox: contactBoxSchema,
|
||||
faqSections: [faqSectionSchema],
|
||||
video: videoSchema
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Virtual property để tính tổng số FAQ items
|
||||
faqSchema.virtual('totalFaqs').get(function() {
|
||||
return this.faqSections.reduce((total, section) => {
|
||||
return total + (section.faqs ? section.faqs.length : 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Static method: Lấy FAQ mặc định
|
||||
faqSchema.statics.getDefault = async function() {
|
||||
let faq = await this.findOne({ name: 'default' });
|
||||
|
||||
// Nếu không có, tạo mới
|
||||
if (!faq) {
|
||||
faq = new this({
|
||||
name: 'default',
|
||||
hero: {
|
||||
title: 'Frequently Asked Questions',
|
||||
backgroundImage: 'yootheme/cache/18/faqs_header_new.jpg',
|
||||
overlayColor: 'rgba(0, 0, 0, 0)',
|
||||
sectionClass: 'uk-section-secondary uk-section-overlap uk-preserve-color uk-light',
|
||||
titleClass: 'uk-heading-large uk-text-center !text-[5vw]',
|
||||
enableScrollspy: true,
|
||||
backgroundPosition: 'top-center'
|
||||
},
|
||||
sidebarNav: [
|
||||
{ id: 'general-information', label: 'General Information' }
|
||||
],
|
||||
contactBox: {
|
||||
title: 'Let\'s plan your perfect nature escape',
|
||||
phone: { icon: 'phone', text: '+(123)-456-789' },
|
||||
email: { icon: 'email', text: 'hello@ggcamp.org' }
|
||||
},
|
||||
faqSections: [
|
||||
{
|
||||
id: 'general-information',
|
||||
title: 'General Information',
|
||||
faqs: [
|
||||
{
|
||||
title: 'Sample FAQ Question',
|
||||
description: 'This is a sample FAQ answer. Please update with your actual content.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
await faq.save();
|
||||
}
|
||||
|
||||
return faq;
|
||||
};
|
||||
|
||||
// Static method: Import từ JSON
|
||||
faqSchema.statics.importFromJson = async function(data) {
|
||||
let faq = await this.findOne({ name: 'default' });
|
||||
|
||||
// Đảm bảo có name
|
||||
const faqData = {
|
||||
name: 'default',
|
||||
...data
|
||||
};
|
||||
|
||||
if (!faq) {
|
||||
faq = new this(faqData);
|
||||
} else {
|
||||
// Update các trường
|
||||
Object.keys(faqData).forEach(key => {
|
||||
faq[key] = faqData[key];
|
||||
});
|
||||
}
|
||||
|
||||
await faq.save();
|
||||
return faq;
|
||||
};
|
||||
|
||||
// Method: Thêm FAQ vào section
|
||||
faqSchema.methods.addFaqToSection = async function(sectionId, faqItem) {
|
||||
const section = this.faqSections.find(s => s.id === sectionId);
|
||||
|
||||
if (!section) {
|
||||
throw new Error(`Section with id ${sectionId} not found`);
|
||||
}
|
||||
|
||||
section.faqs.push(faqItem);
|
||||
await this.save();
|
||||
return this;
|
||||
};
|
||||
|
||||
// Method: Update FAQ item
|
||||
faqSchema.methods.updateFaqItem = async function(sectionId, faqId, updates) {
|
||||
const section = this.faqSections.find(s => s.id === sectionId);
|
||||
|
||||
if (!section) {
|
||||
throw new Error(`Section with id ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const faqItem = section.faqs.id(faqId);
|
||||
|
||||
if (!faqItem) {
|
||||
throw new Error(`FAQ item with id ${faqId} not found in section ${sectionId}`);
|
||||
}
|
||||
|
||||
if (updates.title !== undefined) faqItem.title = updates.title;
|
||||
if (updates.description !== undefined) faqItem.description = updates.description;
|
||||
|
||||
await this.save();
|
||||
return this;
|
||||
};
|
||||
|
||||
// Method: Delete FAQ item
|
||||
faqSchema.methods.deleteFaqItem = async function(sectionId, faqId) {
|
||||
const section = this.faqSections.find(s => s.id === sectionId);
|
||||
|
||||
if (!section) {
|
||||
throw new Error(`Section with id ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const faqItem = section.faqs.id(faqId);
|
||||
|
||||
if (!faqItem) {
|
||||
throw new Error(`FAQ item with id ${faqId} not found in section ${sectionId}`);
|
||||
}
|
||||
|
||||
section.faqs.pull(faqId);
|
||||
await this.save();
|
||||
return this;
|
||||
};
|
||||
|
||||
// Method: Update FAQ section
|
||||
faqSchema.methods.updateFaqSection = async function(sectionId, updates) {
|
||||
const section = this.faqSections.find(s => s.id === sectionId);
|
||||
|
||||
if (!section) {
|
||||
throw new Error(`Section with id ${sectionId} not found`);
|
||||
}
|
||||
|
||||
if (updates.title !== undefined) section.title = updates.title;
|
||||
|
||||
await this.save();
|
||||
return this;
|
||||
};
|
||||
|
||||
const FAQ = mongoose.model('FAQ', faqSchema);
|
||||
|
||||
module.exports = FAQ;
|
||||
234
models/footer.js
Normal file
234
models/footer.js
Normal file
@@ -0,0 +1,234 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho social links
|
||||
const socialLinkSchema = new mongoose.Schema(
|
||||
{
|
||||
platform: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho footer links
|
||||
const footerLinkSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho footer columns
|
||||
const footerColumnSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
links: {
|
||||
type: [footerLinkSchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho logo
|
||||
const logoSchema = new mongoose.Schema(
|
||||
{
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho address
|
||||
const addressSchema = new mongoose.Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
// Thêm address2 (địa chỉ dòng 2) không bắt buộc
|
||||
address2: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
// Optional secondary link (e.g., a second map or external resource)
|
||||
link2: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
mapUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho contact info
|
||||
const contactInfoSchema = new mongoose.Schema(
|
||||
{
|
||||
phone: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
hours: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho copyright
|
||||
const copyrightSchema = new mongoose.Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho about section (column đầu tiên)
|
||||
const aboutSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
mapLink: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Main Footer Schema
|
||||
const footerSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
default: "default",
|
||||
unique: true,
|
||||
},
|
||||
about: {
|
||||
type: aboutSchema,
|
||||
required: true,
|
||||
},
|
||||
address: {
|
||||
type: addressSchema,
|
||||
required: true,
|
||||
},
|
||||
contact: {
|
||||
type: contactInfoSchema,
|
||||
required: true,
|
||||
},
|
||||
columns: {
|
||||
type: [footerColumnSchema],
|
||||
default: [],
|
||||
},
|
||||
social: {
|
||||
links: {
|
||||
type: [socialLinkSchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
copyright: {
|
||||
type: copyrightSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Tạo migration script để import dữ liệu từ footer.json
|
||||
footerSchema.statics.migrateFromJson = async function (jsonData) {
|
||||
try {
|
||||
// Kiểm tra xem đã có footer mặc định chưa
|
||||
const existingFooter = await this.findOne({ name: "default" });
|
||||
|
||||
if (existingFooter) {
|
||||
// Cập nhật footer hiện có
|
||||
Object.assign(existingFooter, jsonData);
|
||||
await existingFooter.save();
|
||||
console.log("Footer data updated successfully");
|
||||
return existingFooter;
|
||||
} else {
|
||||
// Tạo footer mới với dữ liệu từ JSON
|
||||
const newFooter = await this.create({
|
||||
name: "default",
|
||||
...jsonData,
|
||||
});
|
||||
console.log("Footer data imported successfully");
|
||||
return newFooter;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error migrating footer data:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("Footer", footerSchema);
|
||||
51
models/form.js
Normal file
51
models/form.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const formSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
unique: true
|
||||
},
|
||||
admission: {
|
||||
background_image: String,
|
||||
title: String,
|
||||
year: String,
|
||||
description: String,
|
||||
form: {
|
||||
fields: [{
|
||||
type: { type: String },
|
||||
placeholder: String
|
||||
}],
|
||||
button: {
|
||||
text: String,
|
||||
url: String
|
||||
}
|
||||
}
|
||||
},
|
||||
apply: {
|
||||
title: String,
|
||||
steps: [{
|
||||
title: String,
|
||||
description: String
|
||||
}]
|
||||
},
|
||||
application_form: {
|
||||
title: String,
|
||||
question: String,
|
||||
button: {
|
||||
text: String,
|
||||
icon: String,
|
||||
url: String
|
||||
},
|
||||
links: [{
|
||||
text: String,
|
||||
url: String
|
||||
}]
|
||||
},
|
||||
updatedAt: Date
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Form', formSchema);
|
||||
129
models/header.js
Normal file
129
models/header.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Schema cho các link trong topbar
|
||||
const topbarLinkSchema = new mongoose.Schema({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema cho contact info trong topbar
|
||||
const contactInfoSchema = new mongoose.Schema({
|
||||
phone: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema cho topbar
|
||||
const topbarSchema = new mongoose.Schema({
|
||||
contactInfo: {
|
||||
type: contactInfoSchema,
|
||||
required: true
|
||||
},
|
||||
links: {
|
||||
type: [topbarLinkSchema],
|
||||
default: []
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Main Header Schema
|
||||
const headerSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
unique: true
|
||||
},
|
||||
topbar: {
|
||||
type: topbarSchema,
|
||||
required: true
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Method để lấy menu tree từ collection menuHeader
|
||||
headerSchema.statics.getMenuTree = async function() {
|
||||
try {
|
||||
const Menu = require('./menuHeader');
|
||||
return await Menu.getMenuTree();
|
||||
} catch (error) {
|
||||
console.error('Error getting menu tree:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để lấy menu tree với programmes từ collection menuHeader
|
||||
headerSchema.statics.getMenuTreeWithProgrammes = async function() {
|
||||
try {
|
||||
const Menu = require('./menuHeader');
|
||||
return await Menu.getMenuTreeWithProgrammes();
|
||||
} catch (error) {
|
||||
console.error('Error getting menu tree with programmes:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để lấy programmes theo menu ID
|
||||
headerSchema.statics.getProgrammesByMenuId = async function(menuId) {
|
||||
try {
|
||||
const Menu = require('./menuHeader');
|
||||
return await Menu.getProgrammesByMenuId(menuId);
|
||||
} catch (error) {
|
||||
console.error('Error getting programmes by menu ID:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Tạo migration script để import dữ liệu từ menu.json
|
||||
headerSchema.statics.migrateFromJson = async function(jsonData) {
|
||||
try {
|
||||
// Kiểm tra xem đã có header mặc định chưa
|
||||
const existingHeader = await this.findOne({ name: 'default' });
|
||||
|
||||
// Chỉ giữ lại topbar và logo, bỏ search và mainMenu
|
||||
const headerData = {
|
||||
topbar: jsonData.topbar,
|
||||
logo: jsonData.logo
|
||||
};
|
||||
|
||||
if (existingHeader) {
|
||||
// Cập nhật header hiện có
|
||||
Object.assign(existingHeader, headerData);
|
||||
await existingHeader.save();
|
||||
console.log('Header data updated successfully');
|
||||
return existingHeader;
|
||||
} else {
|
||||
// Tạo header mới với dữ liệu từ JSON
|
||||
const newHeader = await this.create({
|
||||
name: 'default',
|
||||
...headerData
|
||||
});
|
||||
console.log('Header data imported successfully');
|
||||
return newHeader;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error migrating header data:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Header', headerSchema);
|
||||
96
models/hero.js
Normal file
96
models/hero.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const heroSchema = new mongoose.Schema({
|
||||
hero: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
overlayColor: {
|
||||
type: String,
|
||||
default: 'rgba(0, 0, 0, 0.35)'
|
||||
},
|
||||
enableScrollspy: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
backgroundPosition: {
|
||||
type: String,
|
||||
default: 'center center'
|
||||
},
|
||||
containerStyles: {
|
||||
width: { type: String, default: '98%' },
|
||||
height: { type: String, default: '130vh' },
|
||||
margin: { type: String, default: '0 auto' },
|
||||
borderRadius: { type: String, default: '2vw' },
|
||||
overflow: { type: String, default: 'hidden' },
|
||||
position: { type: String, default: 'relative' },
|
||||
top: { type: String, default: '-10vh' }
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
default: 'uk-heading-large uk-text-center uk-text-white'
|
||||
},
|
||||
titleStyles: {
|
||||
fontSize: { type: String, default: 'clamp(2rem, 5vw, 4.5rem)' },
|
||||
fontWeight: { type: String, default: 'bold' },
|
||||
lineHeight: { type: String, default: '1.2' },
|
||||
marginBottom: { type: String, default: '1.5rem' },
|
||||
color: { type: String, default: 'white' },
|
||||
textShadow: { type: String, default: '0 2px 10px rgba(0, 0, 0, 0.3)' }
|
||||
},
|
||||
descriptionClass: {
|
||||
type: String,
|
||||
default: 'uk-text-white'
|
||||
},
|
||||
descriptionStyles: {
|
||||
fontSize: { type: String, default: 'clamp(1rem, 1.5vw, 1.25rem)' },
|
||||
maxWidth: { type: String, default: '800px' },
|
||||
margin: { type: String, default: '0 auto 2rem' },
|
||||
lineHeight: { type: String, default: '1.6' },
|
||||
color: { type: String, default: 'white' },
|
||||
textShadow: { type: String, default: '0 1px 5px rgba(0, 0, 0, 0.3)' }
|
||||
}
|
||||
},
|
||||
button: {
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Book Your Adventure'
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: '/booking'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'magic'
|
||||
}
|
||||
},
|
||||
contactBox: {
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
position: {
|
||||
position: { type: String, default: 'absolute' },
|
||||
bottom: { type: String, default: '3rem' },
|
||||
left: { type: String, default: '50%' },
|
||||
transform: { type: String, default: 'translateX(-50%)' },
|
||||
width: { type: String, default: '100%' },
|
||||
zIndex: { type: Number, default: 3 },
|
||||
padding: { type: String, default: '0 1rem' }
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Hero', heroSchema);
|
||||
177
models/home.js
Normal file
177
models/home.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const homeSchema = new mongoose.Schema({
|
||||
// New structure - Camp data
|
||||
hero: {
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
backgroundImage: { type: String, default: '' },
|
||||
button: {
|
||||
label: { type: String, default: 'Book Your Adventure' },
|
||||
href: { type: String, default: '/booking' }
|
||||
},
|
||||
contactBox: {
|
||||
welcomeText: { type: String, default: '' },
|
||||
phone: {
|
||||
label: { type: String, default: 'Call us' },
|
||||
number: { type: String, default: '' },
|
||||
href: { type: String, default: '' }
|
||||
},
|
||||
email: {
|
||||
label: { type: String, default: 'Email' },
|
||||
address: { type: String, default: '' },
|
||||
href: { type: String, default: '' }
|
||||
},
|
||||
workingHours: {
|
||||
label: { type: String, default: 'Working Hours' },
|
||||
hours: { type: String, default: '' }
|
||||
}
|
||||
}
|
||||
},
|
||||
about: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
images: {
|
||||
mainImage1: { type: String, default: '' },
|
||||
mainImage2: { type: String, default: '' },
|
||||
avatars: [{ type: String }]
|
||||
},
|
||||
features: [{ type: String }],
|
||||
quote: { type: String, default: '' },
|
||||
button: {
|
||||
label: { type: String, default: '' },
|
||||
href: { type: String, default: '' }
|
||||
},
|
||||
stats: {
|
||||
customerCount: { type: Number, default: 0 },
|
||||
customerLabel: { type: String, default: '' }
|
||||
}
|
||||
},
|
||||
missionVision: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
backgroundImage: { type: String, default: '' },
|
||||
cards: [{
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' }
|
||||
}]
|
||||
},
|
||||
whyChooseUs: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
button: {
|
||||
label: { type: String, default: '' },
|
||||
href: { type: String, default: '' }
|
||||
},
|
||||
features: [{
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' }
|
||||
}],
|
||||
tags: [{ type: String }],
|
||||
cta: {
|
||||
text: { type: String, default: '' },
|
||||
linkText: { type: String, default: '' },
|
||||
linkHref: { type: String, default: '' }
|
||||
}
|
||||
},
|
||||
activities: {
|
||||
cards: [{
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
image: { type: String, default: '' }
|
||||
}]
|
||||
},
|
||||
faq: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
image: { type: String, default: '' },
|
||||
contact: {
|
||||
title: { type: String, default: '' },
|
||||
info: { type: String, default: '' }
|
||||
},
|
||||
questions: [{
|
||||
question: { type: String, default: '' },
|
||||
answer: { type: String, default: '' }
|
||||
}]
|
||||
},
|
||||
partners: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
backgroundImage: { type: String, default: '' },
|
||||
logos: [{ type: String }],
|
||||
cta: {
|
||||
badge: { type: String, default: '' },
|
||||
text: { type: String, default: '' },
|
||||
linkText: { type: String, default: '' },
|
||||
linkHref: { type: String, default: '' }
|
||||
}
|
||||
},
|
||||
programs: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
button: {
|
||||
label: { type: String, default: '' },
|
||||
href: { type: String, default: '' }
|
||||
},
|
||||
card: {
|
||||
pricePrefix: { type: String, default: 'from' },
|
||||
priceSuffix: { type: String, default: 'USD' },
|
||||
buttonLabel: { type: String, default: 'Camp Detail' },
|
||||
buttonHref: { type: String, default: '/camp-profiles' }
|
||||
},
|
||||
items: [{
|
||||
id: { type: String, default: '' },
|
||||
title: { type: String, default: '' },
|
||||
price: { type: String, default: '' },
|
||||
seasons: [{ type: String }],
|
||||
age: { type: String, default: '' },
|
||||
location: { type: String, default: '' },
|
||||
image: { type: String, default: '' },
|
||||
slug: { type: String, default: '' }
|
||||
}]
|
||||
},
|
||||
|
||||
newsletter: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
image: { type: String, default: '' },
|
||||
decorativeImage: { type: String, default: '' },
|
||||
button: {
|
||||
label: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
href: { type: String, default: '' }
|
||||
}
|
||||
},
|
||||
latestPosts: {
|
||||
title: { type: String, default: '' },
|
||||
subtitle: { type: String, default: '' },
|
||||
searchPlaceholder: { type: String, default: '' },
|
||||
sidebarTitle: { type: String, default: '' },
|
||||
blogPosts: [{
|
||||
id: { type: Number },
|
||||
image: { type: String, default: '' },
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
date: { type: String, default: '' }
|
||||
}],
|
||||
sidebarPosts: [{
|
||||
id: { type: Number },
|
||||
image: { type: String, default: '' },
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' }
|
||||
}],
|
||||
featuredCard: {
|
||||
image: { type: String, default: '' },
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' }
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Home', homeSchema);
|
||||
302
models/insurance.js
Normal file
302
models/insurance.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho content items
|
||||
const contentItemSchema = new mongoose.Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["paragraph", "section", "list", "note", "embed", "header"],
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
items: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
// Embed/video fields
|
||||
embed: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
videoId: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho overlay style
|
||||
const overlayStyleSchema = new mongoose.Schema(
|
||||
{
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho hero section
|
||||
const heroSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
default: "Insurance & Travel Cancellation Guarantee",
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "Comprehensive coverage for your peace of mind",
|
||||
},
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "/uploads/banner/b13.jpg",
|
||||
},
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
},
|
||||
backgroundClasses: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
},
|
||||
overlayStyle: {
|
||||
type: overlayStyleSchema,
|
||||
default: () => ({ backgroundColor: "rgba(0, 0, 0, 0)" }),
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "text-white text-[5vw] uk-text-center",
|
||||
},
|
||||
subtitleClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
|
||||
},
|
||||
enableScrollspy: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho page section
|
||||
const pageSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
default: "Insurance & Travel Information",
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-section-default uk-section-overlap uk-section",
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
},
|
||||
dividerClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-divider-small uk-text-left@m uk-text-center",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho content section
|
||||
const contentSchema = new mongoose.Schema(
|
||||
{
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-section-muted uk-section-overlap uk-section",
|
||||
},
|
||||
textClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-panel uk-margin text-[1vw]",
|
||||
},
|
||||
content: {
|
||||
type: [contentItemSchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Main Insurance Schema - CẤU TRÚC MỚI
|
||||
const insuranceSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
default: "default",
|
||||
unique: true,
|
||||
},
|
||||
// 3 PHẦN CHÍNH
|
||||
hero: {
|
||||
type: heroSchema,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: pageSchema,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: contentSchema,
|
||||
required: true,
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
version: {
|
||||
type: String,
|
||||
default: "2.0.0",
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
migratedFromOldStructure: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Static method: Lấy insurance default
|
||||
insuranceSchema.statics.getDefault = async function(language = "en") {
|
||||
try {
|
||||
let insurance = await this.findOne({ name: "default", language: language });
|
||||
|
||||
if (!insurance) {
|
||||
// Tạo default data nếu chưa có
|
||||
insurance = await this.create({
|
||||
name: "default",
|
||||
language: language,
|
||||
hero: {
|
||||
title: "Insurance & Travel Cancellation Guarantee",
|
||||
subtitle: "Comprehensive coverage for your peace of mind",
|
||||
backgroundImage: "/uploads/banner/b13.jpg",
|
||||
},
|
||||
page: {
|
||||
title: "Insurance & Travel Information",
|
||||
divider: true,
|
||||
},
|
||||
content: {
|
||||
content: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return insurance;
|
||||
} catch (error) {
|
||||
console.error("Error in getDefault:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để get insurance data
|
||||
insuranceSchema.methods.getInsuranceData = function() {
|
||||
return this.toObject();
|
||||
};
|
||||
|
||||
// Migration method - chỉ hỗ trợ cấu trúc mới
|
||||
insuranceSchema.statics.migrateFromJson = async function(jsonData, language = "en") {
|
||||
try {
|
||||
console.log('Migrating insurance from JSON...');
|
||||
|
||||
// Xóa document cũ nếu có
|
||||
await this.deleteOne({ name: "default", language: language });
|
||||
|
||||
// Sử dụng dữ liệu từ JSON trực tiếp
|
||||
const processedData = {
|
||||
name: "default",
|
||||
language: language,
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
hero: jsonData.hero,
|
||||
page: jsonData.page,
|
||||
content: jsonData.content
|
||||
};
|
||||
|
||||
// Tạo document mới
|
||||
const newInsurance = await this.create(processedData);
|
||||
const contentItems = jsonData.content?.content || [];
|
||||
console.log(`Insurance data migrated successfully for language: ${language}`);
|
||||
console.log(`Total content items: ${contentItems.length}`);
|
||||
|
||||
return newInsurance;
|
||||
} catch (error) {
|
||||
console.error("Error migrating insurance data to new structure:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const Insurance = mongoose.model("Insurance", insuranceSchema);
|
||||
|
||||
module.exports = Insurance;
|
||||
67
models/level.js
Normal file
67
models/level.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const levelSchema = new mongoose.Schema({
|
||||
brochure: { type: String },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
banner: {
|
||||
image: String,
|
||||
title: String,
|
||||
text: String
|
||||
},
|
||||
overview: {
|
||||
title: String,
|
||||
paragraphs: [String],
|
||||
contact_info: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
items: [{
|
||||
text: String
|
||||
}]
|
||||
},
|
||||
social_info: {
|
||||
title: String,
|
||||
social_links: [{
|
||||
image: String,
|
||||
url: String,
|
||||
alt: String
|
||||
}],
|
||||
apply_button: {
|
||||
text: String,
|
||||
url: String
|
||||
}
|
||||
}
|
||||
},
|
||||
requirements: {
|
||||
title: String,
|
||||
items: [String]
|
||||
},
|
||||
action_buttons: {
|
||||
title: String,
|
||||
buttons: [{
|
||||
text: String,
|
||||
link: String
|
||||
}]
|
||||
},
|
||||
why_study: {
|
||||
title: String,
|
||||
items: [{
|
||||
number: String,
|
||||
title: String,
|
||||
text: String
|
||||
}]
|
||||
},
|
||||
// Thêm tham chiếu đến Form
|
||||
form: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Form'
|
||||
},
|
||||
updatedAt: Date
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Level', levelSchema);
|
||||
185
models/menuHeader.js
Normal file
185
models/menuHeader.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const MenuSchema = new mongoose.Schema({
|
||||
menuid: { type: String, required: true, unique: true }, // ID tùy chỉnh
|
||||
parent: { type: String, default: null }, // ID menu cha
|
||||
title: { type: String, required: true }, // Tên hiển thị
|
||||
url: { type: String, default: '' }, // Đường dẫn
|
||||
order: { type: Number, default: 0 }, // Thứ tự hiển thị
|
||||
type: { type: String, enum: ['static', 'page', 'level'], default: 'static' }, // Loại menu
|
||||
fetch: { type: Boolean, default: false }, // Có fetch programme không
|
||||
isActive: { type: Boolean, default: true } // Trạng thái hoạt động của menu
|
||||
}, { timestamps: false });
|
||||
|
||||
// Index để tối ưu query
|
||||
MenuSchema.index({ parent: 1, order: 1 });
|
||||
MenuSchema.index({ type: 1 });
|
||||
|
||||
// Method để lấy menu tree
|
||||
MenuSchema.statics.getMenuTree = async function () {
|
||||
try {
|
||||
const allMenus = await this.find().sort({ order: 1 }).lean();
|
||||
|
||||
const menuMap = new Map();
|
||||
const rootMenus = [];
|
||||
|
||||
// Đưa tất cả menus vào map và xử lý URL dựa trên isActive
|
||||
allMenus.forEach(menu => {
|
||||
const activeUrl = menu.isActive === false ? '/maintenance/' : menu.url;
|
||||
menuMap.set(menu.menuid, {
|
||||
...menu,
|
||||
url: activeUrl, // Sử dụng URL đã được xử lý
|
||||
children: []
|
||||
});
|
||||
});
|
||||
|
||||
// Xây dựng tree structure
|
||||
allMenus.forEach(menu => {
|
||||
const menuObj = menuMap.get(menu.menuid);
|
||||
|
||||
if (!menu.parent) {
|
||||
rootMenus.push(menuObj);
|
||||
} else {
|
||||
const parent = menuMap.get(menu.parent);
|
||||
if (parent) {
|
||||
parent.children.push(menuObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return rootMenus;
|
||||
} catch (error) {
|
||||
console.error('Error building menu tree:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để lấy menu tree với programmes
|
||||
MenuSchema.statics.getMenuTreeWithProgrammes = async function () {
|
||||
try {
|
||||
const menuTree = await this.getMenuTree();
|
||||
|
||||
// Thêm programmes cho các menu level
|
||||
for (const menu of menuTree) {
|
||||
await this.addProgrammesToMenu(menu);
|
||||
}
|
||||
|
||||
return menuTree;
|
||||
} catch (error) {
|
||||
console.error('Error building menu tree with programmes:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để thêm programmes vào menu
|
||||
MenuSchema.statics.addProgrammesToMenu = async function (menuItem) {
|
||||
try {
|
||||
if (menuItem.type === 'level' && menuItem.fetch) {
|
||||
const programmes = await this.getProgrammesByMenuId(menuItem.menuid);
|
||||
menuItem.programmes = programmes;
|
||||
}
|
||||
|
||||
if (menuItem.children && menuItem.children.length > 0) {
|
||||
for (const child of menuItem.children) {
|
||||
await this.addProgrammesToMenu(child);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding programmes to menu:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để lấy programmes theo menu ID
|
||||
MenuSchema.statics.getProgrammesByMenuId = async function (menuId) {
|
||||
try {
|
||||
const Programme = require('./programme');
|
||||
const Level = require('./level');
|
||||
|
||||
// Sử dụng trực tiếp menuId làm levelType vì menuid đã được đặt đúng khi tạo
|
||||
const levelType = menuId;
|
||||
|
||||
const level = await Level.findOne({ type: levelType });
|
||||
if (!level) return [];
|
||||
|
||||
const programmes = await Programme.find({ level: level._id })
|
||||
.select('name code level_type')
|
||||
.sort({ name: 1 })
|
||||
.lean();
|
||||
|
||||
return programmes;
|
||||
} catch (error) {
|
||||
console.error('Error getting programmes by menu ID:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để tạo menu mới
|
||||
MenuSchema.statics.createMenu = async function (menuData) {
|
||||
try {
|
||||
const menu = new this(menuData);
|
||||
await menu.save();
|
||||
return menu;
|
||||
} catch (error) {
|
||||
console.error('Error creating menu:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để cập nhật menu
|
||||
MenuSchema.statics.updateMenu = async function (menuId, updateData) {
|
||||
try {
|
||||
const menu = await this.findOneAndUpdate({ menuid: menuId }, updateData, { new: true });
|
||||
return menu;
|
||||
} catch (error) {
|
||||
console.error('Error updating menu:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để xóa menu
|
||||
MenuSchema.statics.deleteMenu = async function (menuId) {
|
||||
try {
|
||||
// Xóa tất cả children trước
|
||||
await this.deleteMany({ parent: menuId });
|
||||
// Sau đó xóa menu chính
|
||||
const result = await this.findOneAndDelete({ menuid: menuId });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error deleting menu:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để sắp xếp lại order
|
||||
MenuSchema.statics.reorderMenus = async function (parentId, menuIds) {
|
||||
try {
|
||||
const updates = menuIds.map((menuId, index) => ({
|
||||
updateOne: {
|
||||
filter: { menuid: menuId },
|
||||
update: { order: index }
|
||||
}
|
||||
}));
|
||||
|
||||
await this.bulkWrite(updates);
|
||||
console.log('Menus reordered successfully');
|
||||
} catch (error) {
|
||||
console.error('Error reordering menus:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để lấy URL dựa trên trạng thái isActive
|
||||
MenuSchema.methods.getActiveUrl = function () {
|
||||
if (this.isActive === false) {
|
||||
return '/maintenance/';
|
||||
}
|
||||
return this.url;
|
||||
};
|
||||
|
||||
// Method để kiểm tra trạng thái hoạt động
|
||||
MenuSchema.methods.isMenuActive = function () {
|
||||
return this.isActive !== false;
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('MenuHeader', MenuSchema);
|
||||
28
models/migration.js
Normal file
28
models/migration.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const migrationSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true
|
||||
},
|
||||
batch: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 1
|
||||
},
|
||||
ranAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Index để tìm kiếm nhanh
|
||||
migrationSchema.index({ name: 1 });
|
||||
migrationSchema.index({ batch: -1 });
|
||||
|
||||
module.exports = mongoose.model('Migration', migrationSchema);
|
||||
|
||||
76
models/safety.js
Normal file
76
models/safety.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho hero section
|
||||
const safetySchema = new mongoose.Schema(
|
||||
{
|
||||
//hero section
|
||||
hero: {
|
||||
banner: String,
|
||||
title: String,
|
||||
},
|
||||
|
||||
//approach section
|
||||
approach: {
|
||||
badge: String,
|
||||
title:String,
|
||||
description:String,
|
||||
imgs:{
|
||||
img1:String,
|
||||
img2:String
|
||||
},
|
||||
stats:{
|
||||
count:String,
|
||||
label:String,
|
||||
avatars:[String]
|
||||
},
|
||||
features:[
|
||||
{text:String}
|
||||
],
|
||||
cards: [
|
||||
{
|
||||
title: String,
|
||||
content: String,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//philosophy section
|
||||
philosophy: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
cards: [
|
||||
{
|
||||
title: String,
|
||||
content: String,
|
||||
author: {
|
||||
avt: String,
|
||||
name: String,
|
||||
role: String,
|
||||
rating: String,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//security section
|
||||
security: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
cards: [
|
||||
{
|
||||
title: String,
|
||||
content: String,
|
||||
author: {
|
||||
avt: String,
|
||||
name: String,
|
||||
role: String,
|
||||
rating: String,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Safety", safetySchema);
|
||||
519
models/terms.js
Normal file
519
models/terms.js
Normal file
@@ -0,0 +1,519 @@
|
||||
// models/terms.js
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho content items
|
||||
const contentItemSchema = new mongoose.Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["paragraph", "section", "header", "list", "cancellation_table", "cancellation_section", "note", "embed", "image"],
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
// Header level (h2, h3, h4, h5, h6)
|
||||
level: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 6,
|
||||
default: 2,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
subsections: {
|
||||
type: [mongoose.Schema.Types.Mixed], // Recursive reference
|
||||
default: [],
|
||||
},
|
||||
items: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
// List style (for list type)
|
||||
style: {
|
||||
type: String,
|
||||
enum: ["ordered", "unordered"],
|
||||
default: "unordered",
|
||||
},
|
||||
// Embed/video fields (optional)
|
||||
embed: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
videoId: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho overlay style
|
||||
const overlayStyleSchema = new mongoose.Schema(
|
||||
{
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho hero section - CẤU TRÚC MỚI
|
||||
const heroSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
default: "Frequently Asked Questions",
|
||||
},
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "/uploads/terms/faqimage.jpg",
|
||||
},
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
},
|
||||
backgroundClasses: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
},
|
||||
overlayStyle: {
|
||||
type: overlayStyleSchema,
|
||||
default: () => ({ backgroundColor: "rgba(0, 0, 0, 0)" }),
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "text-white text-[5vw] uk-text-center",
|
||||
},
|
||||
enableScrollspy: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho page section - CẤU TRÚC MỚI
|
||||
const pageSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
default: "Terms & Conditions Go and Grow Camp e.K.",
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-section-default uk-section-overlap uk-section",
|
||||
},
|
||||
titleClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
},
|
||||
dividerClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-divider-small uk-text-left@m uk-text-center",
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho content section - CẤU TRÚC MỚI
|
||||
const contentSchema = new mongoose.Schema(
|
||||
{
|
||||
sectionClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-section-muted uk-section-overlap uk-section",
|
||||
},
|
||||
textClass: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "uk-panel uk-margin text-[1vw]",
|
||||
},
|
||||
content: {
|
||||
type: [contentItemSchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Main Terms Schema - CẤU TRÚC MỚI
|
||||
const termsSchema = new mongoose.Schema(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
default: "default",
|
||||
unique: true,
|
||||
},
|
||||
// CHỈ CÒN 3 PHẦN CHÍNH
|
||||
hero: {
|
||||
type: heroSchema,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: pageSchema,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: contentSchema,
|
||||
required: true,
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
version: {
|
||||
type: String,
|
||||
default: "2.0.0", // Tăng version vì cấu trúc thay đổi
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
migratedFromOldStructure: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Static method: Lấy terms default - CẬP NHẬT THEO CẤU TRÚC MỚI
|
||||
termsSchema.statics.getDefault = async function(language = "en") {
|
||||
try {
|
||||
let terms = await this.findOne({ name: "default", language: language });
|
||||
|
||||
if (!terms) {
|
||||
// Tạo terms mặc định theo cấu trúc mới
|
||||
terms = new this({
|
||||
name: "default",
|
||||
language: language,
|
||||
hero: {
|
||||
title: "Frequently Asked Questions",
|
||||
subtitle: "Our Terms & Conditions",
|
||||
backgroundImage: "/uploads/terms/faqimage.jpg",
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
overlayStyle: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0)"
|
||||
},
|
||||
titleClass: "text-white text-[5vw] uk-text-center",
|
||||
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
|
||||
enableScrollspy: true
|
||||
},
|
||||
page: {
|
||||
title: "Terms & Conditions Go and Grow Camp e.K.",
|
||||
divider: true,
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-section",
|
||||
titleClass: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
dividerClass: "uk-divider-small uk-text-left@m uk-text-center"
|
||||
},
|
||||
content: {
|
||||
sectionClass: "uk-section-muted uk-section-overlap uk-section",
|
||||
textClass: "uk-panel uk-margin text-[1vw]",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "This is an English translation of the original and legally binding German document \"Allgemeine Geschäftsbedingungen Go and Grow Camp e.K.\", which can be viewed at <a href=\"https://www.campadventure.de/de/infos/agb\" target=\"_self\">https://www.campadventure.de/de/infos/agb</a>. This translation is for your information only and is not legally binding."
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "<strong>Go and Grow Camp e.K. is the tour operator for individuals, for camps in Germany, England and Northern Ireland.</strong>"
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "GUARANTEE: All participants are protected in accordance with the legal regulations governing tour operators in Germany. As per §651, any payments made towards the travel price are insured against insolvency by tourVers."
|
||||
}
|
||||
]
|
||||
},
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false
|
||||
});
|
||||
|
||||
await terms.save();
|
||||
console.log(`Created default terms for language: ${language} (new structure)`);
|
||||
}
|
||||
|
||||
return terms;
|
||||
} catch (error) {
|
||||
console.error("Error in getDefault:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Method để get terms data
|
||||
termsSchema.methods.getTermsData = function() {
|
||||
return this.toObject();
|
||||
};
|
||||
|
||||
// Migration method từ JSON CŨ sang cấu trúc MỚI
|
||||
termsSchema.statics.migrateFromJson = async function(jsonData, language = "en") {
|
||||
try {
|
||||
console.log('Migrating from JSON to new structure...');
|
||||
|
||||
// Xóa document cũ nếu có
|
||||
await this.deleteOne({ name: "default", language: language });
|
||||
|
||||
// Chuyển đổi từ cấu trúc cũ sang mới
|
||||
const processedData = {
|
||||
name: "default",
|
||||
language: language,
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: true,
|
||||
|
||||
hero: {
|
||||
title: jsonData.hero?.title || "Go and Grow Camp",
|
||||
subtitle: jsonData.hero?.subtitle || "Our Terms & Conditions",
|
||||
backgroundImage: jsonData.hero?.backgroundImage || "/uploads/terms/faqimage.jpg",
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
overlayStyle: {
|
||||
backgroundColor: jsonData.hero?.overlayColor || "rgba(0, 0, 0, 0)"
|
||||
},
|
||||
titleClass: "text-white text-[5vw] uk-text-center",
|
||||
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
|
||||
enableScrollspy: jsonData.hero?.enableScrollspy || true
|
||||
},
|
||||
|
||||
page: {
|
||||
title: jsonData.termsHeader?.title || "Terms & Conditions Go and Grow Camp e.K.",
|
||||
divider: jsonData.termsHeader?.divider !== false,
|
||||
sectionClass: jsonData.termsHeader?.sectionClass || "uk-section-default uk-section-overlap uk-section",
|
||||
titleClass: jsonData.termsHeader?.titleClass || "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
dividerClass: jsonData.termsHeader?.dividerClass || "uk-divider-small uk-text-left@m uk-text-center"
|
||||
},
|
||||
|
||||
content: {
|
||||
sectionClass: jsonData.layout?.termsSectionClass || "uk-section-muted uk-section-overlap uk-section",
|
||||
textClass: jsonData.layout?.textContentClass || "uk-panel uk-margin text-[1vw]",
|
||||
content: []
|
||||
}
|
||||
};
|
||||
|
||||
// Chuyển đổi sections cũ sang content mới
|
||||
const contentItems = [];
|
||||
|
||||
// Thêm disclaimer đầu tiên nếu có
|
||||
if (jsonData.disclaimer?.text) {
|
||||
contentItems.push({
|
||||
type: "paragraph",
|
||||
text: jsonData.disclaimer.text
|
||||
});
|
||||
}
|
||||
|
||||
if (jsonData.disclaimer?.importantNote) {
|
||||
contentItems.push({
|
||||
type: "paragraph",
|
||||
text: `<strong>${jsonData.disclaimer.importantNote}</strong>`
|
||||
});
|
||||
}
|
||||
|
||||
if (jsonData.disclaimer?.legalNote) {
|
||||
contentItems.push({
|
||||
type: "paragraph",
|
||||
text: jsonData.disclaimer.legalNote
|
||||
});
|
||||
}
|
||||
|
||||
// Thêm disclaimer note
|
||||
if (jsonData.disclaimer?.note) {
|
||||
contentItems.push({
|
||||
type: "paragraph",
|
||||
text: jsonData.disclaimer.note
|
||||
});
|
||||
}
|
||||
|
||||
// Thêm các sections
|
||||
if (jsonData.sections && Array.isArray(jsonData.sections)) {
|
||||
jsonData.sections.forEach(section => {
|
||||
if (section.title && section.content) {
|
||||
const contentItem = {
|
||||
type: "section",
|
||||
title: section.title,
|
||||
content: section.content
|
||||
};
|
||||
|
||||
// Thêm subsections nếu có
|
||||
if (section.subsections && section.subsections.length > 0) {
|
||||
contentItem.subsections = section.subsections.map(sub => ({
|
||||
type: "note",
|
||||
text: sub.content || sub
|
||||
}));
|
||||
}
|
||||
|
||||
// Thêm cancellation fees nếu có
|
||||
if (section.fees) {
|
||||
contentItem.subsections = contentItem.subsections || [];
|
||||
|
||||
// Individual fees
|
||||
if (section.fees.individual && section.fees.individual.length > 0) {
|
||||
contentItem.subsections.push({
|
||||
type: "cancellation_table",
|
||||
title: "Standard Cancellation Fees",
|
||||
items: section.fees.individual.map(fee => `${fee.period} – ${fee.fee}`)
|
||||
});
|
||||
}
|
||||
|
||||
// School group fees
|
||||
if (section.fees.schoolGroups && section.fees.schoolGroups.fees) {
|
||||
contentItem.subsections.push({
|
||||
type: "cancellation_section",
|
||||
title: "Cancellation policy for school groups:",
|
||||
items: [
|
||||
section.fees.schoolGroups.freeCorrection,
|
||||
...section.fees.schoolGroups.fees.map(fee => `${fee.period}: ${fee.fee}`)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Fee note
|
||||
if (section.fees.note) {
|
||||
contentItem.subsections.push({
|
||||
type: "note",
|
||||
text: section.fees.note
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
contentItems.push(contentItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Thêm footer note nếu có
|
||||
if (jsonData.footerNote?.text) {
|
||||
contentItems.push({
|
||||
type: "paragraph",
|
||||
text: jsonData.footerNote.text
|
||||
});
|
||||
}
|
||||
|
||||
// Gán content items đã chuyển đổi
|
||||
processedData.content.content = contentItems;
|
||||
|
||||
// Tạo document mới
|
||||
const newTerms = await this.create(processedData);
|
||||
console.log(`Terms data migrated to new structure for language: ${language}`);
|
||||
console.log(`Total content items: ${contentItems.length}`);
|
||||
|
||||
return newTerms;
|
||||
} catch (error) {
|
||||
console.error("Error migrating terms data to new structure:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Migration method từ cấu trúc MỚI sang cấu trúc MỚI (dành cho JSON mới)
|
||||
termsSchema.statics.migrateFromNewJson = async function(jsonData, language = "en") {
|
||||
try {
|
||||
console.log('Migrating from new JSON structure...');
|
||||
|
||||
// Xóa document cũ nếu có
|
||||
await this.deleteOne({ name: "default", language: language });
|
||||
|
||||
// Tạo document mới với cấu trúc mới
|
||||
const newTerms = await this.create({
|
||||
name: "default",
|
||||
language: language,
|
||||
version: "2.0.0",
|
||||
isActive: true,
|
||||
migratedFromOldStructure: false,
|
||||
|
||||
hero: {
|
||||
title: jsonData.hero?.title || "Go and Grow Camp",
|
||||
subtitle: jsonData.hero?.subtitle || "Our Terms & Conditions",
|
||||
backgroundImage: jsonData.hero?.backgroundImage || "/uploads/terms/faqimage.jpg",
|
||||
sectionClass: jsonData.hero?.sectionClass || "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
backgroundClasses: jsonData.hero?.backgroundClasses || "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
overlayStyle: jsonData.hero?.overlayStyle || { backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
titleClass: jsonData.hero?.titleClass || "text-white text-[5vw] uk-text-center",
|
||||
subtitleClass: jsonData.hero?.subtitleClass || "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
|
||||
enableScrollspy: jsonData.hero?.enableScrollspy !== undefined ? jsonData.hero.enableScrollspy : true
|
||||
},
|
||||
|
||||
page: {
|
||||
title: jsonData.page?.title || "Terms & Conditions Go and Grow Camp e.K.",
|
||||
divider: jsonData.page?.divider !== undefined ? jsonData.page.divider : true,
|
||||
sectionClass: jsonData.page?.sectionClass || "uk-section-default uk-section-overlap uk-section",
|
||||
titleClass: jsonData.page?.titleClass || "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
dividerClass: jsonData.page?.dividerClass || "uk-divider-small uk-text-left@m uk-text-center"
|
||||
},
|
||||
|
||||
content: {
|
||||
sectionClass: jsonData.content?.sectionClass || "uk-section-muted uk-section-overlap uk-section",
|
||||
textClass: jsonData.content?.textClass || "uk-panel uk-margin text-[1vw]",
|
||||
content: jsonData.content?.content || []
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Terms data created with new structure for language: ${language}`);
|
||||
console.log(`Hero title: ${newTerms.hero.title}`);
|
||||
console.log(`Page title: ${newTerms.page.title}`);
|
||||
console.log(`Content items: ${newTerms.content.content.length}`);
|
||||
|
||||
return newTerms;
|
||||
} catch (error) {
|
||||
console.error("Error creating terms data from new structure:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const Terms = mongoose.model("Terms", termsSchema);
|
||||
|
||||
module.exports = Terms;
|
||||
45
models/travel.js
Normal file
45
models/travel.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const travelSchema = new mongoose.Schema(
|
||||
{
|
||||
page: {
|
||||
title: {
|
||||
type: String,
|
||||
default: "Travel Information",
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
year: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
metadata: {
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
},
|
||||
hero: {
|
||||
title: {
|
||||
type: String,
|
||||
default: "Travel Information",
|
||||
},
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: { blocks: [] },
|
||||
},
|
||||
enableScrollspy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Travel", travelSchema);
|
||||
0
models/visa.js
Normal file
0
models/visa.js
Normal file
Reference in New Issue
Block a user