diff --git a/controllers/homeController.js b/controllers/homeController.js index 92b54fb..d4eac73 100644 --- a/controllers/homeController.js +++ b/controllers/homeController.js @@ -1,315 +1,172 @@ -const { addBaseUrlToImages } = require('../utils/imageHelper'); -const Home = require('../models/home'); +const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper"); +const Home = require("../models/home"); +const Blog = require("../models/blog"); -// -------------------- Helper Functions -------------------- +// Các hàm hỗ trợ +const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 }); +const getHomeData = async () => (await getHomeDoc())?.toObject() || {}; -// Get home data from MongoDB -const getHomeData = async () => { - const home = await Home.findOne().sort({ updatedAt: -1 }).lean(); - return home || {}; -}; - -// Get default home data structure const getDefaultHomeData = () => ({ - hero: { - title: '', - description: '', - backgroundImage: '', - button: { label: 'Book Your Adventure', href: '/booking' }, - contactBox: { - welcomeText: '', - phone: { label: 'Call us', number: '', href: '' }, - email: { label: 'Email', address: '', href: '' }, - workingHours: { label: 'Working Hours', hours: '' } - } + hero: { title: "", subtitle: "", description: "", backgroundImage: "", videoUrl: "", primaryButton: {}, secondaryButton: {} }, + whyChooseUs: { heading: "", subheading: "", description: "", items: [], features: [], ctaButton: {} }, + visaSolutions: { heading: "", subheading: "", items: [] }, + visaCountries: { heading: "", subheading: "", description: "", countries: [], ctaButton: {} }, + testimonials: { heading: "", subheading: "", videoUrl: "", videoThumbnail: "", items: [] }, + videoGallery: { heading: "", videoUrl: "", thumbnail: "" }, + faq: { heading: "", subheading: "", description: "", ctaButton: {}, items: [] }, + achievements: { heading: "", subheading: "", items: [] }, + partners: { visaConsultancy: { items: [] }, brands: { items: [] } }, + blogPreview: { + heading: "Latest Insights & Updates", + subheading: "Visa Tips & Guides", + ctaButton: { label: "View All Articles", href: "/blog" }, + items: [], + selectedBlogIds: [] // Array of manually selected blog IDs }, - about: { - title: '', - subtitle: '', - description: '', - images: { mainImage1: '', mainImage2: '', avatars: [] }, - features: [], - quote: '', - button: { label: '', href: '' }, - stats: { customerCount: 0, customerLabel: '' } - }, - missionVision: { - title: '', - subtitle: '', - backgroundImage: '', - cards: [] - }, - whyChooseUs: { - title: '', - subtitle: '', - description: '', - button: { label: '', href: '' }, - features: [], - tags: [], - cta: { text: '', linkText: '', linkHref: '' } - }, - activities: { - cards: [] - }, - faq: { - title: '', - subtitle: '', - description: '', - image: '', - contact: { title: '', info: '' }, - questions: [] - }, - partners: { - title: '', - subtitle: '', - backgroundImage: '', - logos: [], - cta: { badge: '', text: '', linkText: '', linkHref: '' } - }, - programs: { - title: '', - subtitle: '', - button: { label: '', href: '' }, - card: { - pricePrefix: 'from', - priceSuffix: 'USD', - buttonLabel: 'Camp Detail', - buttonHref: '/camp-profiles' - }, - items: [] - }, - newsletter: { - title: '', - subtitle: '', - description: '', - image: '', - decorativeImage: '', - button: { - label: '', - placeholder: '', - href: '' - } - }, - latestPosts: { - title: '', - subtitle: '', - searchPlaceholder: '', - sidebarTitle: '', - blogPosts: [], - sidebarPosts: [], - featuredCard: { image: '', title: '', description: '' } - } }); - - -// -------------------- Admin Exports -------------------- - - -// Display home management page +// Admin: Xem trang quản lý exports.index = async (req, res) => { try { - // Fetch Home data let data = await getHomeData(); + const defaults = getDefaultHomeData(); - // If no data exists, use default - if (!data || Object.keys(data).length === 0) { - data = getDefaultHomeData(); - } else { - // Merge with defaults to ensure all fields exist - const defaultData = getDefaultHomeData(); - - // Ensure all sections exist with defaults - data.hero = data.hero || defaultData.hero; - data.about = data.about || defaultData.about; - data.missionVision = data.missionVision || defaultData.missionVision; - data.whyChooseUs = data.whyChooseUs || defaultData.whyChooseUs; - data.activities = data.activities || defaultData.activities; - data.faq = data.faq || defaultData.faq; - data.partners = data.partners || defaultData.partners; - data.programs = data.programs || defaultData.programs; - data.newsletter = data.newsletter || defaultData.newsletter; - data.latestPosts = data.latestPosts || defaultData.latestPosts; - } - + // Merge dữ liệu mặc định cho tất cả các phần + const sections = Object.keys(defaults); + sections.forEach(s => { + data[s] = data[s] || defaults[s]; + }); + const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; + const backendUrl = process.env.BACKEND_URL || "http://localhost:3001"; - const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; - - res.render('admin/home/index', { - layout: 'layouts/main', - title: 'Home Management', + // Lấy tất cả blog để chọn trong CMS + const allBlogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).lean(); + + return res.render("admin/home/index", { + layout: "layouts/main", + title: "Home Management", data, + allBlogs, frontendUrl, + backendUrl, + getFullImageUrl, currentPath: req.path, - user: req.session.user + user: req.session.user, }); } catch (err) { - console.error('Home index error:', err); - req.flash('error_msg', 'Error loading home data'); - res.redirect('/admin/dashboard'); + console.error("Home index error:", err); + req.flash("error_msg", "Error loading home data"); + return req.session.save(() => res.redirect("/admin/dashboard")); } }; -// Update home data +// Admin: Cập nhật dữ liệu (tập trung vào achievements, partners; các phần khác giữ nguyên nếu không có dữ liệu mới) exports.update = async (req, res) => { try { - // Get current data - const currentData = await getHomeData(); - - // Create updated data object - const updatedData = { ...(currentData.toObject ? currentData.toObject() : currentData) }; - - // Update Hero section data (from Welcome tab) - if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) { - updatedData.hero = { - title: req.body.heroTitle || '', - description: req.body.heroDescription || '', - backgroundImage: req.body.heroBackgroundImage || '', - button: { - label: req.body.heroButtonLabel || 'Book Your Adventure', - href: req.body.heroButtonHref || '/booking' - }, - contactBox: { - welcomeText: req.body.heroContactWelcome || '', - phone: { - label: 'Call us', - number: req.body.heroContactPhone || '', - href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : '' - }, - email: { - label: 'Email', - address: req.body.heroContactEmail || '', - href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : '' - }, - workingHours: { - label: 'Working Hours', - hours: req.body.heroContactHours || '' - } - } - }; + const sections = [ + "hero", "whyChooseUs", "visaSolutions", "visaCountries", + "testimonials", "videoGallery", "faq", "achievements", + "partners", "blogPreview" + ]; + + let doc = await getHomeDoc(); + if (!doc) { + doc = new Home({}); } - - // Update Why Choose Us section - if (req.body.whyChooseUsTitle || req.body.whyChooseUsSubtitle) { - updatedData.whyChooseUs = { - ...(updatedData.whyChooseUs || {}), - title: req.body.whyChooseUsTitle || '', - subtitle: req.body.whyChooseUsSubtitle || '', - description: req.body.whyChooseUsDescription || '', - button: { - label: req.body.whyChooseUsButtonLabel || '', - href: req.body.whyChooseUsButtonHref || '' - }, - features: updatedData.whyChooseUs?.features || [], - tags: updatedData.whyChooseUs?.tags || [], - cta: updatedData.whyChooseUs?.cta || { text: '', linkText: '', linkHref: '' } - }; - } - - // Handle Home sections (new camp structure only) - const sections = ['hero', 'about', 'missionVision', 'whyChooseUs', 'activities', 'faq', - 'partners', 'programs', 'newsletter', 'latestPosts']; - const errors = []; + let hasChanges = false; - - // Process each section for (const section of sections) { - try { - if (!req.body[section]) { - console.warn(`No data for section: ${section}`); - continue; - } - - // Parse JSON data from form - const newSectionData = JSON.parse(req.body[section]); - - // Check for changes - const currentSectionData = currentData[section]; - const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData); - - if (sectionHasChanges) { - updatedData[section] = newSectionData; + if (req.body[section]) { + try { + const payload = JSON.parse(req.body[section]); + // Gán trực tiếp vào doc, Mongoose sẽ tự check schema + doc[section] = payload; + doc.markModified(section); hasChanges = true; + } catch (e) { + console.error(`Invalid JSON for ${section}:`, e); } - } catch (error) { - console.error(`Error processing section ${section}:`, error); - errors.push(`Error processing ${section} data: ${error.message}`); } } - - // Handle errors - if (errors.length > 0) { - req.flash('error_msg', `Data processing error: ${errors[0]}`); - return req.session.save(() => res.redirect('/admin/home')); - } - - // Check if there are changes + if (!hasChanges) { - req.flash('info_msg', 'No changes were made'); - return req.session.save(() => res.redirect('/admin/home')); - } - - // Update or create document - try { - if (currentData._id) { - await Home.findByIdAndUpdate(currentData._id, updatedData, { new: true }); - } else { - await Home.create(updatedData); - } - - req.flash('success_msg', 'Home data updated successfully'); - return req.session.save(() => res.redirect('/admin/home')); - } catch (dbError) { - console.error('Database error:', dbError); - req.flash('error_msg', `Database error: ${dbError.message || 'Unknown'}`); - return req.session.save(() => res.redirect('/admin/home')); + req.flash("info_msg", "No changes were made"); + return req.session.save(() => res.redirect("/admin/home")); } + + await doc.save(); + req.flash("success_msg", "Home page configuration has been updated!"); + return req.session.save(() => res.redirect("/admin/home")); } catch (err) { - console.error('Update error:', err); - req.flash('error_msg', `Update error: ${err.message || 'Unknown'}`); - return req.session.save(() => res.redirect('/admin/home')); + console.error("Home update error:", err); + req.flash("error_msg", `Update error: ${err.message}`); + return req.session.save(() => res.redirect("/admin/home")); } }; -// -------------------- Public API Exports -------------------- - -// API to get home data for frontend +// Public API// API lấy danh sách blog cho CMS +exports.apiGetBlogs = async (req, res) => { + try { + const blogs = await Blog.find({ status: "published" }).sort({ createdAt: -1 }).select("title slug featuredImage author publishedAt").lean(); + res.json(blogs); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; exports.api = async (req, res) => { try { - const homeData = await getHomeData(); - + let data = await getHomeData(); + const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; + // === Xử lý Blog Preview động === + const blogPreview = data.blogPreview || {}; + let blogs = []; - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processedData = addBaseUrlToImages(homeData, baseUrl); + // Nếu có chọn blog cụ thể + if (blogPreview.selectedBlogIds && blogPreview.selectedBlogIds.length > 0) { + blogs = await Blog.find({ + _id: { $in: blogPreview.selectedBlogIds }, + status: "published" + }).lean(); + + // Sắp xếp theo thứ tự đã chọn trong selectedBlogIds + blogs.sort((a, b) => { + return blogPreview.selectedBlogIds.indexOf(a._id.toString()) - blogPreview.selectedBlogIds.indexOf(b._id.toString()); + }); + } - res.json(processedData); + // Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất (hoặc bù vào) + if (blogs.length === 0) { + blogs = await Blog.find({ status: "published" }) + .sort({ createdAt: -1 }) + .limit(3) + .lean(); + } + + // Map dữ liệu blog sang format mà frontend mong đợi + blogPreview.items = blogs.map(blog => ({ + title: blog.title, + excerpt: blog.excerpt, + category: blog.category && blog.category[0] ? blog.category[0] : "Visa", + date: blog.publishedAt || blog.createdAt, + author: { + name: blog.author || "Admin", + avatar: "" // Frontend đang tự xử lý hoặc dùng logo hệ thống + }, + comments: blog.commentsCount || 0, + link: `/blog/${blog.slug}`, + thumbnail: blog.featuredImage + })); + + data.blogPreview = blogPreview; + // =============================== + + const processed = addBaseUrlToImages(data, baseUrl); + return res.json(processed); } catch (err) { - console.error('Home API error:', err); - res.status(500).json({ error: 'Error loading home data' }); + console.error("Home API error:", err); + return res.status(500).json({ error: "Error loading home data" }); } }; -// API to get hero data for frontend -exports.apiHero = async (req, res) => { - try { - const homeData = await getHomeData(); - const heroData = homeData?.hero; - - if (!heroData) { - return res.status(404).json({ - error: 'Hero data not found', - data: null - }); - } - - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processedData = addBaseUrlToImages(heroData, baseUrl); - - res.json(processedData); - } catch (err) { - console.error('Hero API error:', err); - res.status(500).json({ error: 'Error loading hero data' }); - } -}; \ No newline at end of file diff --git a/controllers/visaController.js b/controllers/visaController.js index 47137bc..7a2ae6a 100644 --- a/controllers/visaController.js +++ b/controllers/visaController.js @@ -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); diff --git a/data/header-menu.json b/data/header-menu.json index 496291b..af22c79 100644 --- a/data/header-menu.json +++ b/data/header-menu.json @@ -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", diff --git a/data/home.json b/data/home.json index c7a1ac3..92bd9ab 100644 --- a/data/home.json +++ b/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." - } } -} \ No newline at end of file +} diff --git a/models/home.js b/models/home.js index ce5f44a..429d3be 100644 --- a/models/home.js +++ b/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); \ No newline at end of file diff --git a/routes/admin.js b/routes/admin.js index 829abaf..8a7b199 100644 --- a/routes/admin.js +++ b/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( diff --git a/scripts/2026_02_05_190000_home.js b/scripts/2026_02_05_190000_home.js new file mode 100644 index 0000000..b8f4dae --- /dev/null +++ b/scripts/2026_02_05_190000_home.js @@ -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(); + diff --git a/scripts/migrate-header.js b/scripts/migrate-header.js index 05b3dc7..8a9c06b 100644 --- a/scripts/migrate-header.js +++ b/scripts/migrate-header.js @@ -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: diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs index c04eaa0..3aec7e3 100644 --- a/views/admin/dashboard.ejs +++ b/views/admin/dashboard.ejs @@ -273,6 +273,25 @@ + + +
Manage visa countries
+Edit content displayed on homepage
@@ -19,75 +15,51 @@