diff --git a/controllers/aboutController.js b/controllers/aboutController.js deleted file mode 100644 index 5607453..0000000 --- a/controllers/aboutController.js +++ /dev/null @@ -1,161 +0,0 @@ -const { addBaseUrlToImages } = require('../utils/imageHelper'); -const About = require('../models/about'); - -// Get about data from MongoDB -const getAboutData = async () => { - const about = await About.findOne().sort({ updatedAt: -1 }); - - // Trả về object rỗng với cấu trúc cơ bản nếu không có dữ liệu - if (!about) { - return { - banner: { - image: '', - title: '', - text: '' - }, - about: { - title: '', - paragraphs: [], - list_items: [], - button: { - text: '', - url: '' - }, - image: '', - quote: { - mark_image: '', - title: '', - text: '', - author: '' - } - }, - values: { - background_image: '', - items: [] - }, - education: { - images: { - student1: '', - student2: '' - }, - subtitle: '', - title: '', - text: '' - }, - advantages: { - title: '', - items: [] - }, - academic_board: { - title: '', - members: [] - } - }; - } - - return about; -}; - -// Display about management page -exports.index = async (req, res) => { - try { - const data = await getAboutData(); - res.render('admin/about', { - title: 'About Management', - data - }); - } catch (err) { - console.error(err); - req.flash('error_msg', 'Error loading about data'); - res.redirect('/admin/dashboard'); - } -}; - -// Update about data -exports.update = async (req, res) => { - try { - // Lấy document hiện tại từ MongoDB - const currentData = await getAboutData(); - - // Danh sách các section cần cập nhật - const sections = ['banner', 'about', 'values', 'education', 'advantages', 'academic_board']; - const errors = []; - let hasChanges = false; - - // Tạo đối tượng dữ liệu mới dựa trên dữ liệu hiện tại - const updatedData = { ...currentData.toObject() }; - - // Xử lý từng section - sections.forEach(section => { - try { - // Kiểm tra nếu section không được gửi lên - if (!req.body[section]) { - console.warn(`No data for section: ${section}`); - return; - } - - // Parse dữ liệu JSON từ form - const newSectionData = JSON.parse(req.body[section]); - - // So sánh dữ liệu mới với dữ liệu hiện tại - const currentSectionData = currentData[section]; - const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData); - - // Nếu có thay đổi, cập nhật vào đối tượng dữ liệu mới - if (sectionHasChanges) { - updatedData[section] = newSectionData; - hasChanges = true; - } - } catch (error) { - console.error(`Error processing section ${section}:`, error); - errors.push(`Error processing ${section} data: ${error.message}`); - } - }); - - // Nếu có lỗi, thông báo và chuyển hướng - if (errors.length > 0) { - req.flash('error_msg', `Data processing error: ${errors[0]}`); - return req.session.save(() => res.redirect('/admin/about')); - } - - // Nếu không có thay đổi, thông báo và chuyển hướng - if (!hasChanges) { - req.flash('info_msg', 'No changes were made'); - return req.session.save(() => res.redirect('/admin/about')); - } - - try { - // Cập nhật hoặc tạo mới document trong MongoDB - if (currentData._id) { - await About.findByIdAndUpdate(currentData._id, updatedData, { new: true }); - } else { - await About.create(updatedData); - } - - // Success notification and redirect - req.flash('success_msg', 'About data updated successfully'); - return req.session.save(() => res.redirect('/admin/about')); - } catch (dbError) { - console.error('Database error:', dbError); - req.flash('error_msg', `Database error: ${dbError.message || 'Unknown'}`); - return req.session.save(() => res.redirect('/admin/about')); - } - } catch (err) { - console.error('Update error:', err); - req.flash('error_msg', `Update error: ${err.message || 'Unknown'}`); - return req.session.save(() => res.redirect('/admin/about')); - } -}; - -// API to get about data -exports.api = async (req, res) => { - try { - const aboutData = await getAboutData(); - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processedData = addBaseUrlToImages(aboutData, baseUrl); - res.json(processedData); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Error loading about data' }); - } -}; \ No newline at end of file diff --git a/controllers/aboutUsController.js b/controllers/aboutUsController.js index 0910c02..cf94d30 100644 --- a/controllers/aboutUsController.js +++ b/controllers/aboutUsController.js @@ -1,363 +1,141 @@ -const {addBaseUrlToImages} = require("../utils/imageHelper"); -const About = require("../models/about"); +const { addBaseUrlToImages } = require("../utils/imageHelper"); const AboutUs = require("../models/aboutUs"); +const jsonHelper = require("../utils/jsonHelper"); -// -------------------- Public (read-only) helpers -------------------- -// Map stored About document back to the original aboutUs.json shape -function transformToAboutUs(doc) { - if (!doc) return null; - - const hero = { - banner: doc.banner?.image || "", - title: doc.banner?.title || "", - breadcrumb: doc.banner?.text || "", - }; - - const stats = Array.isArray(doc.advantages?.items) - ? doc.advantages.items.map((item) => ({ - number: item.number || "", - description: item.title || "", - })) - : []; - - const services = Array.isArray(doc.about?.paragraphs) - ? doc.about.paragraphs.map((p) => ({title: "", description: p})) - : []; - - const features = Array.isArray(doc.values?.items) - ? doc.values.items.map((i) => ({ - title: i.title || "", - description: i.text || "", - icon: i.icon || "", - })) - : []; - - const events = Array.isArray(doc.academic_board?.members) - ? doc.academic_board.members.map((m) => ({ - imageUrl: m.image || "", - date: "", - title: m.title || "", - description: "", - authorName: m.name || "", - authorRole: "", - })) - : []; - - return { - hero, - stats, - services, - features, - events, - }; -} - -// Get aboutUs data: prefer AboutUs collection, fallback to transforming About -const getAboutUsData = async () => { - // Prefer stored AboutUs document - const aboutUsDoc = await AboutUs.findOne().sort({updatedAt: -1}); - if (aboutUsDoc) - return aboutUsDoc.toObject ? aboutUsDoc.toObject() : aboutUsDoc; - - // Fallback: transform legacy About document into aboutUs shape - const about = await About.findOne().sort({updatedAt: -1}); - if (!about) return null; - return transformToAboutUs(about); -}; - -// -------------------- Admin (CRUD on AboutUs model) helpers -------------------- -// Default shape for AboutUs documents (matches data/aboutUs.json) -const getDefaultAboutUsData = () => ({ - hero: {title: "", backgroundImage: ""}, - introduction: { - subtitle: "", - title: "", - description: "", - mainImage: "", - services: [], - }, - statistics: { - items: [], - }, - accommodation: { - subtitle: "", - title: "", - description: "", - features: [], - }, - activities: { - subtitle: "", - title: "", - description: "", - gallery: [], - }, - newsletter: { - imagePath: "", - title: "", - description: "", - buttonText: "", - }, - events: { - title: "", - items: [], - }, -}); - -// Get latest stored AboutUs document or default (returned as plain object) -const getStoredAboutUs = async () => { - const aboutUs = await AboutUs.findOne().sort({updatedAt: -1}); - if (!aboutUs) return getDefaultAboutUsData(); - return aboutUs.toObject ? aboutUs.toObject() : aboutUs; -}; - -// -------------------- Public exports -------------------- -// Public endpoint: return AboutUs JSON (previously rendered HTML) -exports.page = async (req, res) => { - try { - const aboutUsData = await getAboutUsData(); - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processed = addBaseUrlToImages(aboutUsData || {}, baseUrl); - return res.json(processed); - } catch (err) { - console.error("aboutUs.page error:", err); - return res.status(500).json({ error: "Error loading about-us data" }); - } -}; - -// API endpoint to return aboutUs JSON -exports.api = async (req, res) => { - try { - const aboutUsData = await getAboutUsData(); - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processed = addBaseUrlToImages(aboutUsData || {}, baseUrl); - return res.json(processed); - } catch (err) { - console.error("aboutUs.api error:", err); - return res.status(500).json({error: "Error loading about-us data"}); - } -}; - -// API endpoint to return an array of AboutUs records (for frontend listing) -exports.apiList = async (req, res) => { - try { - const docs = await AboutUs.find().sort({ updatedAt: -1 }).lean(); - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processed = (docs || []).map((d) => addBaseUrlToImages(d || {}, baseUrl)); - return res.json(processed); - } catch (err) { - console.error("aboutUs.apiList error:", err); - return res.status(500).json({ error: "Error loading about-us list" }); - } -}; - -// -------------------- Admin exports -------------------- -// Display AboutUs management page -exports.index = async (req, res) => { - try { - const data = await getStoredAboutUs(); - const items = await AboutUs.find().sort({updatedAt: -1}).limit(10); - - res.render("admin/aboutUs/index", { - layout: "layouts/main", - title: "About Us Management", - data, - items, - frontendUrl: - process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"), - currentPath: req.path, - user: req.session.user, - }); - } catch (err) { - console.error(err); - req.flash("error_msg", "Error loading About Us data"); - res.redirect("/admin/dashboard"); - } -}; - -// Display create form -exports.createForm = async (req, res) => { - try { - const data = getDefaultAboutUsData(); - - res.render("admin/aboutUs/create", { - layout: "layouts/main", - title: "Create About Us", - data, - currentPath: req.path, - user: req.session.user, - }); - } catch (err) { - console.error(err); - req.flash("error_msg", "Error loading create form"); - res.redirect("/admin/about-us"); - } -}; - -// Create new AboutUs record -exports.create = async (req, res) => { - try { - const aboutUsData = { - hero: JSON.parse(req.body.hero || "{}"), - introduction: JSON.parse(req.body.introduction || "{}"), - statistics: JSON.parse(req.body.statistics || "{}"), - accommodation: JSON.parse(req.body.accommodation || "{}"), - activities: JSON.parse(req.body.activities || "{}"), - newsletter: JSON.parse(req.body.newsletter || "{}"), - events: JSON.parse(req.body.events || "{}"), - }; - - const newAboutUs = new AboutUs(aboutUsData); - await newAboutUs.save(); - - req.flash("success_msg", "About Us created successfully"); - res.redirect("/admin/about-us"); - } catch (err) { - console.error("Create error:", err); - req.flash("error_msg", `Create error: ${err.message || "Unknown"}`); - res.redirect("/admin/about-us/create"); - } -}; - -// Display edit form -exports.editForm = async (req, res) => { - try { - const aboutUs = await AboutUs.findById(req.params.id); - - if (!aboutUs) { - req.flash("error_msg", "About Us record not found"); - return res.redirect("/admin/about-us"); - } - - res.render("admin/aboutUs/edit", { - layout: "layouts/main", - title: "Edit About Us", - data: aboutUs.toObject ? aboutUs.toObject() : aboutUs, - currentPath: req.path, - user: req.session.user, - }); - } catch (err) { - console.error(err); - req.flash("error_msg", "Error loading edit form"); - res.redirect("/admin/about-us"); - } -}; - -// Update AboutUs record -exports.update = async (req, res) => { - try { - // Get current data - const currentData = await getStoredAboutUs(); - - // Parse form data - const sections = [ - "hero", - "introduction", - "statistics", - "accommodation", - "activities", - "newsletter", - "events", - ]; - const errors = []; - let hasChanges = false; - - // Create updated data object - const updatedData = { - ...(currentData.toObject ? currentData.toObject() : currentData), - }; - - // Process each section - sections.forEach((section) => { - try { - if (!req.body[section]) { - console.warn(`No data for section: ${section}`); - return; - } - - const newSectionData = JSON.parse(req.body[section]); - const currentSectionData = currentData[section]; - const sectionHasChanges = - JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData); - - if (sectionHasChanges) { - updatedData[section] = newSectionData; - hasChanges = true; - } - } catch (error) { - console.error(`Error processing section ${section}:`, error); - errors.push(`Error processing ${section} data: ${error.message}`); - } - }); - - if (errors.length > 0) { - req.flash("error_msg", `Data processing error: ${errors[0]}`); - return req.session.save(() => res.redirect("/admin/about-us")); - } - - if (!hasChanges) { - req.flash("info_msg", "No changes were made"); - return req.session.save(() => res.redirect("/admin/about-us")); - } - +/** + * GET /api/about + * Lấy dữ liệu About Us (Public API cho website và CMS load dữ liệu) + */ +exports.getAbout = async (req, res) => { try { - // Only update existing document; do not create a new one here - if (!currentData || !currentData._id) { - req.flash("error_msg", "No existing About Us record to update. Create one first."); - return req.session.save(() => res.redirect("/admin/about-us")); - } + // Force no-cache headers + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); - await AboutUs.findByIdAndUpdate(currentData._id, updatedData, { - new: true, - }); + const data = await AboutUs.getSingle(); + const rawData = data.toObject(); - req.flash("success_msg", "About Us data updated successfully"); - return req.session.save(() => res.redirect("/admin/about-us")); - } catch (dbError) { - console.error("Database error:", dbError); - req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`); - return req.session.save(() => res.redirect("/admin/about-us")); + const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; + const processedData = addBaseUrlToImages(rawData, baseUrl); + + res.json(processedData); + } catch (error) { + console.error("Error getting about data:", error); + res.status(500).json({ + success: false, + error: "Failed to get about data" + }); } - } catch (err) { - console.error("Update error:", err); - req.flash("error_msg", `Update error: ${err.message || "Unknown"}`); - return req.session.save(() => res.redirect("/admin/about-us")); - } }; -// Delete AboutUs record -exports.delete = async (req, res) => { - try { - const aboutUs = await AboutUs.findById(req.params.id); +/** + * PUT /api/about + * Cập nhật dữ liệu About Us (Dùng cho AJAX từ CMS) + */ +exports.updateAbout = async (req, res) => { + try { + let updateData = req.body; - if (!aboutUs) { - req.flash("error_msg", "About Us record not found"); - return res.redirect("/admin/about-us"); + // Nếu dữ liệu gửi qua trường aboutJson (dạng string JSON) + if (updateData.aboutJson && typeof updateData.aboutJson === "string") { + try { + updateData = JSON.parse(updateData.aboutJson); + } catch (e) { + return res.status(400).json({ + success: false, + message: "Invalid JSON in aboutJson" + }); + } + } + + const doc = await AboutUs.getSingle(); + // Use .set() for better handling of nested objects/arrays in Mongoose + doc.set(updateData); + await doc.save(); + + // Fetch fresh data for syncing and returning + const finalData = await AboutUs.findOne() + .select('-_id -__v -createdAt -updatedAt') + .lean(); + + // Update about.json file to keep it in sync + jsonHelper.writeJsonFile("about", finalData); + + res.json({ + success: true, + message: "About Us updated successfully", + data: finalData + }); + } catch (error) { + console.error("Error updating about data:", error); + res.status(500).json({ + success: false, + error: "Failed to update about data: " + error.message + }); } - - await AboutUs.findByIdAndDelete(req.params.id); - - req.flash("success_msg", "About Us record deleted successfully"); - res.redirect("/admin/about-us"); - } catch (err) { - console.error("Delete error:", err); - req.flash("error_msg", `Delete error: ${err.message || "Unknown"}`); - res.redirect("/admin/about-us"); - } }; -// Preview AboutUs record -exports.preview = async (req, res) => { - try { - const aboutUs = await AboutUs.findById(req.params.id); +/** + * Render admin page (Dùng cho Admin UI) + */ +exports.index = async (req, res) => { + try { + const data = await AboutUs.getSingle(); + const rawData = data.toObject(); - if (!aboutUs) { - return res.status(404).json({error: "About Us record not found"}); + const activeTab = req.query.activeTab || "hero"; + res.render("admin/aboutUs/index", { + layout: "layouts/main", + title: "About Us Management", + data: rawData, + activeTab, + user: req.session.user, + currentPath: req.path, + frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000' + }); + } catch (err) { + console.error("Error in about index:", err); + req.flash("error_msg", "Error loading About Us page"); + res.redirect("/admin/dashboard"); } - - const processedData = addBaseUrlToImages(aboutUs.toObject()); - res.json(processedData); - } catch (err) { - console.error("Preview error:", err); - res.status(500).json({error: "Error loading preview data"}); - } }; + +/** + * Update method cho form-based submission (Admin UI - Post fallback) + */ +exports.update = async (req, res) => { + try { + let updateData = req.body; + if (updateData.aboutJson && typeof updateData.aboutJson === "string") { + try { + updateData = JSON.parse(updateData.aboutJson); + } catch (e) { + req.flash("error_msg", "Invalid JSON data"); + return res.redirect("/admin/about-us"); + } + } + + const doc = await AboutUs.getSingle(); + doc.set(updateData); + await doc.save(); + + const finalData = await AboutUs.findOne() + .select('-_id -__v -createdAt -updatedAt') + .lean(); + jsonHelper.writeJsonFile("about", finalData); + + req.flash("success_msg", "About Us updated successfully"); + const activeTab = req.query.activeTab || "hero"; + res.redirect(`/admin/about-us?activeTab=${activeTab}`); + } catch (err) { + console.error("Update error:", err); + req.flash("error_msg", "Error updating About Us: " + err.message); + res.redirect("/admin/about-us"); + } +}; + +// Aliases for compatibility +exports.api = exports.getAbout; +exports.page = exports.getAbout; +exports.updateAboutUs = exports.updateAbout; diff --git a/data/about.json b/data/about.json new file mode 100644 index 0000000..ac1e658 --- /dev/null +++ b/data/about.json @@ -0,0 +1,123 @@ +{ + "hero": { + "title": "About Us", + "breadcrumb": [ + "Home", + "About Us" + ], + "backgroundImage": "/uploads/about/breadcrumb.jpg" + }, + "intro": { + "subheading": "Company Intro", + "heading": "Building Pathways to Your Immigration Success", + "description": "We provide expert guidance, personalized solutions, and transparent processes to help you achieve your immigration goals. Our dedicated team ensures a smooth journey, building pathways to your international success.", + "image": "http://localhost:3001/uploads/about/intro.jpg" + }, + "mission": { + "subheading": "About Our Consultancy", + "heading": "Turning Study Abroad Dreams Into Reality", + "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.", + "images": { + "main": "/assets/img/home-1/about/about-1.jpg", + "secondary": "/assets/img/home-1/about/about-02.jpg", + "bgShape": "/assets/img/home-1/about/Vector.png", + "planeShape": "/assets/img/home-1/about/plane.png", + "topShape": "/assets/img/home-1/about/shape.png", + "globeShape": "/assets/img/home-1/about/globe.png" + }, + "items": [ + { + "icon": "/assets/img/home-1/icon/01.svg", + "label": "Global Reach", + "description": "Expanding Opportunities Worldwide" + }, + { + "icon": "/assets/img/home-1/icon/01.svg", + "label": "Global Reach", + "description": "Expanding Opportunities Worldwide" + } + ], + "features": [ + "Fastest Visa form processing with skilled immigration agents", + "Partnership with International Educational Institutions" + ], + "ctaButton": { + "label": "Get Started", + "href": "/about" + } + }, + "features": { + "backgroundImage": "/assets/img/home-2/feature/bg-shape.png", + "subheading": "Your Travel Made Easy", + "heading": "Smooth Visa Journey Guaranteed", + "description": "We provide expert guidance for every visa application, ensuring smooth processing, personalized support, and reliable assistance", + "image": "/assets/img/home-2/feature/02.png", + "items": [ + { + "icon": "/assets/img/home-2/icon/01.png", + "title": "Expert Consultants", + "description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors." + }, + { + "icon": "/assets/img/home-2/icon/01.png", + "title": "Personalized Support", + "description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors." + }, + { + "icon": "/assets/img/home-2/icon/01.png", + "title": "Transparent Process", + "description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors." + } + ], + "ctaButton": { + "label": "Get Started Today", + "href": "/contact" + } + }, + "news": { + "subheading": "Visa Tips & Guides", + "heading": "Latest Insights & Updates", + "ctaButton": { + "label": "view all articles", + "href": "/blog" + }, + "items": [ + { + "title": "Step-by-Step Guide to Applying for a Student Visa", + "category": "Student Visa", + "date": "20 August ,2025", + "comments": 8, + "author": { + "name": "Sohel", + "avatar": "/assets/img/home-1/news/client.png" + }, + "link": "/blog/step-by-step-guide-student-visa", + "thumbnail": "/assets/img/home-1/news/news-1.jpg" + }, + { + "title": "Tips to Prepare Financial Documents for Visa Approval", + "category": "IELTS / TOEFL", + "date": "20 August ,2025", + "comments": 8, + "author": { + "name": "Sohel", + "avatar": "/assets/img/home-1/news/client.png" + }, + "link": "/blog/financial-documents-visa-approval", + "thumbnail": "/assets/img/home-1/news/news-2.jpg" + }, + { + "title": "Post-Arrival Guide What Every Student Should Know", + "category": "Study Abroad", + "date": "20 August ,2025", + "comments": 8, + "author": { + "name": "Sohel", + "avatar": "/assets/img/home-1/news/client.png" + }, + "link": "/blog/post-arrival-guide-students", + "thumbnail": "/assets/img/home-1/news/news-3.jpg" + } + ] + } +} \ No newline at end of file diff --git a/data/aboutCamp.json b/data/aboutCamp.json deleted file mode 100644 index e68576e..0000000 --- a/data/aboutCamp.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "avatars": [ - "yootheme/aboutImage/profile-face_1.jpg", - "yootheme/aboutImage/young-tourist-sitting-tent.jpg", - "yootheme/aboutImage/portrait-young-male-tourist-standing-forest-with-tent.jpg" - ], - "images": { - "mainImage1": "yootheme/img/a1.jpg", - "mainImage2": "yootheme/img/a2.jpg" - }, - "content": { - "sectionTitle": "About Us", - "mainTitle": "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.", - "quote": "Your Journey, Your Comfort,\nYour Adventure.", - "authorText": "Adventurer with\nhappy customer", - "targetCount": 50 - }, - "features": [ - "Fun-Filled Experiences for Every Camper", - "Adventures That Inspire Confidence and Growth", - "Memories and Friendships That Last a Lifetime" - ], - "button": { - "label": "Learn More About", - "href": "/info/about" - } -} diff --git a/data/aboutUs.json b/data/aboutUs.json deleted file mode 100644 index f175dfe..0000000 --- a/data/aboutUs.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "hero": { - "title": "About Us", - "backgroundImage": "/uploads/about/banner.jpg" - }, - - "introduction": { - "subtitle": "Go & Grow Camp", - "title": "Go & Grow Camp A Place to Learn, Connect, and Grow", - "description": "Go & Grow Camp brings together young people from different countries and cultures to enjoy fun activities, meaningful projects, and positive community experiences. Every camper—new or returning—quickly feels included thanks to our welcoming environment and supportive team.", - "mainImage": "/uploads/about/section2.jpg", - "services": [ - { - "title": "Always Here", - "description": "Camp leaders are ready to guide and support you whenever needed." - }, - { - "title": "Fun & Learning", - "description": "Engage in exciting activities that help you grow new skills." - }, - { - "title": "Team Spirit", - "description": "Work together, take responsibility, and support each other at camp." - } - ] - }, - - "statistics": { - "items": [ - { - "number": "2K+", - "description": "Smiles and Friendships Made" - }, - { - "number": "25+", - "description": "Countries Connected" - }, - { - "number": "50+", - "description": "Adventure & Skill-Building Activities" - }, - { - "number": "20+", - "description": "Exciting Challenges Every Camp" - } - ] - }, - - "accommodation": { - "subtitle": "Accommodation", - "title": "Blending Comfort With Responsible Living", - "description": "Enjoy a tranquil atmosphere with beautiful views, modern facilities, and personal touches that make you feel at home.", - "features": [ - { - "title": "Safe Environment", - "description": "Safety is our top priority, with secure facilities and connecting with nature.", - "icon": "/uploads/about/act2.jpg" - }, - { - "title": "Family Atmosphere", - "description": "Every camper is part of our big camp family, where friendships grow and everyone feels included.", - "icon": "/uploads/about/act2.jpg" - }, - { - "title": "Cultural Exchange", - "description": "Experience diversity and learn about different cultures from campers around the world.", - "icon": "/uploads/about/act2.jpg" - }, - { - "title": "Personal Growth", - "description": "Activities encourage confidence, independence, and learning through fun challenges.", - "icon": "/uploads/about/act2.jpg" - }, - { - "title": "Creativity & Fun", - "description": "Express yourself through games, arts, and exciting hands-on experiences.", - "icon": "/uploads/about/act2.jpg" - }, - { - "title": "Creativity & Fun", - "description": "Express yourself through games, arts, and exciting hands-on experiences.", - "icon": "/uploads/about/act2.jpg" - } - ] - }, - - "activities": { - "subtitle": "Activities", - "title": "Enjoy unforgettable experiences at Go and Grow Camp", - "description": "Discover a world of adventure, creativity, and friendship. From exciting outdoor activities to hands-on workshops, every day is full of new experiences that help campers grow, have fun, and make memories that last a lifetime.", - "gallery": [ - { - "image": "/uploads/about/act1.jpg", - "title": "Outdoor Adventures", - "description": "Climb, paddle and explore with our experienced team." - }, - { - "image": "/uploads/about/act2.jpg", - "title": "Creative Workshops", - "description": "Arts & crafts sessions to spark imagination." - }, - { - "image": "/uploads/about/act3.jpg", - "title": "Water Sports", - "description": "Safe swimming and supervised water activities." - }, - { - "image": "/uploads/about/act4.jpg", - "title": "Campfire Nights", - "description": "Evening stories, music, and marshmallow roasting." - } - ] - }, - - "newsletter": { - "imagePath": "/uploads/about/newsletter.jpg", - "title": "Stay Updated with Our Monthly", - "description": "Sign up to receive the latest news about new camps, activities, and exciting opportunities. Don't miss out on anything fun!", - "buttonText": "Subscribe" - }, - - "events": { - "title": "Tour Events for you", - "items": [ - - { - "imageUrl": "/uploads/about/act1.jpg", - "date": "September 19, 2022", - "title": "The Bottom Line on Dietary Supplements", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sit amet tristique neque, in suscipit sem. Fusce ut tellus neque. Suspendisse ...", - "age": "Age Group: 10–14" - }, - { - "imageUrl": "/uploads/about/act2.jpg", - "date": "September 19, 2022", - "title": "The Bottom Line on Dietary Supplements", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sit amet tristique neque, in suscipit sem. Fusce ut tellus neque. Suspendisse ...", - "age": "Age Group: 10–14" - }, - { - "imageUrl": "/uploads/about/act3.jpg", - "date": "September 19, 2022", - "title": "The Bottom Line on Dietary Supplements", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sit amet tristique neque, in suscipit sem. Fusce ut tellus neque. Suspendisse ...", - "age": "Age Group: 10–14" - } - ] - } -} \ No newline at end of file diff --git a/models/about.js b/models/about.js deleted file mode 100644 index 18dc509..0000000 --- a/models/about.js +++ /dev/null @@ -1,64 +0,0 @@ -const mongoose = require('mongoose'); - -const aboutSchema = new mongoose.Schema({ - banner: { - image: String, - title: String, - text: String - }, - about: { - title: String, - paragraphs: [String], - list_items: [String], - button: { - text: String, - url: String - }, - image: String, - quote: { - mark_image: String, - title: String, - text: String, - author: String - } - }, - values: { - background_image: String, - items: [{ - icon: String, - title: String, - text: String - }] - }, - education: { - images: { - student1: String, - student2: String - }, - subtitle: String, - title: String, - text: String - }, - advantages: { - title: String, - items: [{ - number: String, - title: String, - text: String - }] - }, - academic_board: { - title: String, - members: [{ - image: String, - title: String, - name: String, - color: String - }] - }, - updatedAt: Date -}, { - timestamps: true -}); - -module.exports = mongoose.model('About', aboutSchema); \ No newline at end of file diff --git a/models/aboutUs.js b/models/aboutUs.js index f38aa82..1c0bfda 100644 --- a/models/aboutUs.js +++ b/models/aboutUs.js @@ -2,87 +2,105 @@ const mongoose = require("mongoose"); const aboutUsSchema = new mongoose.Schema( { - // Hero section hero: { title: String, + breadcrumb: [String], backgroundImage: String, }, - - // Introduction section with nested services - introduction: { - subtitle: String, - title: String, + intro: { + subheading: String, + heading: String, description: String, - mainImage: String, - services: [ - { - title: String, - description: String, - }, - ], + image: String, }, - - // Statistics with nested items - statistics: { + mission: { + subheading: String, + heading: String, + description: String, + images: { + main: String, + secondary: String, + bgShape: String, + planeShape: String, + topShape: String, + globeShape: String, + }, items: [ - { - number: String, - description: String, - }, + new mongoose.Schema( + { + icon: String, + label: String, + description: String, + }, + { _id: false } + ), ], + features: [String], + ctaButton: { + label: String, + href: String, + }, }, - - // Accommodation section with nested features - accommodation: { - subtitle: String, - title: String, + features: { + backgroundImage: String, + subheading: String, + heading: String, description: String, - features: [ - { - title: String, - description: String, - icon: String, - }, - ], - }, - - // Activities section with nested gallery - activities: { - subtitle: String, - title: String, - description: String, - gallery: [ - { - image: String, - title: String, - description: String, - }, - ], - }, - - // Newsletter - newsletter: { - imagePath: String, - title: String, - description: String, - buttonText: String, - }, - - // Events with nested items - events: { - title: String, + image: String, items: [ - { - imageUrl: String, - date: String, - title: String, - description: String, - age: String, - }, + new mongoose.Schema( + { + icon: String, + title: String, + description: String, + }, + { _id: false } + ), + ], + ctaButton: { + label: String, + href: String, + }, + }, + news: { + subheading: String, + heading: String, + ctaButton: { + label: String, + href: String, + }, + items: [ + new mongoose.Schema( + { + title: String, + category: String, + date: String, + comments: Number, + author: { + name: String, + avatar: String, + }, + link: String, + thumbnail: String, + }, + { _id: false } + ), ], }, }, - {timestamps: true} + { + timestamps: true, + collection: "aboutus", + } ); +// Static method để đảm bảo luôn chỉ có 1 bản ghi duy nhất (Singleton) +aboutUsSchema.statics.getSingle = async function () { + let doc = await this.findOne(); + if (!doc) { + doc = await this.create({}); + } + return doc; +}; + module.exports = mongoose.model("AboutUs", aboutUsSchema); diff --git a/routes/admin.js b/routes/admin.js index 029220a..9e5bb0e 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -4,7 +4,6 @@ const { ensureAuthenticated } = require("../middleware/auth"); const dashboardController = require("../controllers/dashboardController"); const uploadController = require("../controllers/uploadController"); const homeController = require("../controllers/homeController"); -const aboutController = require("../controllers/aboutController"); const headerController = require("../controllers/headerController"); const footerController = require("../controllers/footerController"); const aboutUsController = require("../controllers/aboutUsController"); @@ -44,18 +43,9 @@ router.param("code", (req, res, next, code) => { next(); }); -// About -router.get("/about", ensureAuthenticated, aboutController.index); -router.post("/about/update", ensureAuthenticated, aboutController.update); - -// AboutUs admin CRUD +// About Us router.get("/about-us", ensureAuthenticated, aboutUsController.index); -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.post("/about-us/update", ensureAuthenticated, aboutUsController.update); // Booking admin CRUD removed diff --git a/routes/index.js b/routes/index.js index ed05417..9c06203 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,7 +2,6 @@ const express = require("express"); const path = require("path"); const router = express.Router(); const homeController = require("../controllers/homeController"); -const aboutController = require("../controllers/aboutController"); const aboutUsController = require("../controllers/aboutUsController"); const headerController = require("../controllers/headerController"); const socialLinkController = require("../controllers/socialLinkController"); @@ -38,12 +37,12 @@ router.get("/", (req, res) => { router.get("/api/home", homeController.api); // API để lấy dữ liệu about -router.get("/api/about", aboutController.api); +router.get("/api/about", aboutUsController.getAbout); +router.put("/api/about", aboutUsController.updateAbout); -// Public about-us page and API (aboutUs.json flow) -router.get("/about-us", aboutUsController.page); -// Return a list/array of AboutUs records for frontend consumption -router.get("/api/about-us", aboutUsController.apiList); +// Public about-us page and API (legacy support) +router.get("/about-us", aboutUsController.getAbout); +router.get("/api/about-us", aboutUsController.getAbout); // Header API route router.get("/api/header", headerController.api); diff --git a/scripts/migrateAboutUs.js b/scripts/migrateAboutUs.js new file mode 100644 index 0000000..29b705d --- /dev/null +++ b/scripts/migrateAboutUs.js @@ -0,0 +1,48 @@ +const mongoose = require("mongoose"); +const dotenv = require("dotenv"); +const fs = require("fs"); +const path = require("path"); + +// Load environment variables +dotenv.config(); + +const AboutUs = require("../models/aboutUs"); + +const migrate = async () => { + try { + console.log("🚀 Starting About Us migration..."); + + // 1. Connect to MongoDB + await mongoose.connect(process.env.MONGODB_URI); + console.log("✅ MongoDB Connected"); + + // 2. Read about.json from Backend (Source of Truth) + const jsonPath = path.join(__dirname, "../data/about.json"); + if (!fs.existsSync(jsonPath)) { + throw new Error(`Source about.json not found at: ${jsonPath}`); + } + + const rawData = fs.readFileSync(jsonPath, "utf8"); + const jsonData = JSON.parse(rawData); + console.log("✅ Read about.json successfully"); + + // 3. Delete existing AboutUs documents (Singleton pattern) + await AboutUs.deleteMany({}); + console.log("✅ Cleared existing AboutUs collection"); + + // 4. Create new AboutUs document with JSON data + const newAboutUs = new AboutUs(jsonData); + await newAboutUs.save(); + console.log("✅ Successfully migrated about.json data to MongoDB"); + + } catch (error) { + console.error("❌ Migration failed:", error.message); + } finally { + // 5. Close connection + await mongoose.connection.close(); + console.log("👋 Database connection closed"); + process.exit(0); + } +}; + +migrate(); diff --git a/scripts/seedAbout.js b/scripts/seedAbout.js new file mode 100644 index 0000000..3e60620 --- /dev/null +++ b/scripts/seedAbout.js @@ -0,0 +1,52 @@ +const mongoose = require("mongoose"); +const dotenv = require("dotenv"); +const fs = require("fs"); +const path = require("path"); + +// Load environment variables +dotenv.config(); + +const AboutUs = require("../models/aboutUs"); + +const seedAbout = async () => { + try { + console.log("🚀 Starting About section seeding..."); + + // 1. Connect to MongoDB + if (!process.env.MONGODB_URI) { + throw new Error("MONGODB_URI is not defined in environment variables"); + } + await mongoose.connect(process.env.MONGODB_URI); + console.log("✅ MongoDB Connected"); + + // 2. Read about.json (Single Source of Truth) + const jsonPath = path.join(__dirname, "../data/about.json"); + if (!fs.existsSync(jsonPath)) { + throw new Error(`Source about.json not found at: ${jsonPath}`); + } + + const rawData = fs.readFileSync(jsonPath, "utf8"); + const jsonData = JSON.parse(rawData); + console.log("✅ Read data/about.json successfully"); + + // 3. Upsert logic (Singleton pattern) + // We look for any existing document and update it, or create a new one if none exists. + await AboutUs.findOneAndUpdate( + {}, + jsonData, + { upsert: true, new: true, setDefaultsOnInsert: true } + ); + + console.log("✅ Successfully seeded about.json data to MongoDB (Upserted)"); + + } catch (error) { + console.error("❌ Seeding failed:", error.message); + } finally { + // 4. Close connection + await mongoose.connection.close(); + console.log("👋 Database connection closed"); + process.exit(0); + } +}; + +seedAbout(); diff --git a/server.js b/server.js index 721fac3..2a187ad 100644 --- a/server.js +++ b/server.js @@ -44,6 +44,9 @@ app.use( express.static(path.join(__dirname, "assets")), ); +// Map /assets/img to public/img to support frontend paths +app.use("/assets/img", express.static(path.join(__dirname, "public", "img"))); + // Serve public folder at root (for /js, /img, etc.) app.use(express.static(path.join(__dirname, "public"))); diff --git a/utils/imageHelper.js b/utils/imageHelper.js index bce7697..a894130 100644 --- a/utils/imageHelper.js +++ b/utils/imageHelper.js @@ -4,8 +4,8 @@ * @returns {Object} - Dữ liệu đã được xử lý với đường dẫn hình ảnh đầy đủ */ function addBaseUrlToImages(data, baseUrl) { - // baseUrl can be passed explicitly (e.g., from req), otherwise fall back to env - const BACKEND_URL = baseUrl || process.env.BACKEND_URL || ""; + // Use passed baseUrl, then env var, then default to localhost:3001 + const BACKEND_URL = (baseUrl || process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, ""); // Tạo bản sao sâu để tránh thay đổi dữ liệu gốc const processedData = JSON.parse(JSON.stringify(data)); @@ -14,16 +14,27 @@ function addBaseUrlToImages(data, baseUrl) { const processObject = (obj) => { if (!obj || typeof obj !== "object") return; - Object.keys(obj).forEach((key) => { - // Kiểm tra nếu thuộc tính chứa đường dẫn hình ảnh bắt đầu bằng /uploads/ - if (typeof obj[key] === "string" && obj[key].startsWith("/uploads/")) { - // Thêm BACKEND_URL nếu đường dẫn chưa có http - if (!obj[key].startsWith("http")) { - obj[key] = `${BACKEND_URL}${obj[key]}`; + if (Array.isArray(obj)) { + obj.forEach((item, index) => { + if (typeof item === "string" && (item.startsWith("/uploads/") || item.startsWith("/assets/img/"))) { + if (!item.startsWith("http")) { + obj[index] = `${BACKEND_URL}${item}`; + } + } else if (typeof item === "object") { + processObject(item); } - } else if (typeof obj[key] === "object") { - // Đệ quy xử lý các đối tượng và mảng lồng nhau - processObject(obj[key]); + }); + return; + } + + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === "string" && (value.startsWith("/uploads/") || value.startsWith("/assets/img/"))) { + if (!value.startsWith("http")) { + obj[key] = `${BACKEND_URL}${value}`; + } + } else if (value && typeof value === "object") { + processObject(value); } }); }; diff --git a/views/admin/about/index.ejs b/views/admin/about/index.ejs deleted file mode 100644 index e31282a..0000000 --- a/views/admin/about/index.ejs +++ /dev/null @@ -1,1024 +0,0 @@ -
Edit content displayed on About page
-