refactor: restructure home model and update homeController

This commit is contained in:
Wini_Fy
2026-02-05 13:24:07 +07:00
parent c3a55b13f8
commit aea70f89ac
5 changed files with 708 additions and 871 deletions

View File

@@ -1,315 +1,249 @@
const { addBaseUrlToImages } = require('../utils/imageHelper'); const { addBaseUrlToImages } = require("../utils/imageHelper");
const Home = require('../models/home'); const Home = require("../models/home");
// -------------------- Helper Functions -------------------- // -------------------- Helpers --------------------
// Get home data from MongoDB const getHomeDoc = async () => {
const getHomeData = async () => { // Keep newest document as the source of truth
const home = await Home.findOne().sort({ updatedAt: -1 }).lean(); return await Home.findOne().sort({ updatedAt: -1 });
return home || {};
}; };
// Get default home data structure const getHomeData = async () => {
const doc = await Home.findOne().sort({ updatedAt: -1 }).lean();
return doc || {};
};
/**
* Default structure used by the current CMS Home admin UI (`views/admin/home/index.ejs`).
* This is intentionally permissive; the Home model itself also supports the Next.js
* structure from `hailearning.edu.vn/app/home.json`.
*/
const getDefaultHomeData = () => ({ const getDefaultHomeData = () => ({
hero: { hero: {
title: '', title: "",
description: '', description: "",
backgroundImage: '', backgroundImage: "",
button: { label: 'Book Your Adventure', href: '/booking' }, button: { label: "Book Your Adventure", href: "/booking" },
contactBox: { contactBox: {
welcomeText: '', welcomeText: "",
phone: { label: 'Call us', number: '', href: '' }, phone: { label: "Call us", number: "", href: "" },
email: { label: 'Email', address: '', href: '' }, email: { label: "Email", address: "", href: "" },
workingHours: { label: 'Working Hours', hours: '' } workingHours: { label: "Working Hours", hours: "" },
} },
}, },
about: { about: {
title: '', title: "",
subtitle: '', subtitle: "",
description: '', description: "",
images: { mainImage1: '', mainImage2: '', avatars: [] }, images: { mainImage1: "", mainImage2: "", avatars: [] },
features: [], features: [],
quote: '', quote: "",
button: { label: '', href: '' }, button: { label: "", href: "" },
stats: { customerCount: 0, customerLabel: '' } stats: { customerCount: 0, customerLabel: "" },
}, },
missionVision: { missionVision: {
title: '', title: "",
subtitle: '', subtitle: "",
backgroundImage: '', backgroundImage: "",
cards: [] cards: [],
}, },
whyChooseUs: { whyChooseUs: {
title: '', title: "",
subtitle: '', subtitle: "",
description: '', description: "",
button: { label: '', href: '' }, button: { label: "", href: "" },
features: [], features: [],
tags: [], tags: [],
cta: { text: '', linkText: '', linkHref: '' } cta: { text: "", linkText: "", linkHref: "" },
},
activities: {
cards: []
}, },
activities: { cards: [] },
faq: { faq: {
title: '', title: "",
subtitle: '', subtitle: "",
description: '', description: "",
image: '', image: "",
contact: { title: '', info: '' }, contact: { title: "", info: "" },
questions: [] questions: [],
}, },
partners: { partners: {
title: '', title: "",
subtitle: '', subtitle: "",
backgroundImage: '', backgroundImage: "",
logos: [], logos: [],
cta: { badge: '', text: '', linkText: '', linkHref: '' } cta: { badge: "", text: "", linkText: "", linkHref: "" },
}, },
programs: { programs: {
title: '', title: "",
subtitle: '', subtitle: "",
button: { label: '', href: '' }, button: { label: "", href: "" },
card: { card: {
pricePrefix: 'from', pricePrefix: "from",
priceSuffix: 'USD', priceSuffix: "USD",
buttonLabel: 'Camp Detail', buttonLabel: "Camp Detail",
buttonHref: '/camp-profiles' buttonHref: "/camp-profiles",
}, },
items: [] items: [],
}, },
newsletter: { newsletter: {
title: '', title: "",
subtitle: '', subtitle: "",
description: '', description: "",
image: '', image: "",
decorativeImage: '', decorativeImage: "",
button: { button: { label: "", placeholder: "", href: "" },
label: '',
placeholder: '',
href: ''
}
}, },
latestPosts: { latestPosts: {
title: '', title: "",
subtitle: '', subtitle: "",
searchPlaceholder: '', searchPlaceholder: "",
sidebarTitle: '', sidebarTitle: "",
blogPosts: [], blogPosts: [],
sidebarPosts: [], sidebarPosts: [],
featuredCard: { image: '', title: '', description: '' } featuredCard: { image: "", title: "", description: "" },
} },
}); });
// -------------------- Admin --------------------
// -------------------- Admin Exports --------------------
// Display home management page
exports.index = async (req, res) => { exports.index = async (req, res) => {
try { try {
// Fetch Home data
let data = await getHomeData(); let data = await getHomeData();
// If no data exists, use default
if (!data || Object.keys(data).length === 0) { if (!data || Object.keys(data).length === 0) {
data = getDefaultHomeData(); data = getDefaultHomeData();
} else { } else {
// Merge with defaults to ensure all fields exist // Merge minimal defaults to keep the view safe
const defaultData = getDefaultHomeData(); const defaults = getDefaultHomeData();
data.hero = data.hero || defaults.hero;
// Ensure all sections exist with defaults data.about = data.about || defaults.about;
data.hero = data.hero || defaultData.hero; data.missionVision = data.missionVision || defaults.missionVision;
data.about = data.about || defaultData.about; data.whyChooseUs = data.whyChooseUs || defaults.whyChooseUs;
data.missionVision = data.missionVision || defaultData.missionVision; data.activities = data.activities || defaults.activities;
data.whyChooseUs = data.whyChooseUs || defaultData.whyChooseUs; data.faq = data.faq || defaults.faq;
data.activities = data.activities || defaultData.activities; data.partners = data.partners || defaults.partners;
data.faq = data.faq || defaultData.faq; data.programs = data.programs || defaults.programs;
data.partners = data.partners || defaultData.partners; data.newsletter = data.newsletter || defaults.newsletter;
data.programs = data.programs || defaultData.programs; data.latestPosts = data.latestPosts || defaults.latestPosts;
data.newsletter = data.newsletter || defaultData.newsletter;
data.latestPosts = data.latestPosts || defaultData.latestPosts;
} }
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
return res.render("admin/home/index", {
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; layout: "layouts/main",
title: "Home Management",
res.render('admin/home/index', {
layout: 'layouts/main',
title: 'Home Management',
data, data,
frontendUrl, frontendUrl,
currentPath: req.path, currentPath: req.path,
user: req.session.user user: req.session.user,
}); });
} catch (err) { } catch (err) {
console.error('Home index error:', err); console.error("Home index error:", err);
req.flash('error_msg', 'Error loading home data'); req.flash("error_msg", "Error loading home data");
res.redirect('/admin/dashboard'); return req.session.save(() => res.redirect("/admin/dashboard"));
} }
}; };
// Update home data
exports.update = async (req, res) => { exports.update = async (req, res) => {
try { try {
// Get current data const currentDoc = await getHomeDoc();
const currentData = await getHomeData(); const currentData = currentDoc ? currentDoc.toObject() : {};
const updatedData = { ...currentData };
// Create updated data object // Quick fields (Hero) from classic form fields
const updatedData = { ...(currentData.toObject ? currentData.toObject() : currentData) };
// Update Hero section data (from Welcome tab)
if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) { if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) {
updatedData.hero = { updatedData.hero = {
title: req.body.heroTitle || '', title: req.body.heroTitle || "",
description: req.body.heroDescription || '', description: req.body.heroDescription || "",
backgroundImage: req.body.heroBackgroundImage || '', backgroundImage: req.body.heroBackgroundImage || "",
button: { button: {
label: req.body.heroButtonLabel || 'Book Your Adventure', label: req.body.heroButtonLabel || "Book Your Adventure",
href: req.body.heroButtonHref || '/booking' href: req.body.heroButtonHref || "/booking",
}, },
contactBox: { contactBox: {
welcomeText: req.body.heroContactWelcome || '', welcomeText: req.body.heroContactWelcome || "",
phone: { phone: {
label: 'Call us', label: "Call us",
number: req.body.heroContactPhone || '', number: req.body.heroContactPhone || "",
href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : '' href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : "",
}, },
email: { email: {
label: 'Email', label: "Email",
address: req.body.heroContactEmail || '', address: req.body.heroContactEmail || "",
href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : '' href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : "",
}, },
workingHours: { workingHours: {
label: 'Working Hours', label: "Working Hours",
hours: req.body.heroContactHours || '' hours: req.body.heroContactHours || "",
} },
}
};
}
// Update Why Choose Us section
if (req.body.whyChooseUsTitle || req.body.whyChooseUsSubtitle) {
updatedData.whyChooseUs = {
...(updatedData.whyChooseUs || {}),
title: req.body.whyChooseUsTitle || '',
subtitle: req.body.whyChooseUsSubtitle || '',
description: req.body.whyChooseUsDescription || '',
button: {
label: req.body.whyChooseUsButtonLabel || '',
href: req.body.whyChooseUsButtonHref || ''
}, },
features: updatedData.whyChooseUs?.features || [],
tags: updatedData.whyChooseUs?.tags || [],
cta: updatedData.whyChooseUs?.cta || { text: '', linkText: '', linkHref: '' }
}; };
} }
// Handle Home sections (new camp structure only) // Handle sections sent as JSON payloads
const sections = ['hero', 'about', 'missionVision', 'whyChooseUs', 'activities', 'faq', const sections = [
'partners', 'programs', 'newsletter', 'latestPosts']; "hero",
const errors = []; "about",
"missionVision",
"whyChooseUs",
"activities",
"faq",
"partners",
"programs",
"newsletter",
"latestPosts",
];
let hasChanges = false; let hasChanges = false;
// Process each section
for (const section of sections) { for (const section of sections) {
if (!req.body[section]) continue;
try { try {
if (!req.body[section]) {
console.warn(`No data for section: ${section}`);
continue;
}
// Parse JSON data from form
const newSectionData = JSON.parse(req.body[section]); const newSectionData = JSON.parse(req.body[section]);
const currentSectionData = currentData?.[section];
// Check for changes if (JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData)) {
const currentSectionData = currentData[section];
const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData);
if (sectionHasChanges) {
updatedData[section] = newSectionData; updatedData[section] = newSectionData;
hasChanges = true; hasChanges = true;
} }
} catch (error) { } catch (e) {
console.error(`Error processing section ${section}:`, error); console.error(`Error processing section "${section}":`, e);
errors.push(`Error processing ${section} data: ${error.message}`); req.flash("error_msg", `Invalid JSON for section "${section}": ${e.message}`);
return req.session.save(() => res.redirect("/admin/home"));
} }
} }
// Handle errors
if (errors.length > 0) {
req.flash('error_msg', `Data processing error: ${errors[0]}`);
return req.session.save(() => res.redirect('/admin/home'));
}
// Check if there are changes
if (!hasChanges) { if (!hasChanges) {
req.flash('info_msg', 'No changes were made'); req.flash("info_msg", "No changes were made");
return req.session.save(() => res.redirect('/admin/home')); return req.session.save(() => res.redirect("/admin/home"));
} }
// Update or create document if (currentDoc?._id) {
try { await Home.findByIdAndUpdate(currentDoc._id, updatedData, { new: true });
if (currentData._id) {
await Home.findByIdAndUpdate(currentData._id, updatedData, { new: true });
} else { } else {
await Home.create(updatedData); await Home.create(updatedData);
} }
req.flash('success_msg', 'Home data updated successfully'); req.flash("success_msg", "Home data updated successfully");
return req.session.save(() => res.redirect('/admin/home')); return req.session.save(() => res.redirect("/admin/home"));
} catch (dbError) {
console.error('Database error:', dbError);
req.flash('error_msg', `Database error: ${dbError.message || 'Unknown'}`);
return req.session.save(() => res.redirect('/admin/home'));
}
} catch (err) { } catch (err) {
console.error('Update error:', err); console.error("Home update error:", err);
req.flash('error_msg', `Update error: ${err.message || 'Unknown'}`); req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
return req.session.save(() => res.redirect('/admin/home')); return req.session.save(() => res.redirect("/admin/home"));
} }
}; };
// -------------------- Public API Exports -------------------- // -------------------- Public API --------------------
// API to get home data for frontend
exports.api = async (req, res) => { exports.api = async (req, res) => {
try { try {
const homeData = await getHomeData(); const homeData = await getHomeData();
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedData = addBaseUrlToImages(homeData, baseUrl); const processedData = addBaseUrlToImages(homeData, baseUrl);
return res.json(processedData);
res.json(processedData);
} catch (err) { } catch (err) {
console.error('Home API error:', err); console.error("Home API error:", err);
res.status(500).json({ error: 'Error loading home data' }); return res.status(500).json({ error: "Error loading home data" });
} }
}; };
// API to get hero data for frontend
exports.apiHero = async (req, res) => {
try {
const homeData = await getHomeData();
const heroData = homeData?.hero;
if (!heroData) {
return res.status(404).json({
error: 'Hero data not found',
data: null
});
}
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedData = addBaseUrlToImages(heroData, baseUrl);
res.json(processedData);
} catch (err) {
console.error('Hero API error:', err);
res.status(500).json({ error: 'Error loading hero data' });
}
};

View File

@@ -1,537 +1,339 @@
{ {
"hero": { "hero": {
"title": "Discover Adventure and Friendship", "title": "From Application to Visa We've Got You Covered",
"description": "Step into a world where adventure meets comfort. Discover breathtaking landscapes, thrilling outdoor activities, and the serenity of luxury camping.", "subtitle": "Global Education Simplified",
"backgroundImage": "/uploads/home/b2.jpg", "description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.",
"button": { "primaryButton": {
"label": "Book Your Adventure", "label": "Apply now",
"href": "/booking" "href": "/contact"
}, },
"contactBox": { "secondaryButton": {
"welcomeText": "Your Adventure Journey Start Here!", "label": "Book Free Consultation",
"phone": { "href": "/contact"
"label": "Call us",
"number": "+(123) 456 789",
"href": "tel:+123456789"
}, },
"email": { "backgroundImage": "/assets/img/home-1/hero/bg.jpg",
"label": "Email", "videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I"
"address": "office@ggcamp.org",
"href": "mailto:office@ggcamp.org"
},
"workingHours": {
"label": "Working Hours",
"hours": "Monday-Saturday: 08:pm to 05:am"
}
}
},
"about": {
"title": "- About Us",
"subtitle": "Creating Amazing Camps",
"description": "Learning is closely tied to practical experience—summer is the perfect time for hands-on opportunities. While knowledge must still be nurtured, it can take on new and more engaging forms.",
"images": {
"mainImage1": "/uploads/home/a1.jpg",
"mainImage2": "/uploads/home/a2.jpg",
"avatars": [
"/uploads/home/profile-face_1.jpg",
"/uploads/home/young-tourist-sitting-tent.jpg",
"/uploads/home/portrait-young-male-tourist-standing-forest-with-tent.jpg"
]
},
"features": [
"Fun-Filled Experiences for Every Camper",
"Adventures That Inspire Confidence and Growth",
"Memories and Friendships That Last a Lifetime"
],
"quote": "\"Your Journey, Your Comfort, Your Adventure.\"",
"button": {
"label": "Learn More About",
"href": "/info/about"
},
"stats": {
"customerCount": 50,
"customerLabel": "Adventurer with\nhappy customer"
}
},
"missionVision": {
"title": "- Who We Are",
"subtitle": "Company Mission & Vision",
"backgroundImage": "/uploads/home/b8.jpg",
"cards": [
{
"title": "Our Mission",
"description": "We provide a safe, inclusive, and inspiring environment where children and teens can explore, learn, and grow through adventure, creativity, and friendship."
},
{
"title": "Our Vision",
"description": "We aim to be a leading international camp experience that nurtures confident, responsible, and compassionate young individuals connected to nature and their communities."
},
{
"title": "Company Goals",
"description": "Through hands-on activities, community service, and outdoor adventures, we help campers build independence, teamwork, and lifelong memories."
}
]
}, },
"whyChooseUs": { "whyChooseUs": {
"title": "- Why Choose Us", "heading": "Turning Study Abroad Dreams Into Reality",
"subtitle": "Creating unforgettable camp experiences with safety, fun, and friendship.", "subheading": "About Our Consultancy",
"description": "Go and Grow Camp has organized international summer camps and educational outdoor trips across multiple countries. We are committed to providing a safe, inclusive, and inspiring environment—no violence, drugs, alcohol, or cigarettes are permitted for any participant.", "description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.",
"button": { "items": [
"label": "Explore Now", {
"href": "/booking" "icon": "/assets/img/home-1/icon/01.svg",
"title": "Global Reach",
"description": "Expanding Opportunities Worldwide"
}, },
{
"icon": "/assets/img/home-1/icon/01.svg",
"title": "Expert Guidance",
"description": "Professional Support Every Step"
}
],
"features": [ "features": [
{ "Fastest Visa form processing with skilled immigration agents",
"title": "Inclusive & Welcoming", "Partnership with International Educational Institutions"
"description": "Every child, teen, and staff member, regardless of country or culture, feels comfortable and valued, creating a unique and unforgettable camp experience."
},
{
"title": "Adventure-Ready Experiences",
"description": "From team challenges to outdoor exploration, creative workshops, and water sports, we offer a wide range of activities that build confidence, teamwork, and independence."
},
{
"title": "Personal Growth & Friendship",
"description": "Campers develop life skills, make lifelong friends, and return home more confident, motivated, and inspired."
},
{
"title": "Safe & Responsible Environment",
"description": "Our trained staff ensure every camper enjoys a secure, supportive, and funfilled experience."
}
], ],
"tags": [ "ctaButton": {
"Nature-Friendly", "label": "Get Started",
"Adventure-Ready", "href": "/about"
"Community Service",
"Inspiring Locations"
],
"cta": {
"text": "Let's make something great work together.",
"linkText": "Get Free Quote",
"linkHref": "#"
} }
}, },
"activities": { "visaSolutions": {
"cards": [ "heading": "Comprehensive Visa Solutions",
"subheading": "Our Expert Services",
"items": [
{ {
"title": "Surfing Adventures", "number": "01",
"description": "Catch the waves and learn water safety while having a blast on the beach.", "title": "Student Visa Guidance",
"image": "/uploads/home/b13.jpg" "description": "Assistance with admission, documentation, and visa application.Assistance",
"link": "/service-details"
}, },
{ {
"title": "River Kayaking", "number": "02",
"description": "Paddle along scenic rivers, surrounded by wildlife and stunning landscapes.", "title": "PTE Exam Preparation",
"image": "/uploads/home/b14.jpg" "description": "We provide expert guidance and personalized support throughout the education visa process,",
"link": "/service-details"
}, },
{ {
"title": "Campfire Nights", "number": "03",
"description": "Gather around the fire, roast marshmallows, and share stories under the stars", "title": "University Selection Assistance",
"image": "/uploads/home/b16.jpg" "description": "We provide expert guidance and personalized support throughout the education visa process,",
"link": "/service-details"
}, },
{ {
"title": "Community Service Projects", "number": "04",
"description": "Participate in meaningful activities such as beach clean-ups, tree planting, and helping local community initiatives.", "title": "IELTS Exam Preparation",
"image": "/uploads/home/b11.jpg" "description": "We provide expert guidance and personalized support throughout the education visa process,",
"link": "/service-details"
} }
] ]
}, },
"visaCountries": {
"heading": "Visa & VISAWAY Services To UK",
"subheading": "UK. United Kingdom",
"description": "The Express Entry program is designed for skilled workers who wish to immigrate to Canada. It includes the Federal Skilled Worker Program, the Federal Skilled…",
"countries": [
{
"name": "United Kingdom",
"code": "UK",
"flag": "/assets/img/home-1/feature/shape.png",
"link": "/country-details/uk",
"visaTypes": [
"Visitor Visa",
"Student Visa & Admission",
"Work Visa H1B",
"Business Visa",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"name": "United States",
"code": "US",
"flag": "/assets/img/flags/us.png",
"link": "/country-details/us",
"visaTypes": [
"Student Visa F-1",
"Work Visa H1-B",
"Tourist Visa B-2"
]
},
{
"name": "Canada",
"code": "CA",
"flag": "/assets/img/flags/canada.png",
"link": "/country-details/canada",
"visaTypes": [
"Study Permit",
"Work Permit",
"Express Entry"
]
},
{
"name": "Australia",
"code": "AU",
"flag": "/assets/img/flags/australia.png",
"link": "/country-details/australia",
"visaTypes": [
"Student Visa 500",
"Skilled Migration",
"Working Holiday"
]
},
{
"name": "Germany",
"code": "DE",
"flag": "/assets/img/flags/germany.png",
"link": "/country-details/germany",
"visaTypes": [
"Student Visa",
"Job Seeker Visa",
"EU Blue Card"
]
}
],
"ctaButton": {
"label": "Get Started",
"href": "/contact"
}
},
"testimonials": {
"heading": "Student Reviews & Testimonials",
"subheading": "What Our Students Say",
"videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I",
"videoThumbnail": "/assets/img/home-1/testimonial/01.jpg",
"items": [
{
"name": "Sohel Tanvir",
"role": "Student",
"country": "Canada",
"rating": 5,
"comment": "Professional and reliable service. They explained each step clearly, prepared my documents, and supported me during the interview. My visa approval came faster than expected.",
"avatar": "/assets/img/home-1/testimonial/client.png"
},
{
"name": "Ayesha Rahman",
"role": "Student",
"country": "UK. United Kingdom",
"rating": 5,
"comment": "The consultancy guided me from start to finish, making my study abroad journey smooth and stress-free. Thanks to their expert support, I secured my visa successfully.",
"avatar": "/assets/img/home-1/testimonial/client-2.png"
},
{
"name": "Michael Chen",
"role": "Graduate Student",
"country": "Australia",
"rating": 5,
"comment": "Outstanding service from beginning to end. The team was knowledgeable, responsive, and made the entire visa process seamless. Highly recommend to anyone planning to study abroad.",
"avatar": "/assets/img/home-1/testimonial/client.png"
}
]
},
"videoGallery": {
"heading": "VIDEO PLAY GALLERY",
"videoUrl": "https://ex-coders.com/vdo/visa.mp4",
"thumbnail": "/assets/img/home-1/feature/text.png"
},
"faq": { "faq": {
"title": "- Frequently Asked Questions", "heading": "Got Questions? We've Got Answers",
"subtitle": "Essential Camp Info", "subheading": "Visa FAQs",
"description": "Everything you need to know for a safe, fun, and unforgettable experience. Get quick details about our programs, activities, accommodations, and community projects all in one place.", "description": "We understand students often have many questions about studying abroad. Our experts provide clear.",
"image": "/uploads/home/b5.jpg", "ctaButton": {
"contact": { "label": "contact us",
"title": "Need Any Help?", "href": "/contact"
"info": "+(123) 456-789 | info@hailearning.edu.vn"
}, },
"questions": [ "items": [
{ {
"question": "Safety & Supervision?", "question": "How long does the student visa process usually take?",
"answer": "Our trained and friendly staff are dedicated to ensuring every camper feels safe and supported throughout their stay. All camp sites are carefully chosen and regularly inspected for safety, and every activity is closely supervised. From water sports to forest hikes, we maintain high safety standards while encouraging campers to explore, challenge themselves, and grow." "answer": "The student visa process typically takes 4-8 weeks depending on the country and time of year. We recommend starting the application process at least 3 months before your intended travel date to ensure sufficient time for document preparation and processing."
}, },
{ {
"question": "Food & Meals?", "question": "Do you assist with scholarship applications as well?",
"answer": "Nutritious and balanced meals are prepared daily to keep campers energized for their activities. From locally sourced ingredients to delicious, kid-friendly recipes, our meals also accommodate special dietary needs. Mealtime is more than just food—it's a time for friends to gather, share experiences, and enjoy new flavors together." "answer": "Yes, we guide students in identifying suitable scholarships, preparing strong applications, and increasing chances of securing financial aid for their studies abroad."
}, },
{ {
"question": "Activities & Adventure?", "question": "Will you guide me in preparing for the visa interview?",
"answer": "Our diverse program of activities is designed to challenge, inspire, and entertain. Campers can ride the waves during surfing lessons, paddle scenic rivers on kayaking tours, or participate in team challenges and creative workshops. We also include meaningful community service projects, like beach clean-ups and tree planting, to teach responsibility and environmental stewardship. Every activity is a chance to learn, grow, and create lasting memories." "answer": "Absolutely! We provide comprehensive visa interview preparation, including mock interviews, document review, and tips on how to answer common questions confidently and effectively."
}, },
{ {
"question": "Can I bring my own food or cook at the campsite?", "question": "Do you offer post-arrival support for students?",
"answer": "Absolutely! Each site has cooking facilities including BBQ grills and fire pits. You're welcome to bring your own food and beverages." "answer": "Yes, we provide post-arrival support including airport pickup coordination, accommodation assistance, university orientation guidance, and ongoing support throughout your study period."
}, },
{ {
"question": "What types of adventure activities are available?", "question": "What documents are required for a student visa application?",
"answer": "We offer hiking, kayaking, rock climbing, mountain biking, fishing, and guided nature tours. Activities vary by location and season." "answer": "Required documents typically include a valid passport, university acceptance letter, proof of financial support, academic transcripts, language proficiency test scores, and health insurance. We provide a complete checklist tailored to your destination country."
}
]
},
"achievements": {
"heading": "Our Achievements in Numbers",
"subheading": "Did You Know",
"items": [
{
"value": "1000",
"suffix": "k+",
"label": "Students Guided",
"description": "Successfully assisted over a thousand students worldwide."
}, },
{ {
"question": "What is the cancellation policy?", "value": "50",
"answer": "Cancellation policies vary by location and activity. Some may allow cancellations with a fee, while others may have strict cancellation policies. It's important to review the specific cancellation policy for each activity or location before booking." "suffix": "+",
"label": "Countries Covered",
"description": "Helping students apply to universities in more than 50 countries."
}, },
{ {
"question": "What is the refund policy?", "value": "95",
"answer": "Refund policies vary by location and activity. Some may allow refunds with a fee, while others may have strict refund policies. It's important to review the specific refund policy for each activity or location before booking." "suffix": "%",
"label": "Visa Success Rate",
"description": "Inspired students to reach their goals globally"
},
{
"value": "10",
"suffix": "+",
"label": "Years of Experience",
"description": "Trusted experts in global education consulting."
} }
] ]
}, },
"partners": { "partners": {
"title": "- Our Partners", "heading": "Our Trusted Partners",
"subtitle": "Working with the best in outdoor living and exploration",
"backgroundImage": "/uploads/home/b2.jpg",
"logos": [
"/uploads/home/anhsims.png",
"/uploads/home/anhlogo2.png",
"/uploads/home/anhlogo9.png",
"/uploads/home/anhlogo4.png"
],
"cta": {
"badge": "Free",
"text": "Let's make something great work together.",
"linkText": "Get Free Quote",
"linkHref": "/booking"
}
},
"programs": {
"title": "- Activies",
"subtitle": "Explore Our Activities",
"button": {
"label": "Explore Now",
"href": "/booking"
},
"card": {
"pricePrefix": "from",
"priceSuffix": "USD",
"buttonLabel": "Camp Detail",
"buttonHref": "/camp-profiles"
},
"items": [ "items": [
{ {
"id": "adventure-sports-creative", "name": "Best Visa Consultancy",
"title": "Adventure, Sports & Creative", "logo": "/assets/img/home-1/feature/icon-1.png",
"price": "395", "year": "2025"
"seasons": ["spring", "summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Thailand",
"image": "/uploads/home/b5.jpg",
"slug": "adventure-sports-creative"
}, },
{ {
"id": "arts-crafts", "name": "Visa Success Award",
"title": "Arts & Crafts", "logo": "/assets/img/home-1/feature/icon-2.png",
"price": "500", "year": "2025"
"seasons": ["spring", "summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Vietnam",
"image": "/uploads/home/b6.jpg",
"slug": "arts-crafts"
}, },
{ {
"id": "climbing", "name": "Innovation Award",
"title": "Climbing", "logo": "/assets/img/home-1/feature/icon-3.png",
"price": "515", "year": "2025"
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/b1.jpg",
"slug": "climbing"
}, },
{ {
"id": "dancing", "name": "Global Education Partner",
"title": "Dancing", "logo": "/assets/img/home-1/feature/icon-4.png",
"price": "520", "year": "2025"
"seasons": ["summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Malaysia",
"image": "/uploads/home/b4.jpg",
"slug": "dancing"
}, },
{ {
"id": "diving", "name": "University Partner 1",
"title": "Diving", "logo": "/assets/img/home-1/brand/01.png",
"price": "1190", "year": "2025"
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/b2.jpg",
"slug": "diving"
}, },
{ {
"id": "englisch-toefl", "name": "University Partner 2",
"title": "Englisch TOEFL®", "logo": "/assets/img/home-1/brand/02.png",
"price": "1290", "year": "2025"
"seasons": ["spring", "summer"],
"age": "From 12 - 18 years old",
"location": "Malaysia",
"image": "/uploads/home/b1.jpg",
"slug": "englisch-toefl"
}, },
{ {
"id": "englischcamps", "name": "University Partner 3",
"title": "Englischcamps", "logo": "/assets/img/home-1/brand/03.png",
"price": "530", "year": "2025"
"seasons": ["spring", "summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/00-Language-Camps-by-Camp-Adventure-add7aa60.jpg",
"slug": "englischcamps"
}, },
{ {
"id": "fishing", "name": "University Partner 4",
"title": "Fishing", "logo": "/assets/img/home-1/brand/04.png",
"price": "580", "year": "2025"
"seasons": ["spring", "summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Vietnam",
"image": "/uploads/home/01-Angeln-im-Ferienlager-02243939.jpg",
"slug": "fishing"
}, },
{ {
"id": "german-camps", "name": "University Partner 5",
"title": "German Camps", "logo": "/assets/img/home-1/brand/05.png",
"price": "610", "year": "2025"
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Thailand",
"image": "/uploads/home/Deutschcamps-in-Deutschland-0ed3ea07.jpg",
"slug": "german-camps"
},
{
"id": "horseback-riding",
"title": "Horseback Riding",
"price": "620",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Portugal",
"image": "/uploads/home/00-Reiten-Sommercamp-Ausritt-6930f841.jpg",
"slug": "horseback-riding"
},
{
"id": "husky-camp",
"title": "Husky Camp",
"price": "525",
"seasons": ["spring", "summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "China",
"image": "/uploads/home/00-Husky%20Camp_sommercamp%20mit%20Hunden-9c098a17.jpg",
"slug": "husky-camp"
},
{
"id": "icit",
"title": "International Counsellor in Training (ICIT)",
"price": "995",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Thailand",
"image": "/uploads/home/00-INTERNATIONAL%20COUNSELOR%20IN%20TRAINING_teambuilding-3b91547c.jpg",
"slug": "international-counsellor-in-training-icit"
},
{
"id": "lifeguarding",
"title": "Lifeguarding",
"price": "580",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Malaysia",
"image": "/uploads/home/00-Rettungsschwimmen-Feriencamp-6a364891.jpg",
"slug": "lifeguarding"
},
{
"id": "leadership",
"title": "Leadership",
"price": "1185",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/00-Leadership-Camp-0d21c60a.jpg",
"slug": "senior-plus-leadership"
},
{
"id": "multi-water-adventure",
"title": "Multi Water Adventure",
"price": "990",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/00-Multi-Water-Adventure-im-Sommercamp-a47c08a3.jpg",
"slug": "multi-water-adventure"
},
{
"id": "sailing",
"title": "Sailing",
"price": "990",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Thailand",
"image": "/uploads/home/01-Segeln-im-Sommercamp-in-Spanien-e9d06b28.jpg",
"slug": "sailing"
},
{
"id": "skating",
"title": "Skating",
"price": "420",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Vietnam",
"image": "/uploads/home/00-Skaten%20im%20Sommercamp-8240a4c7.jpg",
"slug": "skating"
},
{
"id": "soccer",
"title": "Soccer",
"price": "445",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Malaysia",
"image": "/uploads/home/00-Soccer-Camps-543a1625.jpg",
"slug": "soccer"
},
{
"id": "space-exploration",
"title": "Space Exploration",
"price": "665",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "China",
"image": "/uploads/home/00-Space-Exploration-Sommer-Camp-599962e5.jpg",
"slug": "space-exploration"
},
{
"id": "spanishcourse",
"title": "Spanishcourses",
"price": "0",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Portugal",
"image": "/uploads/home/Spanischcamp-in-Spanien-d118b0e9.jpg",
"slug": "spanishcourse"
},
{
"id": "survival",
"title": "Survival",
"price": "560",
"seasons": ["spring", "summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Vietnam",
"image": "/uploads/home/00-Survival%20im%20Feriencamp-28694148.jpg",
"slug": "survival-camps"
},
{
"id": "swimming",
"title": "Swimming",
"price": "490",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/Schwimmen_camp-00683667.jpg",
"slug": "swimming"
},
{
"id": "tennis",
"title": "Tennis",
"price": "695",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Malaysia",
"image": "/uploads/home/00-Tenniscamp-57cd2c79.jpg",
"slug": "tennis"
},
{
"id": "windsurfing",
"title": "Windsurfing",
"price": "0",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Thailand",
"image": "/uploads/home/00-Windsurfen-im-Sommercamp-f9c58dd4.webp",
"slug": "windsurfing"
} }
] ]
}, },
"newsletter": { "blogPreview": {
"title": "Stay Updated with Our Monthly", "heading": "Latest Insights & Updates",
"subtitle": "Newsletter", "subheading": "Visa Tips & Guides",
"description": "Sign up to receive the latest news about new camps, activities, and exciting opportunities. Don't miss out on anything fun!", "ctaButton": {
"image": "/uploads/home/b10.jpg", "label": "view all articles",
"decorativeImage": "/uploads/home/footer-shape.png", "href": "/blog"
"button": { },
"label": "Subscribe", "items": [
"placeholder": "Enter your email address", {
"href": "/booking" "title": "Step-by-Step Guide to Applying for a Student Visa",
} "excerpt": "Learn the complete process of applying for a student visa, from gathering documents to attending your interview. Our comprehensive guide covers everything you need to know.",
}, "category": "Student Visa",
"latestPosts": { "date": "2025-08-20",
"title": "- Your next step", "author": {
"subtitle": "Read Every News & Blog", "name": "Sohel",
"searchPlaceholder": "Search...", "avatar": "/assets/img/home-1/news/client.png"
"sidebarTitle": "Latest Posts", },
"blogPosts": [ "comments": 8,
{ "link": "/blog/step-by-step-guide-student-visa",
"id": 1, "thumbnail": "/assets/img/home-1/news/news-1.jpg"
"image": "/uploads/home/b1.jpg", },
"title": "Power of Consistency", {
"description": "Customized training programs to enhance skills and improve team performance.", "title": "Tips to Prepare Financial Documents for Visa Approval",
"date": "June 30, 2025" "excerpt": "Financial documentation is crucial for visa approval. Discover expert tips on preparing bank statements, sponsorship letters, and proof of funds that meet embassy requirements.",
}, "category": "IELTS / TOEFL",
{ "date": "2025-08-20",
"id": 2, "author": {
"image": "/uploads/home/b2.jpg", "name": "Sohel",
"title": "You Need to Know", "avatar": "/assets/img/home-1/news/client.png"
"description": "Expert project management ensuring timely delivery and budget compliance.", },
"date": "June 30, 2025" "comments": 8,
}, "link": "/blog/financial-documents-visa-approval",
{ "thumbnail": "/assets/img/home-1/news/news-2.jpg"
"id": 3, },
"image": "/uploads/home/b3.jpg", {
"title": "Common Mistakes", "title": "Post-Arrival Guide What Every Student Should Know",
"description": "Comprehensive marketing strategies focused on increasing brand awareness and sales.", "excerpt": "Moving to a new country can be overwhelming. Our post-arrival guide helps international students navigate accommodation, banking, healthcare, and cultural adaptation successfully.",
"date": "June 30, 2025" "category": "Study Abroad",
}, "date": "2025-08-20",
{ "author": {
"id": 4, "name": "Sohel",
"image": "/uploads/home/b4.jpg", "avatar": "/assets/img/home-1/news/client.png"
"title": "Quality Always Wins", },
"description": "Innovative design services that bring your creative visions to life.", "comments": 8,
"date": "June 30, 2025" "link": "/blog/post-arrival-guide-students",
}, "thumbnail": "/assets/img/home-1/news/news-3.jpg"
{
"id": 5,
"image": "/uploads/home/b5.jpg",
"title": "Tips You Should Know",
"description": "Reliable customer support designed to provide fast and effective solutions.",
"date": "June 30, 2025"
},
{
"id": 6,
"image": "/uploads/home/b6.jpg",
"title": "Make the Most of It",
"description": "Professional consulting tailored to meet your unique business challenges and goals.",
"date": "May 31, 2025"
}
],
"sidebarPosts": [
{
"id": 1,
"image": "/uploads/home/b7.jpg",
"title": "Make the Most of It",
"description": "Professional consulting tailored to meet your unique business challenges."
},
{
"id": 2,
"image": "/uploads/home/b8.jpg",
"title": "Tips You Should Know",
"description": "Reliable customer support designed to provide fast and effective solutions."
},
{
"id": 3,
"image": "/uploads/home/b1.jpg",
"title": "Quality Always Wins",
"description": "Innovative design services that bring your creative visions to life."
}
],
"featuredCard": {
"image": "/uploads/home/b2.jpg",
"title": "Comfort Plus",
"description": "Expert project management ensuring timely delivery and budget compliance."
} }
]
} }
} }

View File

@@ -1,177 +1,231 @@
const mongoose = require('mongoose'); const mongoose = require("mongoose");
const homeSchema = new mongoose.Schema({ const { Schema } = mongoose;
// 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: { // Reusable small schemas
title: { type: String, default: '' }, const LinkSchema = new Schema(
subtitle: { type: String, default: '' }, {
description: { type: String, default: '' }, label: { type: String, default: "" },
image: { type: String, default: '' }, href: { type: String, default: "" },
decorativeImage: { type: String, default: '' },
button: {
label: { type: String, default: '' },
placeholder: { type: String, default: '' },
href: { type: String, default: '' }
}
}, },
latestPosts: { { _id: false },
title: { type: String, default: '' }, );
subtitle: { type: String, default: '' },
searchPlaceholder: { type: String, default: '' }, const HeroSchema = new Schema(
sidebarTitle: { type: String, default: '' }, {
blogPosts: [{ title: { type: String, default: "" },
id: { type: Number }, subtitle: { type: String, default: "" },
image: { type: String, default: '' }, description: { type: String, default: "" },
title: { type: String, default: '' }, primaryButton: { type: LinkSchema, default: () => ({}) },
description: { type: String, default: '' }, secondaryButton: { type: LinkSchema, default: () => ({}) },
date: { type: String, default: '' } backgroundImage: { type: String, default: "" },
}], videoUrl: { type: String, default: "" },
sidebarPosts: [{ },
id: { type: Number }, { _id: false },
image: { type: String, default: '' }, );
title: { type: String, default: '' },
description: { type: String, default: '' } const WhyChooseUsItemSchema = new Schema(
}], {
featuredCard: { icon: { type: String, default: "" },
image: { type: String, default: '' }, title: { type: String, default: "" },
title: { type: String, default: '' }, description: { type: String, default: "" },
description: { type: String, default: '' } },
} { _id: false },
} );
}, {
timestamps: true const WhyChooseUsSchema = new Schema(
}); {
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
description: { type: String, default: "" },
items: { type: [WhyChooseUsItemSchema], default: [] },
features: { type: [String], default: [] },
ctaButton: { type: LinkSchema, default: () => ({}) },
},
{ _id: false },
);
const VisaSolutionItemSchema = new Schema(
{
number: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
link: { type: String, default: "" },
},
{ _id: false },
);
const VisaSolutionsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
items: { type: [VisaSolutionItemSchema], default: [] },
},
{ _id: false },
);
const VisaCountrySchema = new Schema(
{
name: { type: String, default: "" },
code: { type: String, default: "" },
flag: { type: String, default: "" },
link: { type: String, default: "" },
visaTypes: { type: [String], default: [] },
},
{ _id: false },
);
const VisaCountriesSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
description: { type: String, default: "" },
countries: { type: [VisaCountrySchema], default: [] },
ctaButton: { type: LinkSchema, default: () => ({}) },
},
{ _id: false },
);
const TestimonialSchema = new Schema(
{
name: { type: String, default: "" },
role: { type: String, default: "" },
country: { type: String, default: "" },
rating: { type: Number, default: 5 },
comment: { type: String, default: "" },
avatar: { type: String, default: "" },
},
{ _id: false },
);
const TestimonialsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
videoUrl: { type: String, default: "" },
videoThumbnail: { type: String, default: "" },
items: { type: [TestimonialSchema], default: [] },
},
{ _id: false },
);
const VideoGallerySchema = new Schema(
{
heading: { type: String, default: "" },
videoUrl: { type: String, default: "" },
thumbnail: { type: String, default: "" },
},
{ _id: false },
);
const FaqItemSchema = new Schema(
{
question: { type: String, default: "" },
answer: { type: String, default: "" },
},
{ _id: false },
);
const FaqSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
description: { type: String, default: "" },
ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [FaqItemSchema], default: [] },
},
{ _id: false },
);
const AchievementItemSchema = new Schema(
{
value: { type: String, default: "" },
suffix: { type: String, default: "" },
label: { type: String, default: "" },
description: { type: String, default: "" },
},
{ _id: false },
);
const AchievementsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
items: { type: [AchievementItemSchema], default: [] },
},
{ _id: false },
);
const PartnerItemSchema = new Schema(
{
name: { type: String, default: "" },
logo: { type: String, default: "" },
year: { type: String, default: "" },
},
{ _id: false },
);
const PartnersSchema = new Schema(
{
heading: { type: String, default: "" },
items: { type: [PartnerItemSchema], default: [] },
},
{ _id: false },
);
const BlogPreviewItemSchema = new Schema(
{
title: { type: String, default: "" },
excerpt: { type: String, default: "" },
category: { type: String, default: "" },
date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20")
author: {
name: { type: String, default: "" },
avatar: { type: String, default: "" },
},
comments: { type: Number, default: 0 },
link: { type: String, default: "" },
thumbnail: { type: String, default: "" },
},
{ _id: false },
);
const BlogPreviewSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [BlogPreviewItemSchema], default: [] },
},
{ _id: false },
);
/**
* Home page content model
*
* NOTE:
* - This schema is based on `hailearning.edu.vn/app/home.json`.
* - `strict: false` keeps backward compatibility with any existing CMS-only sections
* (e.g. about/missionVision/programs/newsletter/latestPosts...) that the admin UI might still send.
*/
const HomeSchema = new Schema(
{
hero: { type: HeroSchema, default: () => ({}) },
whyChooseUs: { type: WhyChooseUsSchema, default: () => ({}) },
visaSolutions: { type: VisaSolutionsSchema, default: () => ({}) },
visaCountries: { type: VisaCountriesSchema, default: () => ({}) },
testimonials: { type: TestimonialsSchema, default: () => ({}) },
videoGallery: { type: VideoGallerySchema, default: () => ({}) },
faq: { type: FaqSchema, default: () => ({}) },
achievements: { type: AchievementsSchema, default: () => ({}) },
partners: { type: PartnersSchema, default: () => ({}) },
blogPreview: { type: BlogPreviewSchema, default: () => ({}) },
},
{
timestamps: true,
strict: false,
},
);
module.exports = mongoose.model("Home", HomeSchema);
module.exports = mongoose.model('Home', homeSchema);

View File

@@ -0,0 +1,47 @@
require("dotenv").config();
const fs = require("fs").promises;
const path = require("path");
const connectDB = require("../config/database");
/**
* Migration: import_home_content
* Created: 19:00:00 2026-02-05
* Description:
* Import nội dung trang Home từ file JSON (Next.js) vào MongoDB (model Home).
* Nguồn dữ liệu: hailearning.edu.vn/app/home.json
*/
async function migrate() {
try {
// 1) Connect DB
await connectDB();
console.log("🚀 Starting migration: import_home_content...");
// 2) Load model
const Home = require("../models/home");
console.log("✅ Home model registered successfully");
// 3) Load JSON data
const dataPath = path.join(__dirname, "..", "..", "hailearning.edu.vn", "app", "home.json");
const raw = await fs.readFile(dataPath, "utf8");
const homeData = JSON.parse(raw);
console.log("📖 Home data loaded from:", dataPath);
// 4) Clear existing
console.log("🧹 Clearing existing Home data...");
await Home.deleteMany({});
console.log("✅ Existing Home documents cleared");
// 5) Insert new document
const created = await Home.create(homeData);
console.log("✅ Home document created with _id:", created._id.toString());
console.log("🎉 Migration import_home_content completed successfully.");
process.exit(0);
} catch (err) {
console.error("❌ Migration failed:", err);
process.exit(1);
}
}
migrate();

View File

@@ -21,17 +21,17 @@ const migrateHeader = async () => {
// Transform and insert data // Transform and insert data
const headerDocument = { const headerDocument = {
top: { top: {
phone: headerData.top.phone, phone: headerData.top?.phone || "",
email: headerData.top.email, email: headerData.top?.email || "",
location: headerData.top.location, location: headerData.top?.location || "",
socialLinks: headerData.top.socialLinks.map((link, idx) => ({ socialLinks: (headerData.top?.socialLinks || []).map((link, idx) => ({
...link, ...link,
order: idx, order: idx,
})), })),
languages: headerData.top.languages, languages: headerData.top?.languages || [],
}, },
offcanvas: headerData.offcanvas, offcanvas: headerData.offcanvas || {},
menu: headerData.menu.map((item, idx) => ({ menu: (headerData.menu || []).map((item, idx) => ({
...item, ...item,
order: idx, order: idx,
children: children: