forked from UKSOURCE/cms.hailearning.edu.vn
refactor: restructure home model and update homeController
This commit is contained in:
@@ -1,315 +1,249 @@
|
||||
const { addBaseUrlToImages } = require('../utils/imageHelper');
|
||||
const Home = require('../models/home');
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Home = require("../models/home");
|
||||
|
||||
// -------------------- Helper Functions --------------------
|
||||
// -------------------- Helpers --------------------
|
||||
|
||||
// Get home data from MongoDB
|
||||
const getHomeData = async () => {
|
||||
const home = await Home.findOne().sort({ updatedAt: -1 }).lean();
|
||||
return home || {};
|
||||
const getHomeDoc = async () => {
|
||||
// Keep newest document as the source of truth
|
||||
return await Home.findOne().sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
// 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 = () => ({
|
||||
hero: {
|
||||
title: '',
|
||||
description: '',
|
||||
backgroundImage: '',
|
||||
button: { label: 'Book Your Adventure', href: '/booking' },
|
||||
title: "",
|
||||
description: "",
|
||||
backgroundImage: "",
|
||||
button: { label: "Book Your Adventure", href: "/booking" },
|
||||
contactBox: {
|
||||
welcomeText: '',
|
||||
phone: { label: 'Call us', number: '', href: '' },
|
||||
email: { label: 'Email', address: '', href: '' },
|
||||
workingHours: { label: 'Working Hours', hours: '' }
|
||||
}
|
||||
welcomeText: "",
|
||||
phone: { label: "Call us", number: "", href: "" },
|
||||
email: { label: "Email", address: "", href: "" },
|
||||
workingHours: { label: "Working Hours", hours: "" },
|
||||
},
|
||||
},
|
||||
about: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
images: { mainImage1: '', mainImage2: '', avatars: [] },
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
images: { mainImage1: "", mainImage2: "", avatars: [] },
|
||||
features: [],
|
||||
quote: '',
|
||||
button: { label: '', href: '' },
|
||||
stats: { customerCount: 0, customerLabel: '' }
|
||||
quote: "",
|
||||
button: { label: "", href: "" },
|
||||
stats: { customerCount: 0, customerLabel: "" },
|
||||
},
|
||||
missionVision: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
backgroundImage: '',
|
||||
cards: []
|
||||
title: "",
|
||||
subtitle: "",
|
||||
backgroundImage: "",
|
||||
cards: [],
|
||||
},
|
||||
whyChooseUs: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
button: { label: '', href: '' },
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
button: { label: "", href: "" },
|
||||
features: [],
|
||||
tags: [],
|
||||
cta: { text: '', linkText: '', linkHref: '' }
|
||||
},
|
||||
activities: {
|
||||
cards: []
|
||||
cta: { text: "", linkText: "", linkHref: "" },
|
||||
},
|
||||
activities: { cards: [] },
|
||||
faq: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
image: '',
|
||||
contact: { title: '', info: '' },
|
||||
questions: []
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
image: "",
|
||||
contact: { title: "", info: "" },
|
||||
questions: [],
|
||||
},
|
||||
partners: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
backgroundImage: '',
|
||||
title: "",
|
||||
subtitle: "",
|
||||
backgroundImage: "",
|
||||
logos: [],
|
||||
cta: { badge: '', text: '', linkText: '', linkHref: '' }
|
||||
cta: { badge: "", text: "", linkText: "", linkHref: "" },
|
||||
},
|
||||
programs: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
button: { label: '', href: '' },
|
||||
title: "",
|
||||
subtitle: "",
|
||||
button: { label: "", href: "" },
|
||||
card: {
|
||||
pricePrefix: 'from',
|
||||
priceSuffix: 'USD',
|
||||
buttonLabel: 'Camp Detail',
|
||||
buttonHref: '/camp-profiles'
|
||||
pricePrefix: "from",
|
||||
priceSuffix: "USD",
|
||||
buttonLabel: "Camp Detail",
|
||||
buttonHref: "/camp-profiles",
|
||||
},
|
||||
items: []
|
||||
items: [],
|
||||
},
|
||||
newsletter: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
image: '',
|
||||
decorativeImage: '',
|
||||
button: {
|
||||
label: '',
|
||||
placeholder: '',
|
||||
href: ''
|
||||
}
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
image: "",
|
||||
decorativeImage: "",
|
||||
button: { label: "", placeholder: "", href: "" },
|
||||
},
|
||||
latestPosts: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
searchPlaceholder: '',
|
||||
sidebarTitle: '',
|
||||
title: "",
|
||||
subtitle: "",
|
||||
searchPlaceholder: "",
|
||||
sidebarTitle: "",
|
||||
blogPosts: [],
|
||||
sidebarPosts: [],
|
||||
featuredCard: { image: '', title: '', description: '' }
|
||||
}
|
||||
featuredCard: { image: "", title: "", description: "" },
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------- Admin --------------------
|
||||
|
||||
|
||||
// -------------------- Admin Exports --------------------
|
||||
|
||||
|
||||
// Display home management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
// Fetch Home data
|
||||
let data = await getHomeData();
|
||||
|
||||
// If no data exists, use default
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
data = getDefaultHomeData();
|
||||
} else {
|
||||
// Merge with defaults to ensure all fields exist
|
||||
const defaultData = getDefaultHomeData();
|
||||
|
||||
// Ensure all sections exist with defaults
|
||||
data.hero = data.hero || defaultData.hero;
|
||||
data.about = data.about || defaultData.about;
|
||||
data.missionVision = data.missionVision || defaultData.missionVision;
|
||||
data.whyChooseUs = data.whyChooseUs || defaultData.whyChooseUs;
|
||||
data.activities = data.activities || defaultData.activities;
|
||||
data.faq = data.faq || defaultData.faq;
|
||||
data.partners = data.partners || defaultData.partners;
|
||||
data.programs = data.programs || defaultData.programs;
|
||||
data.newsletter = data.newsletter || defaultData.newsletter;
|
||||
data.latestPosts = data.latestPosts || defaultData.latestPosts;
|
||||
// Merge minimal defaults to keep the view safe
|
||||
const defaults = getDefaultHomeData();
|
||||
data.hero = data.hero || defaults.hero;
|
||||
data.about = data.about || defaults.about;
|
||||
data.missionVision = data.missionVision || defaults.missionVision;
|
||||
data.whyChooseUs = data.whyChooseUs || defaults.whyChooseUs;
|
||||
data.activities = data.activities || defaults.activities;
|
||||
data.faq = data.faq || defaults.faq;
|
||||
data.partners = data.partners || defaults.partners;
|
||||
data.programs = data.programs || defaults.programs;
|
||||
data.newsletter = data.newsletter || defaults.newsletter;
|
||||
data.latestPosts = data.latestPosts || defaults.latestPosts;
|
||||
}
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||
|
||||
res.render('admin/home/index', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Home Management',
|
||||
return res.render("admin/home/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Home Management",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Home index error:', err);
|
||||
req.flash('error_msg', 'Error loading home data');
|
||||
res.redirect('/admin/dashboard');
|
||||
console.error("Home index error:", err);
|
||||
req.flash("error_msg", "Error loading home data");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
};
|
||||
|
||||
// Update home data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
// Get current data
|
||||
const currentData = await getHomeData();
|
||||
const currentDoc = await getHomeDoc();
|
||||
const currentData = currentDoc ? currentDoc.toObject() : {};
|
||||
const updatedData = { ...currentData };
|
||||
|
||||
// Create updated data object
|
||||
const updatedData = { ...(currentData.toObject ? currentData.toObject() : currentData) };
|
||||
|
||||
// Update Hero section data (from Welcome tab)
|
||||
// Quick fields (Hero) from classic form fields
|
||||
if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) {
|
||||
updatedData.hero = {
|
||||
title: req.body.heroTitle || '',
|
||||
description: req.body.heroDescription || '',
|
||||
backgroundImage: req.body.heroBackgroundImage || '',
|
||||
title: req.body.heroTitle || "",
|
||||
description: req.body.heroDescription || "",
|
||||
backgroundImage: req.body.heroBackgroundImage || "",
|
||||
button: {
|
||||
label: req.body.heroButtonLabel || 'Book Your Adventure',
|
||||
href: req.body.heroButtonHref || '/booking'
|
||||
label: req.body.heroButtonLabel || "Book Your Adventure",
|
||||
href: req.body.heroButtonHref || "/booking",
|
||||
},
|
||||
contactBox: {
|
||||
welcomeText: req.body.heroContactWelcome || '',
|
||||
welcomeText: req.body.heroContactWelcome || "",
|
||||
phone: {
|
||||
label: 'Call us',
|
||||
number: req.body.heroContactPhone || '',
|
||||
href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : ''
|
||||
label: "Call us",
|
||||
number: req.body.heroContactPhone || "",
|
||||
href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : "",
|
||||
},
|
||||
email: {
|
||||
label: 'Email',
|
||||
address: req.body.heroContactEmail || '',
|
||||
href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : ''
|
||||
label: "Email",
|
||||
address: req.body.heroContactEmail || "",
|
||||
href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : "",
|
||||
},
|
||||
workingHours: {
|
||||
label: 'Working Hours',
|
||||
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 || ''
|
||||
label: "Working Hours",
|
||||
hours: req.body.heroContactHours || "",
|
||||
},
|
||||
},
|
||||
features: updatedData.whyChooseUs?.features || [],
|
||||
tags: updatedData.whyChooseUs?.tags || [],
|
||||
cta: updatedData.whyChooseUs?.cta || { text: '', linkText: '', linkHref: '' }
|
||||
};
|
||||
}
|
||||
|
||||
// Handle Home sections (new camp structure only)
|
||||
const sections = ['hero', 'about', 'missionVision', 'whyChooseUs', 'activities', 'faq',
|
||||
'partners', 'programs', 'newsletter', 'latestPosts'];
|
||||
const errors = [];
|
||||
// Handle sections sent as JSON payloads
|
||||
const sections = [
|
||||
"hero",
|
||||
"about",
|
||||
"missionVision",
|
||||
"whyChooseUs",
|
||||
"activities",
|
||||
"faq",
|
||||
"partners",
|
||||
"programs",
|
||||
"newsletter",
|
||||
"latestPosts",
|
||||
];
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
// Process each section
|
||||
for (const section of sections) {
|
||||
if (!req.body[section]) continue;
|
||||
|
||||
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 currentSectionData = currentData?.[section];
|
||||
|
||||
// Check for changes
|
||||
const currentSectionData = currentData[section];
|
||||
const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData);
|
||||
|
||||
if (sectionHasChanges) {
|
||||
if (JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData)) {
|
||||
updatedData[section] = newSectionData;
|
||||
hasChanges = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing section ${section}:`, error);
|
||||
errors.push(`Error processing ${section} data: ${error.message}`);
|
||||
} catch (e) {
|
||||
console.error(`Error processing section "${section}":`, e);
|
||||
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) {
|
||||
req.flash('info_msg', 'No changes were made');
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
req.flash("info_msg", "No changes were made");
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
}
|
||||
|
||||
// Update or create document
|
||||
try {
|
||||
if (currentData._id) {
|
||||
await Home.findByIdAndUpdate(currentData._id, updatedData, { new: true });
|
||||
} else {
|
||||
await Home.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash('success_msg', 'Home data updated successfully');
|
||||
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'));
|
||||
if (currentDoc?._id) {
|
||||
await Home.findByIdAndUpdate(currentDoc._id, updatedData, { new: true });
|
||||
} else {
|
||||
await Home.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Home data updated successfully");
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
} catch (err) {
|
||||
console.error('Update error:', err);
|
||||
req.flash('error_msg', `Update error: ${err.message || 'Unknown'}`);
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
console.error("Home update error:", err);
|
||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
||||
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) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
res.json(processedData);
|
||||
return res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error('Home API error:', err);
|
||||
res.status(500).json({ error: 'Error loading home data' });
|
||||
console.error("Home API error:", err);
|
||||
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' });
|
||||
}
|
||||
};
|
||||
732
data/home.json
732
data/home.json
@@ -1,537 +1,339 @@
|
||||
{
|
||||
|
||||
"hero": {
|
||||
"title": "Discover Adventure and Friendship",
|
||||
"description": "Step into a world where adventure meets comfort. Discover breathtaking landscapes, thrilling outdoor activities, and the serenity of luxury camping.",
|
||||
"backgroundImage": "/uploads/home/b2.jpg",
|
||||
"button": {
|
||||
"label": "Book Your Adventure",
|
||||
"href": "/booking"
|
||||
"title": "From Application to Visa – We've Got You Covered",
|
||||
"subtitle": "Global Education Simplified",
|
||||
"description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.",
|
||||
"primaryButton": {
|
||||
"label": "Apply now",
|
||||
"href": "/contact"
|
||||
},
|
||||
"contactBox": {
|
||||
"welcomeText": "Your Adventure Journey Start Here!",
|
||||
"phone": {
|
||||
"label": "Call us",
|
||||
"number": "+(123) 456 789",
|
||||
"href": "tel:+123456789"
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"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"
|
||||
]
|
||||
"secondaryButton": {
|
||||
"label": "Book Free Consultation",
|
||||
"href": "/contact"
|
||||
},
|
||||
"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."
|
||||
}
|
||||
]
|
||||
"backgroundImage": "/assets/img/home-1/hero/bg.jpg",
|
||||
"videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I"
|
||||
},
|
||||
"whyChooseUs": {
|
||||
"title": "- Why Choose Us",
|
||||
"subtitle": "Creating unforgettable camp experiences with safety, fun, and friendship.",
|
||||
"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.",
|
||||
"button": {
|
||||
"label": "Explore Now",
|
||||
"href": "/booking"
|
||||
},
|
||||
"features": [
|
||||
"heading": "Turning Study Abroad Dreams Into Reality",
|
||||
"subheading": "About Our Consultancy",
|
||||
"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.",
|
||||
"items": [
|
||||
{
|
||||
"title": "Inclusive & Welcoming",
|
||||
"description": "Every child, teen, and staff member, regardless of country or culture, feels comfortable and valued, creating a unique and unforgettable camp experience."
|
||||
"icon": "/assets/img/home-1/icon/01.svg",
|
||||
"title": "Global Reach",
|
||||
"description": "Expanding Opportunities Worldwide"
|
||||
},
|
||||
{
|
||||
"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."
|
||||
"icon": "/assets/img/home-1/icon/01.svg",
|
||||
"title": "Expert Guidance",
|
||||
"description": "Professional Support Every Step"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Nature-Friendly",
|
||||
"Adventure-Ready",
|
||||
"Community Service",
|
||||
"Inspiring Locations"
|
||||
"features": [
|
||||
"Fastest Visa form processing with skilled immigration agents",
|
||||
"Partnership with International Educational Institutions"
|
||||
],
|
||||
"cta": {
|
||||
"text": "Let's make something great work together.",
|
||||
"linkText": "Get Free Quote",
|
||||
"linkHref": "#"
|
||||
"ctaButton": {
|
||||
"label": "Get Started",
|
||||
"href": "/about"
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"cards": [
|
||||
"visaSolutions": {
|
||||
"heading": "Comprehensive Visa Solutions",
|
||||
"subheading": "Our Expert Services",
|
||||
"items": [
|
||||
{
|
||||
"title": "Surfing Adventures",
|
||||
"description": "Catch the waves and learn water safety while having a blast on the beach.",
|
||||
"image": "/uploads/home/b13.jpg"
|
||||
"number": "01",
|
||||
"title": "Student Visa Guidance",
|
||||
"description": "Assistance with admission, documentation, and visa application.Assistance",
|
||||
"link": "/service-details"
|
||||
},
|
||||
{
|
||||
"title": "River Kayaking",
|
||||
"description": "Paddle along scenic rivers, surrounded by wildlife and stunning landscapes.",
|
||||
"image": "/uploads/home/b14.jpg"
|
||||
"number": "02",
|
||||
"title": "PTE Exam Preparation",
|
||||
"description": "We provide expert guidance and personalized support throughout the education visa process,",
|
||||
"link": "/service-details"
|
||||
},
|
||||
{
|
||||
"title": "Campfire Nights",
|
||||
"description": "Gather around the fire, roast marshmallows, and share stories under the stars",
|
||||
"image": "/uploads/home/b16.jpg"
|
||||
"number": "03",
|
||||
"title": "University Selection Assistance",
|
||||
"description": "We provide expert guidance and personalized support throughout the education visa process,",
|
||||
"link": "/service-details"
|
||||
},
|
||||
{
|
||||
"title": "Community Service Projects",
|
||||
"description": "Participate in meaningful activities such as beach clean-ups, tree planting, and helping local community initiatives.",
|
||||
"image": "/uploads/home/b11.jpg"
|
||||
"number": "04",
|
||||
"title": "IELTS Exam Preparation",
|
||||
"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": {
|
||||
"title": "- Frequently Asked Questions",
|
||||
"subtitle": "Essential Camp Info",
|
||||
"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.",
|
||||
"image": "/uploads/home/b5.jpg",
|
||||
"contact": {
|
||||
"title": "Need Any Help?",
|
||||
"info": "+(123) 456-789 | info@hailearning.edu.vn"
|
||||
"heading": "Got Questions? We've Got Answers",
|
||||
"subheading": "Visa FAQs",
|
||||
"description": "We understand students often have many questions about studying abroad. Our experts provide clear.",
|
||||
"ctaButton": {
|
||||
"label": "contact us",
|
||||
"href": "/contact"
|
||||
},
|
||||
"questions": [
|
||||
"items": [
|
||||
{
|
||||
"question": "Safety & Supervision?",
|
||||
"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."
|
||||
"question": "How long does the student visa process usually take?",
|
||||
"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?",
|
||||
"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."
|
||||
"question": "Do you assist with scholarship applications as well?",
|
||||
"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?",
|
||||
"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."
|
||||
"question": "Will you guide me in preparing for the visa interview?",
|
||||
"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?",
|
||||
"answer": "Absolutely! Each site has cooking facilities including BBQ grills and fire pits. You're welcome to bring your own food and beverages."
|
||||
"question": "Do you offer post-arrival support for students?",
|
||||
"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?",
|
||||
"answer": "We offer hiking, kayaking, rock climbing, mountain biking, fishing, and guided nature tours. Activities vary by location and season."
|
||||
"question": "What documents are required for a student visa application?",
|
||||
"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?",
|
||||
"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."
|
||||
"value": "50",
|
||||
"suffix": "+",
|
||||
"label": "Countries Covered",
|
||||
"description": "Helping students apply to universities in more than 50 countries."
|
||||
},
|
||||
{
|
||||
"question": "What is the refund policy?",
|
||||
"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."
|
||||
"value": "95",
|
||||
"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": {
|
||||
"title": "- Our 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"
|
||||
},
|
||||
"heading": "Our Trusted Partners",
|
||||
"items": [
|
||||
{
|
||||
"id": "adventure-sports-creative",
|
||||
"title": "Adventure, Sports & Creative",
|
||||
"price": "395",
|
||||
"seasons": ["spring", "summer", "autumn"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Thailand",
|
||||
"image": "/uploads/home/b5.jpg",
|
||||
"slug": "adventure-sports-creative"
|
||||
"name": "Best Visa Consultancy",
|
||||
"logo": "/assets/img/home-1/feature/icon-1.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "arts-crafts",
|
||||
"title": "Arts & Crafts",
|
||||
"price": "500",
|
||||
"seasons": ["spring", "summer", "autumn"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Vietnam",
|
||||
"image": "/uploads/home/b6.jpg",
|
||||
"slug": "arts-crafts"
|
||||
"name": "Visa Success Award",
|
||||
"logo": "/assets/img/home-1/feature/icon-2.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "climbing",
|
||||
"title": "Climbing",
|
||||
"price": "515",
|
||||
"seasons": ["summer"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Philippines",
|
||||
"image": "/uploads/home/b1.jpg",
|
||||
"slug": "climbing"
|
||||
"name": "Innovation Award",
|
||||
"logo": "/assets/img/home-1/feature/icon-3.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "dancing",
|
||||
"title": "Dancing",
|
||||
"price": "520",
|
||||
"seasons": ["summer", "autumn"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Malaysia",
|
||||
"image": "/uploads/home/b4.jpg",
|
||||
"slug": "dancing"
|
||||
"name": "Global Education Partner",
|
||||
"logo": "/assets/img/home-1/feature/icon-4.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "diving",
|
||||
"title": "Diving",
|
||||
"price": "1190",
|
||||
"seasons": ["summer"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Philippines",
|
||||
"image": "/uploads/home/b2.jpg",
|
||||
"slug": "diving"
|
||||
"name": "University Partner 1",
|
||||
"logo": "/assets/img/home-1/brand/01.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "englisch-toefl",
|
||||
"title": "Englisch TOEFL®",
|
||||
"price": "1290",
|
||||
"seasons": ["spring", "summer"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Malaysia",
|
||||
"image": "/uploads/home/b1.jpg",
|
||||
"slug": "englisch-toefl"
|
||||
"name": "University Partner 2",
|
||||
"logo": "/assets/img/home-1/brand/02.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "englischcamps",
|
||||
"title": "Englischcamps",
|
||||
"price": "530",
|
||||
"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"
|
||||
"name": "University Partner 3",
|
||||
"logo": "/assets/img/home-1/brand/03.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "fishing",
|
||||
"title": "Fishing",
|
||||
"price": "580",
|
||||
"seasons": ["spring", "summer", "autumn"],
|
||||
"age": "From 12 - 18 years old",
|
||||
"location": "Vietnam",
|
||||
"image": "/uploads/home/01-Angeln-im-Ferienlager-02243939.jpg",
|
||||
"slug": "fishing"
|
||||
"name": "University Partner 4",
|
||||
"logo": "/assets/img/home-1/brand/04.png",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"id": "german-camps",
|
||||
"title": "German Camps",
|
||||
"price": "610",
|
||||
"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"
|
||||
"name": "University Partner 5",
|
||||
"logo": "/assets/img/home-1/brand/05.png",
|
||||
"year": "2025"
|
||||
}
|
||||
]
|
||||
},
|
||||
"newsletter": {
|
||||
"title": "Stay Updated with Our Monthly",
|
||||
"subtitle": "Newsletter",
|
||||
"description": "Sign up to receive the latest news about new camps, activities, and exciting opportunities. Don't miss out on anything fun!",
|
||||
"image": "/uploads/home/b10.jpg",
|
||||
"decorativeImage": "/uploads/home/footer-shape.png",
|
||||
"button": {
|
||||
"label": "Subscribe",
|
||||
"placeholder": "Enter your email address",
|
||||
"href": "/booking"
|
||||
}
|
||||
},
|
||||
"latestPosts": {
|
||||
"title": "- Your next step",
|
||||
"subtitle": "Read Every News & Blog",
|
||||
"searchPlaceholder": "Search...",
|
||||
"sidebarTitle": "Latest Posts",
|
||||
"blogPosts": [
|
||||
"blogPreview": {
|
||||
"heading": "Latest Insights & Updates",
|
||||
"subheading": "Visa Tips & Guides",
|
||||
"ctaButton": {
|
||||
"label": "view all articles",
|
||||
"href": "/blog"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"image": "/uploads/home/b1.jpg",
|
||||
"title": "Power of Consistency",
|
||||
"description": "Customized training programs to enhance skills and improve team performance.",
|
||||
"date": "June 30, 2025"
|
||||
"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",
|
||||
"date": "2025-08-20",
|
||||
"author": {
|
||||
"name": "Sohel",
|
||||
"avatar": "/assets/img/home-1/news/client.png"
|
||||
},
|
||||
"comments": 8,
|
||||
"link": "/blog/step-by-step-guide-student-visa",
|
||||
"thumbnail": "/assets/img/home-1/news/news-1.jpg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"image": "/uploads/home/b2.jpg",
|
||||
"title": "You Need to Know",
|
||||
"description": "Expert project management ensuring timely delivery and budget compliance.",
|
||||
"date": "June 30, 2025"
|
||||
"title": "Tips to Prepare Financial Documents for Visa Approval",
|
||||
"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",
|
||||
"author": {
|
||||
"name": "Sohel",
|
||||
"avatar": "/assets/img/home-1/news/client.png"
|
||||
},
|
||||
"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",
|
||||
"description": "Comprehensive marketing strategies focused on increasing brand awareness and sales.",
|
||||
"date": "June 30, 2025"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"image": "/uploads/home/b4.jpg",
|
||||
"title": "Quality Always Wins",
|
||||
"description": "Innovative design services that bring your creative visions to life.",
|
||||
"date": "June 30, 2025"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"title": "Post-Arrival Guide What Every Student Should Know",
|
||||
"excerpt": "Moving to a new country can be overwhelming. Our post-arrival guide helps international students navigate accommodation, banking, healthcare, and cultural adaptation successfully.",
|
||||
"category": "Study Abroad",
|
||||
"date": "2025-08-20",
|
||||
"author": {
|
||||
"name": "Sohel",
|
||||
"avatar": "/assets/img/home-1/news/client.png"
|
||||
},
|
||||
"comments": 8,
|
||||
"link": "/blog/post-arrival-guide-students",
|
||||
"thumbnail": "/assets/img/home-1/news/news-3.jpg"
|
||||
}
|
||||
],
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
400
models/home.js
400
models/home.js
@@ -1,177 +1,231 @@
|
||||
const mongoose = require('mongoose');
|
||||
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: '' }
|
||||
}]
|
||||
},
|
||||
const { Schema } = mongoose;
|
||||
|
||||
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: '' }
|
||||
}
|
||||
// Reusable small schemas
|
||||
const LinkSchema = new Schema(
|
||||
{
|
||||
label: { 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
|
||||
});
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const HeroSchema = new Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
subtitle: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
primaryButton: { type: LinkSchema, default: () => ({}) },
|
||||
secondaryButton: { type: LinkSchema, default: () => ({}) },
|
||||
backgroundImage: { type: String, default: "" },
|
||||
videoUrl: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const WhyChooseUsItemSchema = new Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const WhyChooseUsSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
subheading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
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);
|
||||
47
scripts/2026_02_05_190000_home.js
Normal file
47
scripts/2026_02_05_190000_home.js
Normal 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();
|
||||
|
||||
@@ -21,17 +21,17 @@ const migrateHeader = async () => {
|
||||
// Transform and insert data
|
||||
const headerDocument = {
|
||||
top: {
|
||||
phone: headerData.top.phone,
|
||||
email: headerData.top.email,
|
||||
location: headerData.top.location,
|
||||
socialLinks: headerData.top.socialLinks.map((link, idx) => ({
|
||||
phone: headerData.top?.phone || "",
|
||||
email: headerData.top?.email || "",
|
||||
location: headerData.top?.location || "",
|
||||
socialLinks: (headerData.top?.socialLinks || []).map((link, idx) => ({
|
||||
...link,
|
||||
order: idx,
|
||||
})),
|
||||
languages: headerData.top.languages,
|
||||
languages: headerData.top?.languages || [],
|
||||
},
|
||||
offcanvas: headerData.offcanvas,
|
||||
menu: headerData.menu.map((item, idx) => ({
|
||||
offcanvas: headerData.offcanvas || {},
|
||||
menu: (headerData.menu || []).map((item, idx) => ({
|
||||
...item,
|
||||
order: idx,
|
||||
children:
|
||||
|
||||
Reference in New Issue
Block a user