Merge pull request 'fea/thanh-05022026-home' (#26) from fea/thanh-05022026-home into main

Reviewed-on: UKSOURCE/cms.hailearning.edu.vn#26
This commit is contained in:
2026-02-05 09:57:21 +00:00
24 changed files with 3531 additions and 4851 deletions

View File

@@ -1,315 +1,172 @@
const { addBaseUrlToImages } = require('../utils/imageHelper');
const Home = require('../models/home');
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
const Home = require("../models/home");
const Blog = require("../models/blog");
// -------------------- Helper Functions --------------------
// Các hàm hỗ trợ
const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 });
const getHomeData = async () => (await getHomeDoc())?.toObject() || {};
// Get home data from MongoDB
const getHomeData = async () => {
const home = await Home.findOne().sort({ updatedAt: -1 }).lean();
return home || {};
};
// Get default home data structure
const getDefaultHomeData = () => ({
hero: {
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: '' }
}
hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} },
whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} },
visaSolutions: { heading: "", subheading: "", items: [] },
visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} },
testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] },
videoGallery: { heading: "", videoUrl: "", thumbnail: "" },
faq: { heading: "", subheading: "", description: "", ctaButton: {}, items: [] },
achievements: { heading: "", subheading: "", items: [] },
partners: { visaConsultancy: { items: [] }, brands: { items: [] } },
blogPreview: {
heading: "Latest Insights & Updates",
subheading: "Visa Tips & Guides",
ctaButton: { label: "View All Articles", href: "/blog" },
items: [],
selectedBlogIds: [] // Array of manually selected blog IDs
},
about: {
title: '',
subtitle: '',
description: '',
images: { mainImage1: '', mainImage2: '', avatars: [] },
features: [],
quote: '',
button: { label: '', href: '' },
stats: { customerCount: 0, customerLabel: '' }
},
missionVision: {
title: '',
subtitle: '',
backgroundImage: '',
cards: []
},
whyChooseUs: {
title: '',
subtitle: '',
description: '',
button: { label: '', href: '' },
features: [],
tags: [],
cta: { text: '', linkText: '', linkHref: '' }
},
activities: {
cards: []
},
faq: {
title: '',
subtitle: '',
description: '',
image: '',
contact: { title: '', info: '' },
questions: []
},
partners: {
title: '',
subtitle: '',
backgroundImage: '',
logos: [],
cta: { badge: '', text: '', linkText: '', linkHref: '' }
},
programs: {
title: '',
subtitle: '',
button: { label: '', href: '' },
card: {
pricePrefix: 'from',
priceSuffix: 'USD',
buttonLabel: 'Camp Detail',
buttonHref: '/camp-profiles'
},
items: []
},
newsletter: {
title: '',
subtitle: '',
description: '',
image: '',
decorativeImage: '',
button: {
label: '',
placeholder: '',
href: ''
}
},
latestPosts: {
title: '',
subtitle: '',
searchPlaceholder: '',
sidebarTitle: '',
blogPosts: [],
sidebarPosts: [],
featuredCard: { image: '', title: '', description: '' }
}
});
// -------------------- Admin Exports --------------------
// Display home management page
// Admin: Xem trang quản lý
exports.index = async (req, res) => {
try {
// Fetch Home data
let data = await getHomeData();
const defaults = getDefaultHomeData();
// 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 dữ liệu mặc định cho tất cả các phần
const sections = Object.keys(defaults);
sections.forEach(s => {
data[s] = data[s] || defaults[s];
});
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/home/index', {
layout: 'layouts/main',
title: 'Home Management',
// Lấy tất cả blog để chọn trong CMS
const allBlogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).lean();
return res.render("admin/home/index", {
layout: "layouts/main",
title: "Home Management",
data,
allBlogs,
frontendUrl,
backendUrl,
getFullImageUrl,
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
// Admin: Cập nhật dữ liệu (tập trung vào achievements, partners; các phần khác giữ nguyên nếu không có dữ liệu mới)
exports.update = async (req, res) => {
try {
// Get current data
const currentData = await getHomeData();
// Create updated data object
const updatedData = { ...(currentData.toObject ? currentData.toObject() : currentData) };
// Update Hero section data (from Welcome tab)
if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) {
updatedData.hero = {
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'
},
contactBox: {
welcomeText: req.body.heroContactWelcome || '',
phone: {
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}` : ''
},
workingHours: {
label: 'Working Hours',
hours: req.body.heroContactHours || ''
}
}
};
const sections = [
"hero", "whyChooseUs", "visaSolutions", "visaCountries",
"testimonials", "videoGallery", "faq", "achievements",
"partners", "blogPreview"
];
let doc = await getHomeDoc();
if (!doc) {
doc = new Home({});
}
// 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)
const sections = ['hero', 'about', 'missionVision', 'whyChooseUs', 'activities', 'faq',
'partners', 'programs', 'newsletter', 'latestPosts'];
const errors = [];
let hasChanges = false;
// Process each section
for (const section of sections) {
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]);
// Check for changes
const currentSectionData = currentData[section];
const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData);
if (sectionHasChanges) {
updatedData[section] = newSectionData;
if (req.body[section]) {
try {
const payload = JSON.parse(req.body[section]);
// Gán trực tiếp vào doc, Mongoose sẽ tự check schema
doc[section] = payload;
doc.markModified(section);
hasChanges = true;
} catch (e) {
console.error(`Invalid JSON for ${section}:`, e);
}
} catch (error) {
console.error(`Error processing section ${section}:`, error);
errors.push(`Error processing ${section} data: ${error.message}`);
}
}
// 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'));
}
// 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'));
req.flash("info_msg", "No changes were made");
return req.session.save(() => res.redirect("/admin/home"));
}
await doc.save();
req.flash("success_msg", "Home page configuration has been updated!");
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}`);
return req.session.save(() => res.redirect("/admin/home"));
}
};
// -------------------- Public API Exports --------------------
// API to get home data for frontend
// Public API// API lấy danh sách blog cho CMS
exports.apiGetBlogs = async (req, res) => {
try {
const blogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).select("title slug featuredImage author publishedAt").lean();
res.json(blogs);
} catch (err) {
res.status(500).json({ error: err.message });
}
};
exports.api = async (req, res) => {
try {
const homeData = await getHomeData();
let data = await getHomeData();
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
// === Xử lý Blog Preview động ===
const blogPreview = data.blogPreview || {};
let blogs = [];
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const processedData = addBaseUrlToImages(homeData, baseUrl);
// Nếu có chọn blog cụ thể
if (blogPreview.selectedBlogIds && blogPreview.selectedBlogIds.length > 0) {
blogs = await Blog.find({
_id: { $in: blogPreview.selectedBlogIds },
status: "published"
}).lean();
// Sắp xếp theo thứ tự đã chọn trong selectedBlogIds
blogs.sort((a, b) => {
return blogPreview.selectedBlogIds.indexOf(a._id.toString()) - blogPreview.selectedBlogIds.indexOf(b._id.toString());
});
}
res.json(processedData);
// Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất (hoặc bù vào)
if (blogs.length === 0) {
blogs = await Blog.find({ status: "published" })
.sort({ createdAt: -1 })
.limit(3)
.lean();
}
// Map dữ liệu blog sang format mà frontend mong đợi
blogPreview.items = blogs.map(blog => ({
title: blog.title,
excerpt: blog.excerpt,
category: blog.category && blog.category[0] ? blog.category[0] : "Visa",
date: blog.publishedAt || blog.createdAt,
author: {
name: blog.author || "Admin",
avatar: "" // Frontend đang tự xử lý hoặc dùng logo hệ thống
},
comments: blog.commentsCount || 0,
link: `/blog/${blog.slug}`,
thumbnail: blog.featuredImage
}));
data.blogPreview = blogPreview;
// ===============================
const processed = addBaseUrlToImages(data, baseUrl);
return res.json(processed);
} 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' });
}
};

View File

@@ -47,9 +47,9 @@ const Visa = require("../models/visa");
const slugify = require("slugify");
const createSlug = (text) => {
return slugify(text, {
lower: true, // Chuyển về chữ thường
strict: true, // Loại bỏ ký tự đặc biệt
locale: "vi", // Xử lý tiếng Việt chuẩn
lower: true,
strict: true,
locale: "en",
trim: true,
});
};
@@ -57,7 +57,7 @@ const createSlug = (text) => {
// Get visa data from MongoDB
const getVisaData = async () => {
const visa = await Visa.findOne().sort({ updatedAt: -1 }).lean();
const visa = await Visa.findOne().sort({ updatedAt: -1 });
return visa || {};
};
@@ -324,11 +324,11 @@ exports.updateCountry = async (req, res) => {
const { id } = req.params;
let visaData = await getVisaData();
// if (!visaData || !visaData.hero || !visaData.hero.summaryList) {
// return res
// .status(400)
// .json({ error: "Cấu trúc dữ liệu Visa không hợp lệ" });
// }
if (!visaData || !visaData.hero || !visaData.hero.summaryList) {
return res
.status(400)
.json({ error: "Cấu trúc dữ liệu Visa không hợp lệ" });
}
// 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác)
const countryIndex = visaData.hero.summaryList.findIndex(
@@ -413,43 +413,51 @@ exports.updateCountry = async (req, res) => {
// Delete country
exports.deleteCountry = async (req, res) => {
try {
const { slug } = req.params;
// 1. Lấy id từ params
const { id } = req.params;
let visaData = await getVisaData();
if (!visaData.hero || !visaData.hero.summaryList) {
return res.status(400).json({ error: "Invalid visa data structure" });
if (!visaData || !visaData.hero || !visaData.hero.summaryList) {
return res
.status(400)
.json({ success: false, error: "Cấu trúc dữ liệu Visa không hợp lệ" });
}
// 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác)
const countryIndex = visaData.hero.summaryList.findIndex(
(c) => c.slug === slug,
(c) => c.id === parseInt(id),
);
if (countryIndex === -1) {
return res.status(404).json({ error: `Country "${slug}" not found` });
return res.status(404).json({
success: false,
error: `Không tìm thấy quốc gia có ID: ${id}`,
});
}
// 3. Xóa phần tử khỏi mảng
const deletedCountry = visaData.hero.summaryList[countryIndex];
visaData.hero.summaryList.splice(countryIndex, 1);
// Update database
const updatedData = {
...(visaData.toObject ? visaData.toObject() : visaData),
};
if (visaData._id) {
await Visa.findByIdAndUpdate(visaData._id, updatedData, { new: true });
} else {
await Visa.create(updatedData);
// 4. Cập nhật vào Database
if (visaData.markModified) {
visaData.markModified("hero.summaryList");
}
console.log(`✅ Country "${deletedCountry.name}" deleted successfully`);
res.json({
if (visaData._id) {
await visaData.save();
} else {
await Visa.create(visaData);
}
console.log(`✅ Deleted Successfully: "${deletedCountry.name}"`);
return res.json({
success: true,
message: `Country "${deletedCountry.name}" deleted successfully`,
message: `Country "${deletedCountry.name}" Deleted Successfully`,
});
} catch (err) {
console.error("Delete country error:", err);
res.status(500).json({ error: err.message });
console.error("❌ Error Delete:", err);
return res.status(500).json({ success: false, error: err.message });
}
};
@@ -469,9 +477,7 @@ exports.api = async (req, res) => {
const heroData = visaData?.hero;
// 2. Lấy riêng phần hero (Dùng biến mới, không gán đè vào const)
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(heroData, baseUrl);
const processedData = heroData;
return res.json({
success: true,
@@ -552,20 +558,16 @@ exports.apiCountry = async (req, res) => {
data: null,
});
}
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
// 3. Chỉ lấy phần chi tiết (detailed view)
// Lưu ý: Chúng ta copy ra object mới để tránh tham chiếu dữ liệu gốc
const detailedData = JSON.parse(JSON.stringify(country.viewDetail));
// 4. Gắn baseUrl vào các ảnh nằm trong phần chi tiết này
const processedData = addBaseUrlToImages(detailedData, baseUrl);
const processedData = detailedData;
return res.json({
success: true,
data: processedData, // Trả về nội dung của detailedView
data: processedData,
});
} catch (err) {
console.error("Visa country API error:", err);

View File

@@ -132,36 +132,11 @@
{
"label": "Blog",
"slug": "blog",
"href": "#",
"href": "/blog",
"type": "internal",
"order": 5,
"isActive": true,
"children": [
{
"label": "Blog Grid",
"slug": "blog-grid",
"href": "/blog-grid",
"type": "internal",
"order": 1,
"isActive": true
},
{
"label": "Blog Standard",
"slug": "blog-standard",
"href": "/blog",
"type": "internal",
"order": 2,
"isActive": true
},
{
"label": "Blog Details",
"slug": "blog-details",
"href": "/blog-details",
"type": "internal",
"order": 3,
"isActive": true
}
]
"children": []
},
{
"label": "Contact Us",

View File

@@ -1,537 +1,335 @@
{
"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"
"visaConsultancy": {
"heading": "Our Achievements & Awards",
"items": [
{
"name": "Best Visa Consultancy",
"icon": "/assets/img/home-1/feature/icon-1.png",
"year": "2025"
},
{
"name": "Visa Success Award",
"icon": "/assets/img/home-1/feature/icon-2.png",
"year": "2025"
},
{
"name": "Innovation Award",
"icon": "/assets/img/home-1/feature/icon-3.png",
"year": "2025"
},
{
"name": "Global Education Partner",
"icon": "/assets/img/home-1/feature/icon-4.png",
"year": "2025"
}
]
},
"brands": {
"items": [
{
"logo": "/assets/img/home-1/brand/01.png"
},
{
"logo": "/assets/img/home-1/brand/02.png"
},
{
"logo": "/assets/img/home-1/brand/03.png"
},
{
"logo": "/assets/img/home-1/brand/04.png"
},
{
"logo": "/assets/img/home-1/brand/05.png"
}
]
}
},
"programs": {
"title": "- Activies",
"subtitle": "Explore Our Activities",
"button": {
"label": "Explore Now",
"href": "/booking"
},
"card": {
"pricePrefix": "from",
"priceSuffix": "USD",
"buttonLabel": "Camp Detail",
"buttonHref": "/camp-profiles"
"blogPreview": {
"heading": "Latest Insights & Updates",
"subheading": "Visa Tips & Guides",
"ctaButton": {
"label": "view all articles",
"href": "/blog"
},
"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"
"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": "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"
"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": "climbing",
"title": "Climbing",
"price": "515",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/b1.jpg",
"slug": "climbing"
},
{
"id": "dancing",
"title": "Dancing",
"price": "520",
"seasons": ["summer", "autumn"],
"age": "From 12 - 18 years old",
"location": "Malaysia",
"image": "/uploads/home/b4.jpg",
"slug": "dancing"
},
{
"id": "diving",
"title": "Diving",
"price": "1190",
"seasons": ["summer"],
"age": "From 12 - 18 years old",
"location": "Philippines",
"image": "/uploads/home/b2.jpg",
"slug": "diving"
},
{
"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"
},
{
"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"
},
{
"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"
},
{
"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"
"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"
}
]
},
"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": [
{
"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"
},
{
"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"
},
{
"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"
}
],
"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,253 @@
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 VisaConsultancyItemSchema = new Schema(
{
name: { type: String, default: "" },
icon: { type: String, default: "" },
year: { type: String, default: "" },
},
{ _id: false },
);
const VisaConsultancySchema = new Schema(
{
items: { type: [VisaConsultancyItemSchema], default: [] },
},
{ _id: false },
);
const BrandItemSchema = new Schema(
{
logo: { type: String, default: "" },
},
{ _id: false },
);
const BrandsSchema = new Schema(
{
items: { type: [BrandItemSchema], default: [] },
},
{ _id: false },
);
const PartnersSchema = new Schema(
{
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
brands: { type: BrandsSchema, default: () => ({}) },
},
{ _id: false },
);
const BlogPreviewItemSchema = new Schema(
{
title: { type: String, default: "" },
excerpt: { type: String, default: "" },
category: { type: String, default: "" },
date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20")
author: {
name: { type: String, default: "" },
avatar: { type: String, default: "" },
},
comments: { type: Number, default: 0 },
link: { type: String, default: "" },
thumbnail: { type: String, default: "" },
},
{ _id: false },
);
const BlogPreviewSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [BlogPreviewItemSchema], default: [] },
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],
},
{ _id: false },
);
/**
* Home page content model
*
* NOTE:
* - This schema is based on `hailearning.edu.vn/app/home.json`.
* - `strict: false` keeps backward compatibility with any existing CMS-only sections
* (e.g. about/missionVision/programs/newsletter/latestPosts...) that the admin UI might still send.
*/
const HomeSchema = new Schema(
{
hero: { type: HeroSchema, default: () => ({}) },
whyChooseUs: { type: WhyChooseUsSchema, default: () => ({}) },
visaSolutions: { type: VisaSolutionsSchema, default: () => ({}) },
visaCountries: { type: VisaCountriesSchema, default: () => ({}) },
testimonials: { type: TestimonialsSchema, default: () => ({}) },
videoGallery: { type: VideoGallerySchema, default: () => ({}) },
faq: { type: FaqSchema, default: () => ({}) },
achievements: { type: AchievementsSchema, default: () => ({}) },
partners: { type: PartnersSchema, default: () => ({}) },
blogPreview: { type: BlogPreviewSchema, default: () => ({}) },
},
{
timestamps: true,
strict: false,
},
);
module.exports = mongoose.model("Home", HomeSchema);
module.exports = mongoose.model('Home', homeSchema);

View File

@@ -37,11 +37,12 @@ router.get("/dashboard", ensureAuthenticated, dashboardController.getDashboard);
// Home
router.get("/home", ensureAuthenticated, homeController.index);
router.post("/home/update", ensureAuthenticated, homeController.update);
router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs);
// Middleware chuẩn hóa code
router.param("code", (req, res, next, code) => {
req.params.code = code.toUpperCase();
next();
req.params.code = code.toUpperCase();
next();
});
// About
@@ -50,51 +51,125 @@ router.post("/about/update", ensureAuthenticated, aboutController.update);
// AboutUs admin CRUD
router.get("/about-us", ensureAuthenticated, aboutUsController.index);
router.get("/about-us/create", ensureAuthenticated, aboutUsController.createForm);
router.get(
"/about-us/create",
ensureAuthenticated,
aboutUsController.createForm,
);
router.post("/about-us/create", ensureAuthenticated, aboutUsController.create);
router.get("/about-us/:id/edit", ensureAuthenticated, aboutUsController.editForm);
router.post("/about-us/:id/update", ensureAuthenticated, aboutUsController.update);
router.post("/about-us/:id/delete", ensureAuthenticated, aboutUsController.delete);
router.get("/about-us/:id/preview", ensureAuthenticated, aboutUsController.preview);
router.get(
"/about-us/:id/edit",
ensureAuthenticated,
aboutUsController.editForm,
);
router.post(
"/about-us/:id/update",
ensureAuthenticated,
aboutUsController.update,
);
router.post(
"/about-us/:id/delete",
ensureAuthenticated,
aboutUsController.delete,
);
router.get(
"/about-us/:id/preview",
ensureAuthenticated,
aboutUsController.preview,
);
// Booking admin CRUD removed
// Form Management
router.get("/form", ensureAuthenticated, formController.index);
router.post("/form/update", ensureAuthenticated, formController.updateDefaultForm);
router.post(
"/form/update",
ensureAuthenticated,
formController.updateDefaultForm,
);
// Upload routes
router.get("/upload", ensureAuthenticated, (req, res) => {
res.render("admin/upload/index", {
layout: "layouts/admin",
title: "Quản lý Upload Ảnh",
user: req.session.user,
});
res.render("admin/upload/index", {
layout: "layouts/admin",
title: "Quản lý Upload Ảnh",
user: req.session.user,
});
});
router.post("/upload/image", ensureAuthenticated, upload.single("image"), uploadController.uploadImage);
router.post("/upload/video", ensureAuthenticated, uploadVideo.single("video"), uploadController.uploadVideo);
router.post("/upload/update-path", ensureAuthenticated, uploadController.updateImagePath);
router.post("/upload/delete", ensureAuthenticated, uploadController.deleteImage);
router.post(
"/upload/image",
ensureAuthenticated,
upload.single("image"),
uploadController.uploadImage,
);
router.post(
"/upload/video",
ensureAuthenticated,
uploadVideo.single("video"),
uploadController.uploadVideo,
);
router.post(
"/upload/update-path",
ensureAuthenticated,
uploadController.updateImagePath,
);
router.post(
"/upload/delete",
ensureAuthenticated,
uploadController.deleteImage,
);
// Header routes
router.get("/header", ensureAuthenticated, headerController.index);
router.post("/header/update", ensureAuthenticated, headerController.update);
router.get("/header/data", ensureAuthenticated, headerController.api); // Normalized from getHeaderData
router.patch("/header/:id/status", ensureAuthenticated, headerController.updateStatus);
router.patch(
"/header/:id/status",
ensureAuthenticated,
headerController.updateStatus,
);
router.delete("/header/:id", ensureAuthenticated, headerController.destroy);
// Header Menu INTEGRATED routes
router.post("/header/menu/create", ensureAuthenticated, headerMenuController.store);
router.post("/header/menu/update/:id", ensureAuthenticated, headerMenuController.update);
router.post("/header/menu/delete", ensureAuthenticated, headerMenuController.destroy);
router.post("/header/menu/reorder", ensureAuthenticated, headerMenuController.reorder);
router.post(
"/header/menu/create",
ensureAuthenticated,
headerMenuController.store,
);
router.post(
"/header/menu/update/:id",
ensureAuthenticated,
headerMenuController.update,
);
router.post(
"/header/menu/delete",
ensureAuthenticated,
headerMenuController.destroy,
);
router.post(
"/header/menu/reorder",
ensureAuthenticated,
headerMenuController.reorder,
);
// Social Links routes
router.get("/social-links", ensureAuthenticated, socialLinkController.index);
router.post("/social-links", ensureAuthenticated, socialLinkController.store);
router.put("/social-links/:platform", ensureAuthenticated, socialLinkController.update);
router.delete("/social-links/:platform", ensureAuthenticated, socialLinkController.destroy);
router.post("/social-links/reorder", ensureAuthenticated, socialLinkController.reorder);
router.put(
"/social-links/:platform",
ensureAuthenticated,
socialLinkController.update,
);
router.delete(
"/social-links/:platform",
ensureAuthenticated,
socialLinkController.destroy,
);
router.post(
"/social-links/reorder",
ensureAuthenticated,
socialLinkController.reorder,
);
// Footer routes
router.get("/footer", ensureAuthenticated, footerController.index);
@@ -104,60 +179,160 @@ router.get("/footer/data", ensureAuthenticated, footerController.getFooterData);
// Contact routes
router.get("/contact", ensureAuthenticated, contactController.index);
router.post("/contact/update", ensureAuthenticated, contactController.update);
router.get("/contact/data", ensureAuthenticated, contactController.getContactData);
router.get(
"/contact/data",
ensureAuthenticated,
contactController.getContactData,
);
// Contact submissions management
router.get("/contact/submissions", ensureAuthenticated, contactController.getSubmissions);
router.put("/contact/submissions/:id", ensureAuthenticated, contactController.updateSubmissionStatus);
router.get(
"/contact/submissions",
ensureAuthenticated,
contactController.getSubmissions,
);
router.put(
"/contact/submissions/:id",
ensureAuthenticated,
contactController.updateSubmissionStatus,
);
// Appointment management
const appointmentController = require("../controllers/appointmentController");
router.get("/appointments", ensureAuthenticated, appointmentController.getAppointments);
router.get("/appointments/:id", ensureAuthenticated, appointmentController.getAppointmentById);
router.put("/appointments/:id", ensureAuthenticated, appointmentController.updateAppointmentStatus);
router.delete("/appointments/:id", ensureAuthenticated, appointmentController.deleteAppointment);
router.get(
"/appointments",
ensureAuthenticated,
appointmentController.getAppointments,
);
router.get(
"/appointments/:id",
ensureAuthenticated,
appointmentController.getAppointmentById,
);
router.put(
"/appointments/:id",
ensureAuthenticated,
appointmentController.updateAppointmentStatus,
);
router.delete(
"/appointments/:id",
ensureAuthenticated,
appointmentController.deleteAppointment,
);
// Appointment CMS page management
router.get("/appointment", ensureAuthenticated, appointmentController.index);
router.post("/appointment/update", ensureAuthenticated, appointmentController.update);
router.get("/appointment/data", ensureAuthenticated, appointmentController.getAppointmentData);
router.post(
"/appointment/update",
ensureAuthenticated,
appointmentController.update,
);
router.get(
"/appointment/data",
ensureAuthenticated,
appointmentController.getAppointmentData,
);
// Pricing CMS page management
const pricingController = require("../controllers/pricingController");
router.get("/pricing", ensureAuthenticated, pricingController.index);
router.post("/pricing/update", ensureAuthenticated, pricingController.update);
router.get("/pricing/data", ensureAuthenticated, pricingController.getPricingData);
router.get(
"/pricing/data",
ensureAuthenticated,
pricingController.getPricingData,
);
// Activity CRUD routes
router.get("/activity", ensureAuthenticated, activityController.index);
router.get("/activity/create", ensureAuthenticated, activityController.createForm);
router.get(
"/activity/create",
ensureAuthenticated,
activityController.createForm,
);
router.post("/activity/create", ensureAuthenticated, activityController.create);
// Update filters (place before any parameterized /activity/:id routes to avoid route collision)
router.post("/activity/filters/update", ensureAuthenticated, activityController.updateFilters);
router.post(
"/activity/filters/update",
ensureAuthenticated,
activityController.updateFilters,
);
// Update hero (global hero section for activities)
router.post("/activity/hero/update", ensureAuthenticated, activityController.updateHero);
router.get("/activity/:id/edit", ensureAuthenticated, activityController.editForm);
router.post("/activity/:id/update", ensureAuthenticated, activityController.update);
router.post("/activity/:id/delete", ensureAuthenticated, activityController.delete);
router.post("/activity/:id/toggle-status", ensureAuthenticated, activityController.toggleStatus);
router.post(
"/activity/hero/update",
ensureAuthenticated,
activityController.updateHero,
);
router.get(
"/activity/:id/edit",
ensureAuthenticated,
activityController.editForm,
);
router.post(
"/activity/:id/update",
ensureAuthenticated,
activityController.update,
);
router.post(
"/activity/:id/delete",
ensureAuthenticated,
activityController.delete,
);
router.post(
"/activity/:id/toggle-status",
ensureAuthenticated,
activityController.toggleStatus,
);
// Update display order
router.post("/activity/update-order", ensureAuthenticated, activityController.updateOrder);
router.post(
"/activity/update-order",
ensureAuthenticated,
activityController.updateOrder,
);
// Booking submissions routes
router.get("/activity/:id/bookings/count", ensureAuthenticated, activityController.getBookingCount);
router.get("/activity/:id/bookings", ensureAuthenticated, activityController.getBookingSubmissions);
router.get("/activity/:id/bookings/export", ensureAuthenticated, activityController.exportBookingData);
router.get(
"/activity/:id/bookings/count",
ensureAuthenticated,
activityController.getBookingCount,
);
router.get(
"/activity/:id/bookings",
ensureAuthenticated,
activityController.getBookingSubmissions,
);
router.get(
"/activity/:id/bookings/export",
ensureAuthenticated,
activityController.exportBookingData,
);
// Export all bookings (across all activities)
router.get("/bookings/export-all", ensureAuthenticated, activityController.exportAllBookingsData);
router.get(
"/bookings/export-all",
ensureAuthenticated,
activityController.exportAllBookingsData,
);
// Update booking submission
router.put("/bookings/:bookingId", ensureAuthenticated, bookingSubmissionController.updateBookingSubmission);
router.put(
"/bookings/:bookingId",
ensureAuthenticated,
bookingSubmissionController.updateBookingSubmission,
);
// Delete booking submission
router.delete("/bookings/:bookingId", ensureAuthenticated, bookingSubmissionController.deleteBookingSubmission);
router.delete(
"/bookings/:bookingId",
ensureAuthenticated,
bookingSubmissionController.deleteBookingSubmission,
);
// Update filters
// Preview activity
router.get("/activity/:id/preview", ensureAuthenticated, activityController.preview);
router.get(
"/activity/:id/preview",
ensureAuthenticated,
activityController.preview,
);
// FAQ routes - Thêm vào đây
router.get("/faq", ensureAuthenticated, faqController.index);
@@ -257,6 +432,69 @@ router.post(
serviceController.updateDetails,
);
// Test Image Paths route
router.get("/test-images", ensureAuthenticated, (req, res) => {
const fs = require("fs");
const path = require("path");
const campLocationData = require("../data/camp-location.json");
// Collect all image paths
const imagePaths = [];
// Camps images
if (campLocationData.camps) {
campLocationData.camps.forEach((camp) => {
if (camp.image) {
imagePaths.push({
type: "Camp",
name: camp.title,
path: camp.image,
exists: fs.existsSync(path.join(__dirname, "../public", camp.image)),
});
}
});
}
// Locations images
if (campLocationData.locations) {
campLocationData.locations.forEach((location) => {
if (location.imageSrc) {
imagePaths.push({
type: "Location",
name: location.title,
path: location.imageSrc,
exists: fs.existsSync(
path.join(__dirname, "../public", location.imageSrc),
),
});
}
// Program images
if (location.programOptions) {
location.programOptions.forEach((program) => {
if (program.imageSrc) {
imagePaths.push({
type: "Program",
name: program.title,
path: program.imageSrc,
exists: fs.existsSync(
path.join(__dirname, "../public", program.imageSrc),
),
});
}
});
}
});
}
res.render("admin/test-images", {
layout: "layouts/admin",
title: "Test Image Paths",
images: imagePaths,
user: req.session.user,
});
});
// Display visa management page
router.get("/visa", ensureAuthenticated, visaController.index);
@@ -264,7 +502,7 @@ router.get("/visa", ensureAuthenticated, visaController.index);
router.get("/visa/edit/:id", ensureAuthenticated, visaController.getCountry);
// Update hero title
router.post("/visa/update", ensureAuthenticated, visaController.update);
router.post("/visa/update", ensureAuthenticated, visaController.updateCountry);
// Add new country
router.post("/visa/add", ensureAuthenticated, visaController.addCountry);
@@ -278,7 +516,7 @@ router.put(
// Delete country
router.delete(
"/delete/:slug",
"/visa/delete/:id",
ensureAuthenticated,
visaController.deleteCountry,
);
@@ -292,9 +530,21 @@ router.post("/blog/:id/edit", ensureAuthenticated, blogController.update);
router.post("/blog/:id/delete", ensureAuthenticated, blogController.destroy);
// Comment management routes
router.post("/blog/:blogId/comments/:commentId/approve", ensureAuthenticated, blogController.approveComment);
router.post("/blog/:blogId/comments/:commentId/reject", ensureAuthenticated, blogController.rejectComment);
router.post("/blog/:blogId/comments/:commentId/delete", ensureAuthenticated, blogController.deleteComment);
router.post(
"/blog/:blogId/comments/:commentId/approve",
ensureAuthenticated,
blogController.approveComment,
);
router.post(
"/blog/:blogId/comments/:commentId/reject",
ensureAuthenticated,
blogController.rejectComment,
);
router.post(
"/blog/:blogId/comments/:commentId/delete",
ensureAuthenticated,
blogController.deleteComment,
);
// Blog Categories Management
router.get(

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
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:

View File

@@ -273,6 +273,25 @@
</a>
</div>
</div>
<!-- Visa -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-passport fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Visa</h5>
<p class="text-muted mb-0 small">Manage visa countries</p>
</div>
</div>
<a href="/admin/visa" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
<!-- Achievements Tab -->
<div class="tab-pane fade" id="achievements" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2"></i>General Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="achievementsHeading"
value="<%= data.achievements?.heading || '' %>"
placeholder="e.g., Our Achievements in Numbers"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="achievementsSubheading"
value="<%= data.achievements?.subheading || '' %>"
placeholder="e.g., Did You Know"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Achievement Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-list-ul me-2"></i>Achievement Items (Fixed 4 Items)
</h6>
</div>
<div class="card-body" id="achievementItemsContainer">
<% for(let i=0; i<4; i++) {
const item = (data.achievements?.items && data.achievements.items[i]) || {};
%>
<div class="card mb-3 bg-light border achievement-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="card-title fw-bold mb-0">Achievement #<%= i + 1 %></h6>
</div>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label fw-medium">Value (Number)</label>
<input
type="text"
class="form-control achievement-value"
value="<%= item.value || '' %>"
placeholder="e.g., 95"
/>
</div>
<div class="col-md-3">
<label class="form-label fw-medium">Suffix</label>
<input
type="text"
class="form-control achievement-suffix"
value="<%= item.suffix || '' %>"
placeholder="e.g., %"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control achievement-label"
value="<%= item.label || '' %>"
placeholder="e.g., Visa Success Rate"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control achievement-description"
rows="2"
placeholder="Short description of this achievement"
><%= item.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<script>
// Đăng ký scraper cho phần achievements
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.achievements = function() {
const items = [];
document.querySelectorAll('.achievement-item').forEach(el => {
items.push({
value: el.querySelector('.achievement-value').value,
suffix: el.querySelector('.achievement-suffix').value,
label: el.querySelector('.achievement-label').value,
description: el.querySelector('.achievement-description').value
});
});
return {
heading: document.getElementById('achievementsHeading').value,
subheading: document.getElementById('achievementsSubheading').value,
items: items
};
};
</script>

View File

@@ -0,0 +1,175 @@
<!-- Blog Preview Tab -->
<div class="tab-pane fade" id="blogpreview" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information & Blog Selection
</h6>
<span class="badge bg-info text-dark">CMS will automatically fetch the 3 latest posts if no specific blog is
selected.</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="blogPreviewHeading"
value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="blogPreviewSubheading"
value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides" />
</div>
<div class="col-md-12 mt-4">
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct
from Blog Module)</label>
<p class="text-muted small mb-3">Select blog posts to prioritize on the home page. If none are selected,
the system will use the 3 latest posts.</p>
<div class="row g-3 blog-selector-container"
style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
<% if (allBlogs && allBlogs.length> 0) { %>
<% allBlogs.forEach(blog=> {
const isSelected = data.blogPreview?.selectedBlogIds && data.blogPreview.selectedBlogIds.some(id =>
id.toString() === blog._id.toString());
%>
<div class="col-md-4">
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>"
onclick="toggleBlogSelection(this, '<%= blog._id %>')"
style="cursor: pointer; transition: all 0.2s;">
<div class="position-absolute top-0 end-0 m-2">
<div class="form-check">
<input class="form-check-input blog-checkbox" type="checkbox" value="<%= blog._id %>"
<%=isSelected ? 'checked' : '' %> onclick="event.stopPropagation();
handleCheckboxChange(this)">
</div>
</div>
<img
src="<%= blog.featuredImage ? getFullImageUrl(blog.featuredImage, backendUrl) : '/assets/img/placeholder.jpg' %>"
class="card-img-top" style="height: 210px; object-fit: cover;">
<div class="card-body p-2">
<h6 class="card-title small fw-bold mb-1" title="<%= blog.title %>"
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.6em; line-height: 1.3em;">
<%= blog.title %>
</h6>
<p class="card-text tiny text-muted mb-0">
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
</p>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="col-12 text-center py-4">
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="blogPreviewCtaLabel"
value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="blogPreviewCtaHref"
value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function toggleBlogSelection(card, blogId) {
const checkbox = card.querySelector('.blog-checkbox');
const isChecking = !checkbox.checked;
if (isChecking) {
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
if (checkedCount >= 3) {
alert('You can only select up to 3 blogs.');
return;
}
}
checkbox.checked = isChecking;
handleCheckboxUpdate(card, checkbox.checked);
}
function handleCheckboxChange(checkbox) {
if (checkbox.checked) {
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
if (checkedCount > 3) {
checkbox.checked = false;
alert('You can only select up to 3 blogs.');
return;
}
}
const card = checkbox.closest('.blog-select-card');
handleCheckboxUpdate(card, checkbox.checked);
}
function handleCheckboxUpdate(card, isChecked) {
if (isChecked) {
card.classList.add('border-primary', 'bg-light');
} else {
card.classList.remove('border-primary', 'bg-light');
}
}
// Đăng ký scraper cho Blog Preview
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.blogPreview = () => {
const selectedIds = [];
document.querySelectorAll('.blog-checkbox:checked').forEach(cb => {
selectedIds.push(cb.value);
});
// Chúng ta vẫn giữ cấu trúc cũ cho items nếu muốn preview offline,
// nhưng server sẽ ưu tiên dùng selectedBlogIds để populate dữ liệu mới nhất.
return {
heading: document.getElementById('blogPreviewHeading').value,
subheading: document.getElementById('blogPreviewSubheading').value,
ctaButton: {
label: document.getElementById('blogPreviewCtaLabel').value,
href: document.getElementById('blogPreviewCtaHref').value
},
selectedBlogIds: selectedIds,
items: [] // Server side will handle full items content
};
};
</script>
<style>
.tiny {
font-size: 0.75rem;
}
.blog-select-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -0,0 +1,124 @@
<!-- FAQ Tab -->
<div class="tab-pane fade" id="faq" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="faqHeading"
value="<%= data.faq?.heading || '' %>"
placeholder="e.g., Got Questions? We've Got Answers"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="faqSubheading"
value="<%= data.faq?.subheading || '' %>"
placeholder="e.g., Visa FAQs"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control"
id="faqDescription"
rows="3"
placeholder="Enter description"
><%= data.faq?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- FAQ Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-question-circle me-2"></i>FAQ Items
</h6>
</div>
<div class="card-body">
<% (data.faq?.items || []).forEach(function(item, index) { %>
<div class="card mb-3 bg-light border">
<div class="card-body">
<h6 class="card-title fw-bold mb-3">FAQ <%= index + 1 %></h6>
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Question</label>
<input
type="text"
class="form-control"
id="faqQuestion_<%= index %>"
value="<%= item.question || '' %>"
placeholder="Enter question"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Answer</label>
<textarea
class="form-control"
id="faqAnswer_<%= index %>"
rows="3"
placeholder="Enter answer"
><%= item.answer || '' %></textarea>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control"
id="faqCtaLabel"
value="<%= data.faq?.ctaButton?.label || '' %>"
placeholder="e.g., contact us"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="faqCtaHref"
value="<%= data.faq?.ctaButton?.href || '' %>"
placeholder="/contact"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,157 @@
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input
type="text"
class="form-control"
id="heroTitle"
value="<%= data.hero?.title || '' %>"
placeholder="e.g., From Application to Visa"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input
type="text"
class="form-control"
id="heroSubtitle"
value="<%= data.hero?.subtitle || '' %>"
placeholder="e.g., Global Education Simplified"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control"
id="heroDescription"
rows="3"
placeholder="Enter hero description"
><%= data.hero?.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="home"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.hero?.backgroundImage) { %>
<div class="mt-2">
<img
src="<%= data.hero.backgroundImage %>"
class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background preview"
/>
</div>
<% } %>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video URL</label>
<input
type="text"
class="form-control"
id="heroVideoUrl"
value="<%= data.hero?.videoUrl || '' %>"
placeholder="https://www.youtube.com/watch?v=..."
/>
</div>
</div>
</div>
</div>
</div>
<!-- Primary Button -->
<div class="col-md-6">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control"
id="heroPrimaryButtonLabel"
value="<%= data.hero?.primaryButton?.label || '' %>"
placeholder="e.g., Apply now"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="heroPrimaryButtonHref"
value="<%= data.hero?.primaryButton?.href || '' %>"
placeholder="/contact"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Secondary Button -->
<div class="col-md-6">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>Secondary Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control"
id="heroSecondaryButtonLabel"
value="<%= data.hero?.secondaryButton?.label || '' %>"
placeholder="e.g., Book Free Consultation"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="heroSecondaryButtonHref"
value="<%= data.hero?.secondaryButton?.href || '' %>"
placeholder="/contact"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,150 @@
<!-- Partners Tab -->
<div class="tab-pane fade" id="partners" role="tabpanel">
<div class="row g-4">
<!-- Visa Consultancy Awards -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-award me-2"></i>Awards & Certifications (Fixed 4 Items)
</h6>
</div>
<div class="card-body">
<div id="visaConsultancyContainer">
<% for(let i=0; i<4; i++) {
const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {};
%>
<div class="card mb-3 bg-light border visa-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="card-title fw-bold mb-0">Award #<%= i + 1 %></h6>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label fw-medium">Award Name</label>
<input type="text" class="form-control visa-name" value="<%= item.name || '' %>" placeholder="Award Name" />
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Year</label>
<input type="text" class="form-control visa-year" value="<%= item.year || '' %>" placeholder="e.g., 2025" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Icon / Logo</label>
<div class="input-group">
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>" value="<%= item.icon || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="visaIcon_<%= i %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="mt-2 preview-container">
<img src="<%= item.icon || '' %>" class="img-thumbnail <%= item.icon ? '' : 'd-none' %>" style="height: 60px; object-fit: contain;">
</div>
</div>
</div>
</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Brand Logos -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-images me-2"></i>Brand Partner Logos (Slider)
</h6>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addBrandPartnerItem()">
<i class="fas fa-plus me-1"></i>Add Logo
</button>
</div>
<div class="card-body">
<div id="brandPartnersContainer" class="row g-3">
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
<div class="col-md-4 brand-partner-item">
<div class="card border bg-light h-100">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small fw-bold">Brand Logo</span>
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>" value="<%= item.logo || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="brandLogo_<%= index %>" data-image-type="home">
<i class="fas fa-upload"></i>
</button>
</div>
<div class="mt-2 text-center preview-container">
<img src="<%= item.logo || '' %>" class="img-thumbnail <%= item.logo ? '' : 'd-none' %>" style="height: 50px; object-fit: contain;">
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Thu thập dữ liệu partners
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.partners = function() {
const visaItems = [];
document.querySelectorAll('.visa-item').forEach(el => {
visaItems.push({
name: el.querySelector('.visa-name').value,
year: el.querySelector('.visa-year').value,
icon: el.querySelector('.visa-icon').value
});
});
const brandItems = [];
document.querySelectorAll('.brand-partner-item').forEach(el => {
const logo = el.querySelector('.brand-logo-input').value;
if (logo) brandItems.push({ logo: logo });
});
return {
visaConsultancy: { items: visaItems },
brands: { items: brandItems }
};
};
function addBrandPartnerItem() {
const container = document.getElementById('brandPartnersContainer');
const id = 'brandLogo_' + Date.now();
const div = document.createElement('div');
div.className = 'col-md-4 brand-partner-item';
div.innerHTML = `
<div class="card border bg-light h-100">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small fw-bold">New Brand Logo</span>
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="${id}">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="${id}" data-image-type="home">
<i class="fas fa-upload"></i>
</button>
</div>
<div class="mt-2 text-center preview-container">
<img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;">
</div>
</div>
</div>
`;
container.appendChild(div);
}
</script>

View File

@@ -0,0 +1,159 @@
<!-- Testimonials Tab -->
<div class="tab-pane fade" id="testimonials" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>"
placeholder="e.g., Student Reviews & Testimonials"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="testimonialsSubheading"
value="<%= data.testimonials?.subheading || '' %>"
placeholder="e.g., What Our Students Say"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video URL</label>
<input
type="text"
class="form-control"
id="testimonialsVideoUrl"
value="<%= data.testimonials?.videoUrl || '' %>"
placeholder="https://www.youtube.com/watch?v=..."
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video Thumbnail</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="testimonialsVideoThumbnail"
value="<%= data.testimonials?.videoThumbnail || '' %>"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsVideoThumbnail"
data-image-type="home"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Testimonial Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-comments me-2"></i>Testimonials
</h6>
</div>
<div class="card-body">
<% (data.testimonials?.items || []).forEach(function(item, index) { %>
<div class="card mb-3 bg-light border">
<div class="card-body">
<h6 class="card-title fw-bold mb-3">Testimonial <%= index + 1 %></h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Name</label>
<input
type="text"
class="form-control"
id="testimonialsName_<%= index %>"
value="<%= item.name || '' %>"
placeholder="e.g., Sohel Tanvir"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Role</label>
<input
type="text"
class="form-control"
id="testimonialsRole_<%= index %>"
value="<%= item.role || '' %>"
placeholder="e.g., Student"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Country</label>
<input
type="text"
class="form-control"
id="testimonialsCountry_<%= index %>"
value="<%= item.country || '' %>"
placeholder="e.g., Canada"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Rating</label>
<input
type="number"
class="form-control"
id="testimonialsRating_<%= index %>"
value="<%= item.rating || 5 %>"
min="1"
max="5"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Comment</label>
<textarea
class="form-control"
id="testimonialsComment_<%= index %>"
rows="3"
placeholder="Enter testimonial comment"
><%= item.comment || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Avatar</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="testimonialsAvatar_<%= index %>"
value="<%= item.avatar || '' %>"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsAvatar_<%= index %>"
data-image-type="home"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,67 @@
<!-- Video Gallery Tab -->
<div class="tab-pane fade" id="videogallery" role="tabpanel">
<div class="row g-4">
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-video me-2"></i>Video Gallery
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="videoGalleryHeading"
value="<%= data.videoGallery?.heading || '' %>"
placeholder="e.g., VIDEO PLAY GALLERY"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Video URL</label>
<input
type="text"
class="form-control"
id="videoGalleryVideoUrl"
value="<%= data.videoGallery?.videoUrl || '' %>"
placeholder="https://example.com/video.mp4"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Thumbnail Image</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="videoGalleryThumbnail"
value="<%= data.videoGallery?.thumbnail || '' %>"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="videoGalleryThumbnail"
data-image-type="home"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.videoGallery?.thumbnail) { %>
<div class="mt-2">
<img
src="<%= data.videoGallery.thumbnail %>"
class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Thumbnail preview"
/>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,163 @@
<!-- Visa Countries Tab -->
<div class="tab-pane fade" id="visacountries" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="visaCountriesHeading"
value="<%= data.visaCountries?.heading || '' %>"
placeholder="e.g., Visa & VISAWAY Services To UK"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="visaCountriesSubheading"
value="<%= data.visaCountries?.subheading || '' %>"
placeholder="e.g., UK. United Kingdom"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control"
id="visaCountriesDescription"
rows="3"
placeholder="Enter description"
><%= data.visaCountries?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Countries -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-globe me-2"></i>Countries
</h6>
</div>
<div class="card-body">
<% (data.visaCountries?.countries || []).forEach(function(country, index) { %>
<div class="card mb-3 bg-light border">
<div class="card-body">
<h6 class="card-title fw-bold mb-3">Country <%= index + 1 %></h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Country Name</label>
<input
type="text"
class="form-control"
id="visaCountriesName_<%= index %>"
value="<%= country.name || '' %>"
placeholder="e.g., United Kingdom"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Country Code</label>
<input
type="text"
class="form-control"
id="visaCountriesCode_<%= index %>"
value="<%= country.code || '' %>"
placeholder="e.g., UK"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Flag Image</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="visaCountriesFlag_<%= index %>"
value="<%= country.flag || '' %>"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="visaCountriesFlag_<%= index %>"
data-image-type="home"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="visaCountriesLink_<%= index %>"
value="<%= country.link || '' %>"
placeholder="/country-details/uk"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Visa Types (comma-separated)</label>
<textarea
class="form-control"
id="visaCountriesVisaTypes_<%= index %>"
rows="2"
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"
><%= (country.visaTypes || []).join(', ') %></textarea>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control"
id="visaCountriesCtaLabel"
value="<%= data.visaCountries?.ctaButton?.label || '' %>"
placeholder="e.g., Get Started"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="visaCountriesCtaHref"
value="<%= data.visaCountries?.ctaButton?.href || '' %>"
placeholder="/contact"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,100 @@
<!-- Visa Solutions Tab -->
<div class="tab-pane fade" id="visasolutions" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="visaSolutionsHeading"
value="<%= data.visaSolutions?.heading || '' %>"
placeholder="e.g., Comprehensive Visa Solutions"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="visaSolutionsSubheading"
value="<%= data.visaSolutions?.subheading || '' %>"
placeholder="e.g., Our Expert Services"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Services Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-list-ul me-2"></i>Visa Solutions Items
</h6>
</div>
<div class="card-body">
<% (data.visaSolutions?.items || []).forEach(function(item, index) { %>
<div class="card mb-3 bg-light border">
<div class="card-body">
<h6 class="card-title fw-bold mb-3">Service <%= index + 1 %></h6>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label fw-medium">Number</label>
<input
type="text"
class="form-control"
id="visaSolutionsNumber_<%= index %>"
value="<%= item.number || '' %>"
placeholder="e.g., 01"
/>
</div>
<div class="col-md-9">
<label class="form-label fw-medium">Title</label>
<input
type="text"
class="form-control"
id="visaSolutionsTitle_<%= index %>"
value="<%= item.title || '' %>"
placeholder="e.g., Student Visa Guidance"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control"
id="visaSolutionsDescription_<%= index %>"
rows="2"
placeholder="Enter description"
><%= item.description || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="visaSolutionsLink_<%= index %>"
value="<%= item.link || '' %>"
placeholder="/service-details"
/>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,169 @@
<!-- Why Choose Us Tab -->
<div class="tab-pane fade" id="whychooseus" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="whyChooseUsHeading"
value="<%= data.whyChooseUs?.heading || '' %>"
placeholder="e.g., Turning Study Abroad Dreams Into Reality"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="whyChooseUsSubheading"
value="<%= data.whyChooseUs?.subheading || '' %>"
placeholder="e.g., About Our Consultancy"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control"
id="whyChooseUsDescription"
rows="3"
placeholder="Enter description"
><%= data.whyChooseUs?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-list-ul me-2"></i>Why Choose Us Items
</h6>
</div>
<div class="card-body">
<% (data.whyChooseUs?.items || []).forEach(function(item, index) { %>
<div class="card mb-3 bg-light border">
<div class="card-body">
<h6 class="card-title fw-bold mb-3">Item <%= index + 1 %></h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Icon URL</label>
<div class="input-group mb-2">
<input
type="text"
class="form-control"
id="whyChooseUsIcon_<%= index %>"
value="<%= item.icon || '' %>"
/>
<button
type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsIcon_<%= index %>"
data-image-type="home"
>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input
type="text"
class="form-control"
id="whyChooseUsTitle_<%= index %>"
value="<%= item.title || '' %>"
placeholder="e.g., Global Reach"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<input
type="text"
class="form-control"
id="whyChooseUsItemDescription_<%= index %>"
value="<%= item.description || '' %>"
placeholder="e.g., Expanding Opportunities Worldwide"
/>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
<!-- Features -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-star me-2"></i>Features
</h6>
</div>
<div class="card-body">
<% (data.whyChooseUs?.features || []).forEach(function(feature, index) { %>
<div class="mb-3">
<label class="form-label fw-medium">Feature <%= index + 1 %></label>
<input
type="text"
class="form-control"
id="whyChooseUsFeature_<%= index %>"
value="<%= feature %>"
placeholder="Enter feature"
/>
</div>
<% }); %>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control"
id="whyChooseUsCtaLabel"
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>"
placeholder="e.g., Get Started"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input
type="text"
class="form-control"
id="whyChooseUsCtaHref"
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>"
placeholder="/about"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -178,8 +178,8 @@
</div>
<div style="text-align: center; margin-bottom: 30px;">
<h4 style="color: #b8b76a; font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
<p style="color: #666; font-size: 14px;">Welcome to Content Management System</p>
<h4 style="color: var(--primary-color); font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
<p style="color: var(--text-color); font-size: 14px;">Welcome to Content Management System</p>
</div>
<form action="/auth/login" method="POST" class="login-form">

View File

@@ -84,6 +84,9 @@
<li class="nav-item">
<a class="nav-link" href="/admin/blog">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/visa">Visa</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/blog-category">Blog Category</a>
</li>

View File

@@ -772,6 +772,10 @@
<a class="nav-link <%= currentPath === '/admin/blog' ? 'active' : '' %>"
href="/admin/blog">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/visa' ? 'active' : '' %>"
href="/admin/visa">Visa</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
href="/admin/contact">Contact Us</a>