forked from UKSOURCE/cms.hailearning.edu.vn
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:
@@ -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();
|
||||
// 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];
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||
|
||||
// Lấy tất cả blog để chọn trong CMS
|
||||
const allBlogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).lean();
|
||||
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||
|
||||
res.render('admin/home/index', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Home Management',
|
||||
return res.render("admin/home/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Home Management",
|
||||
data,
|
||||
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();
|
||||
const sections = [
|
||||
"hero", "whyChooseUs", "visaSolutions", "visaCountries",
|
||||
"testimonials", "videoGallery", "faq", "achievements",
|
||||
"partners", "blogPreview"
|
||||
];
|
||||
|
||||
// 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 || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
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'));
|
||||
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'));
|
||||
}
|
||||
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 = [];
|
||||
|
||||
// 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();
|
||||
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(homeData, baseUrl);
|
||||
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error('Home API error:', err);
|
||||
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
|
||||
// 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());
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(heroData, baseUrl);
|
||||
// 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();
|
||||
}
|
||||
|
||||
res.json(processedData);
|
||||
// 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('Hero API error:', err);
|
||||
res.status(500).json({ error: 'Error loading hero data' });
|
||||
console.error("Home API error:", err);
|
||||
return res.status(500).json({ error: "Error loading home data" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
760
data/home.json
760
data/home.json
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
422
models/home.js
422
models/home.js
@@ -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);
|
||||
360
routes/admin.js
360
routes/admin.js
@@ -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(
|
||||
|
||||
47
scripts/2026_02_05_190000_home.js
Normal file
47
scripts/2026_02_05_190000_home.js
Normal file
@@ -0,0 +1,47 @@
|
||||
require("dotenv").config();
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
const connectDB = require("../config/database");
|
||||
|
||||
/**
|
||||
* Migration: import_home_content
|
||||
* Created: 19:00:00 2026-02-05
|
||||
* Description:
|
||||
* Import nội dung trang Home từ file JSON (Next.js) vào MongoDB (model Home).
|
||||
* Nguồn dữ liệu: hailearning.edu.vn/app/home.json
|
||||
*/
|
||||
async function migrate() {
|
||||
try {
|
||||
// 1) Connect DB
|
||||
await connectDB();
|
||||
console.log("🚀 Starting migration: import_home_content...");
|
||||
|
||||
// 2) Load model
|
||||
const Home = require("../models/home");
|
||||
console.log("✅ Home model registered successfully");
|
||||
|
||||
// 3) Load JSON data
|
||||
const dataPath = path.join(__dirname, "..", "..", "hailearning.edu.vn", "app", "home.json");
|
||||
const raw = await fs.readFile(dataPath, "utf8");
|
||||
const homeData = JSON.parse(raw);
|
||||
console.log("📖 Home data loaded from:", dataPath);
|
||||
|
||||
// 4) Clear existing
|
||||
console.log("🧹 Clearing existing Home data...");
|
||||
await Home.deleteMany({});
|
||||
console.log("✅ Existing Home documents cleared");
|
||||
|
||||
// 5) Insert new document
|
||||
const created = await Home.create(homeData);
|
||||
console.log("✅ Home document created with _id:", created._id.toString());
|
||||
|
||||
console.log("🎉 Migration import_home_content completed successfully.");
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("❌ Migration failed:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
|
||||
@@ -21,17 +21,17 @@ const migrateHeader = async () => {
|
||||
// Transform and insert data
|
||||
const headerDocument = {
|
||||
top: {
|
||||
phone: headerData.top.phone,
|
||||
email: headerData.top.email,
|
||||
location: headerData.top.location,
|
||||
socialLinks: headerData.top.socialLinks.map((link, idx) => ({
|
||||
phone: headerData.top?.phone || "",
|
||||
email: headerData.top?.email || "",
|
||||
location: headerData.top?.location || "",
|
||||
socialLinks: (headerData.top?.socialLinks || []).map((link, idx) => ({
|
||||
...link,
|
||||
order: idx,
|
||||
})),
|
||||
languages: headerData.top.languages,
|
||||
languages: headerData.top?.languages || [],
|
||||
},
|
||||
offcanvas: headerData.offcanvas,
|
||||
menu: headerData.menu.map((item, idx) => ({
|
||||
offcanvas: headerData.offcanvas || {},
|
||||
menu: (headerData.menu || []).map((item, idx) => ({
|
||||
...item,
|
||||
order: idx,
|
||||
children:
|
||||
|
||||
@@ -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
122
views/admin/home/sections/achievements.ejs
Normal file
122
views/admin/home/sections/achievements.ejs
Normal 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>
|
||||
175
views/admin/home/sections/blogPreview.ejs
Normal file
175
views/admin/home/sections/blogPreview.ejs
Normal 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>
|
||||
124
views/admin/home/sections/faq.ejs
Normal file
124
views/admin/home/sections/faq.ejs
Normal 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>
|
||||
157
views/admin/home/sections/hero.ejs
Normal file
157
views/admin/home/sections/hero.ejs
Normal 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>
|
||||
150
views/admin/home/sections/partners.ejs
Normal file
150
views/admin/home/sections/partners.ejs
Normal 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>
|
||||
159
views/admin/home/sections/testimonials.ejs
Normal file
159
views/admin/home/sections/testimonials.ejs
Normal 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>
|
||||
67
views/admin/home/sections/videoGallery.ejs
Normal file
67
views/admin/home/sections/videoGallery.ejs
Normal 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>
|
||||
163
views/admin/home/sections/visaCountries.ejs
Normal file
163
views/admin/home/sections/visaCountries.ejs
Normal 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>
|
||||
100
views/admin/home/sections/visaSolutions.ejs
Normal file
100
views/admin/home/sections/visaSolutions.ejs
Normal 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>
|
||||
169
views/admin/home/sections/whyChooseUs.ejs
Normal file
169
views/admin/home/sections/whyChooseUs.ejs
Normal 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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user