From befe6b30aaf4ffb582d3cf5733fdb40a9f1c33e7 Mon Sep 17 00:00:00 2001 From: Le Nhut Huy Date: Wed, 4 Feb 2026 10:06:22 +0700 Subject: [PATCH 1/2] feat(header): add admin UI and APIs for header management --- controllers/headerController.js | 660 +++++----- controllers/socialLinkController.js | 321 +++++ data/header.json | 158 ++- models/header.js | 226 ++-- public/uploads/header/black-logo.svg | 18 + public/uploads/header/white-logo.svg | 18 + routes/admin.js | 359 ++---- routes/index.js | 89 +- scripts/migrate-header.js | 73 ++ scripts/update-header-data.js | 106 ++ server.js | 159 ++- views/admin/header/index.ejs | 1794 +++++++++++++++----------- 12 files changed, 2359 insertions(+), 1622 deletions(-) create mode 100644 controllers/socialLinkController.js create mode 100644 public/uploads/header/black-logo.svg create mode 100644 public/uploads/header/white-logo.svg create mode 100644 scripts/migrate-header.js create mode 100644 scripts/update-header-data.js diff --git a/controllers/headerController.js b/controllers/headerController.js index c4e3183..80dc130 100644 --- a/controllers/headerController.js +++ b/controllers/headerController.js @@ -1,347 +1,335 @@ -const { addBaseUrlToImages } = require('../utils/imageHelper'); -const Header = require('../models/header'); -const Menu = require('../models/menuHeader'); +const Header = require("../models/header"); -// Helper function để thêm title và url cho programmes -const addProgrammeDetails = (programmes, menuUrl) => { - return programmes.map(prog => ({ - ...prog, - title: prog.name, - url: `${menuUrl}${prog.code}/` - })); -}; - -// Helper function để xử lý menu tree cho API (đơn giản hóa, map menuid thành id) -const processMenuTreeForAPI = (menuTree) => { - return menuTree.map(item => { - const processedItem = { - id: item.menuid, // Map menuid to id for frontend - title: item.title, - url: item.url, - order: item.order, - parent: item.parent || null, - type: item.type, - children: [] - }; - - // Đệ quy cho children - if (item.children && item.children.length > 0) { - processedItem.children = processMenuTreeForAPI(item.children); - } - - return processedItem; - }); -}; - -// Helper function để xử lý menu tree và thêm programme details (cho admin) -const processMenuTree = (menuTree) => { - return menuTree.map(item => { - const processedItem = { ...item }; - - // Nếu có programmes, thêm title và url - if (item.programmes && item.programmes.length > 0) { - processedItem.programmes = addProgrammeDetails(item.programmes, item.url); - } - - // Đệ quy cho children - if (item.children && item.children.length > 0) { - processedItem.children = processMenuTree(item.children); - } - - return processedItem; - }); -}; - -// Get header data from MongoDB -const getHeaderData = async () => { - const header = await Header.findOne({ name: 'default' }); - - if (!header) { - return { - topbar: { - contactInfo: { - phone: '', - email: '' - }, - links: [] - }, - mainMenu: [], - logo: '' - }; - } - - // Convert to plain object to allow modifications - const headerData = header.toObject(); - - // Lấy menu tree từ collection menuHeader (đơn giản, không có programmes) - try { - const menuTree = await Menu.getMenuTree(); - // Xử lý menu tree để map menuid thành id cho frontend - headerData.mainMenu = processMenuTreeForAPI(menuTree); - } catch (error) { - console.error('Error getting menu tree:', error); - headerData.mainMenu = []; - } - - return headerData; -}; - -// API to get header data -exports.api = async (req, res) => { - try { - // Lấy header data - const header = await getHeaderData(); - - // Xử lý URL cho logo và các hình ảnh khác - const processedData = addBaseUrlToImages(header); - - res.json(processedData); - } catch (err) { - console.error('API Error:', err); - res.status(500).json({ error: 'Error loading header data' }); - } -}; - -// API để lấy menu tree cho frontend (public API) -exports.getMenuTreeAPI = async (req, res) => { - try { - const menuTree = await Menu.getMenuTree(); - // Xử lý menu tree để map menuid thành id cho frontend - const processedMenuTree = processMenuTreeForAPI(menuTree); - res.json(processedMenuTree); - } catch (error) { - console.error('Error getting menu tree:', error); - res.status(500).json({ error: 'Error loading menu tree' }); - } -}; - -// API để lấy menu tree (cho admin) -exports.getMenuTree = async (req, res) => { - try { - const menuTree = await Menu.getMenuTree(); - res.json(menuTree); - } catch (error) { - console.error('Error getting menu tree:', error); - res.status(500).json({ error: 'Error loading menu tree' }); - } -}; - -// API để lấy programmes theo menu ID -exports.getProgrammesByMenuId = async (req, res) => { - try { - const { menuId } = req.params; - const programmes = await Header.getProgrammesByMenuId(menuId); - - // Lấy menu item để có URL - const menuItem = await Menu.findOne({ menuid: menuId }); - if (menuItem) { - const programmesWithDetails = addProgrammeDetails(programmes, menuItem.url); - res.json(programmesWithDetails); - } else { - res.json(programmes); - } - } catch (error) { - console.error('Error getting programmes by menu ID:', error); - res.status(500).json({ error: 'Error loading programmes' }); - } -}; - -// API để lấy toàn bộ header data -exports.getHeaderData = async (req, res) => { - try { - const headerData = await getHeaderData(); - res.json(headerData); - } catch (error) { - console.error('Error getting header data:', error); - res.status(500).json({ error: 'Error loading header data' }); - } -}; - -// API để lấy menu item theo ID -exports.getMenuItem = async (req, res) => { - try { - const { menuId } = req.params; - const Menu = require('../models/menuHeader'); - const menuItem = await Menu.findOne({ menuid: menuId }); - - if (!menuItem) { - return res.status(404).json({ error: 'Menu item not found' }); - } - - // Nếu là level type và có fetch = true, lấy programmes - if (menuItem.type === 'level' && menuItem.fetch) { - const programmes = await Menu.getProgrammesByMenuId(menuItem.menuid); - menuItem.programmes = addProgrammeDetails(programmes, menuItem.url); - } - - res.json(menuItem); - } catch (error) { - console.error('Error getting menu item:', error); - res.status(500).json({ error: 'Error loading menu item' }); - } -}; - -// Render admin view +// Admin: Render header management page exports.index = async (req, res) => { - try { - const data = await getHeaderData(); + try { + const header = await Header.findOne().sort({ order: 1 }); - res.render('admin/header/index', { - title: 'Header Management', - data - }); - } catch (error) { - console.error('Error in header index:', error); - req.flash('error_msg', 'An error occurred while loading the page'); - res.redirect('/admin/dashboard'); - } -}; - -// Update header (chỉ cập nhật topbar và logo) -exports.update = async (req, res) => { - try { - const headerData = req.body; - - // Tìm và cập nhật header - const header = await Header.findOne({ name: 'default' }); - if (!header) { - req.flash('error_msg', 'Header not found'); - return res.redirect('/admin/header'); - } - - // Cập nhật từng phần - if (headerData.topbarJson) { - header.topbar = JSON.parse(headerData.topbarJson); - } - - if (headerData.logo) { - header.logo = headerData.logo; - } - header.updatedAt = new Date(); - await header.save(); - - // Process menu updates if any - let menuUpdateCount = 0; - let menuErrorCount = 0; - - if (headerData.menuUpdates) { - try { - const updates = JSON.parse(headerData.menuUpdates); - - if (Array.isArray(updates) && updates.length > 0) { - const Menu = require('../models/menuHeader'); - - for (const update of updates) { - try { - const { menuid, title, order, type, parent, fetch, isActive } = update; - - const updateData = { - title: title, - order: order, - type: type, - parent: parent + // Prepare data for view + const data = header + ? { + topbar: { + contactInfo: { + phone: header.top?.phone || "", + email: header.top?.email || "", + location: header.top?.location || "", + }, + socialLinks: header.top?.socialLinks || [], + }, + logo: header.logo?.light || "", + } + : { + topbar: { + contactInfo: { + phone: "", + email: "", + location: "", + }, + socialLinks: [], + }, + logo: "", }; - // Add fetch field if provided - if (fetch !== undefined) { - updateData.fetch = fetch; - } - - // Add isActive field if provided - if (isActive !== undefined) { - updateData.isActive = isActive; - } - - const result = await Menu.findOneAndUpdate( - { menuid: menuid }, - updateData, - { new: true } - ); - - if (result) { - menuUpdateCount++; - } else { - console.warn(`Menu item not found for update: ${menuid}`); - menuErrorCount++; - } - } catch (innerErr) { - console.error(`Error updating menu item ${update.menuid}:`, innerErr); - menuErrorCount++; - } - } - } - } catch (err) { - console.error('Error processing menu updates:', err); - } + res.render("admin/header/index", { + layout: "layouts/main", + title: "Header Management", + user: req.session.user, + data: data, + }); + } catch (error) { + console.error("Error loading header management:", error); + res.status(500).render("page/error", { + title: "Error", + message: "Failed to load header management page", + }); } - - let flashMsg = 'Header updated successfully.'; - if (menuUpdateCount > 0) { - flashMsg += ` Updated ${menuUpdateCount} menu items.`; - } - if (menuErrorCount > 0) { - req.flash('error_msg', `Updated ${menuUpdateCount} items, but failed to update ${menuErrorCount} items. Check logs.`); - } else { - req.flash('success_msg', flashMsg); - } - - // Redirect back to the active tab - const activeTab = req.body.activeTab || 'topbar'; - res.redirect(`/admin/header?activeTab=${activeTab}`); - } catch (error) { - console.error('Error updating header:', error); - req.flash('error_msg', 'Error updating header: ' + error.message); - res.redirect('/admin/header'); - } }; -// Update menu structure (order and parent) -exports.updateMenu = async (req, res) => { - try { - const { updates } = req.body; - - if (!updates || !Array.isArray(updates)) { - return res.status(400).json({ - success: false, - error: 'Invalid updates data' - }); +// Admin: Get all headers (API) +exports.getAll = async (req, res) => { + try { + const headers = await Header.find().sort({ order: 1 }); + res.json({ + success: true, + data: headers, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); } +}; - const Menu = require('../models/menuHeader'); - - // Update each menu item - for (const update of updates) { - const { menuid, title, order, type, parent, fetch, isActive } = update; - - console.log(menuid, title, order, type, parent, fetch, isActive); - - const updateData = { - title: title, - order: order, - type: type, - parent: parent - }; - - // Add fetch field if provided (for level type menus) - if (fetch !== undefined) { - updateData.fetch = fetch; - } - - // Add isActive field if provided - if (isActive !== undefined) { - updateData.isActive = isActive; - } - - await Menu.findOneAndUpdate( - { menuid: menuid }, - updateData, - { new: true } - ); +// Admin: Get single header +exports.show = async (req, res) => { + try { + const header = await Header.findById(req.params.id); + if (!header) { + return res.status(404).json({ + success: false, + message: "Header not found", + }); + } + res.json({ + success: true, + data: header, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); } - res.json({ success: true }); - } catch (error) { - console.error('Error updating menu structure:', error); - req.flash('error_msg', 'Error updating menu structure: ' + error.message); - res.redirect('/admin/header'); - } -}; \ No newline at end of file +}; + +// Admin: Create header +exports.store = async (req, res) => { + try { + const { top, offcanvas, menu, logo, ctaButton, status, order } = req.body; + + const header = new Header({ + top, + offcanvas, + menu, + logo, + ctaButton, + status: status || "active", + order: order || 1, + }); + + await header.save(); + res.status(201).json({ + success: true, + message: "Header created successfully", + data: header, + }); + } catch (error) { + res.status(400).json({ + success: false, + message: error.message, + }); + } +}; + +// Admin: Update header +exports.update = async (req, res) => { + try { + let { top, topbarJson, offcanvas, menu, logo, ctaButton, status, order } = req.body; + + console.log("=== UPDATE REQUEST RECEIVED ==="); + console.log("Raw body:", JSON.stringify(req.body, null, 2)); + console.log("topbarJson type:", typeof topbarJson); + console.log("topbarJson value:", topbarJson); + + // Nếu có topbarJson, parse nó + if (topbarJson && typeof topbarJson === "string") { + try { + const parsedData = JSON.parse(topbarJson); + console.log("✓ Parsed topbarJson successfully:", parsedData); + // Chuyển đổi từ topbarData sang top format + top = { + phone: parsedData.contactInfo?.phone || "", + email: parsedData.contactInfo?.email || "", + location: parsedData.contactInfo?.location || "", + socialLinks: parsedData.socialLinks || [], + }; + console.log("✓ Converted to top object:", top); + } catch (e) { + console.error("✗ Error parsing topbarJson:", e.message); + return res.status(400).json({ + success: false, + message: "Invalid JSON in topbarJson: " + e.message, + }); + } + } + + // Nếu không có id, tìm header đầu tiên hoặc tạo mới + let headerId = req.params.id; + + if (!headerId) { + // Tìm header đầu tiên + let header = await Header.findOne().sort({ order: 1 }); + if (!header) { + console.log("No existing header found, creating new one"); + // Tạo header mới nếu chưa có + header = new Header({ + top, + offcanvas, + menu, + logo: logo ? { light: logo } : {}, + ctaButton, + status: status || "active", + order: order || 1, + }); + await header.save(); + console.log("✓ Header created:", header._id); + return res.json({ + success: true, + message: "Header created successfully", + data: header, + }); + } + headerId = header._id; + console.log("✓ Found existing header:", headerId); + } + + // Chuẩn bị dữ liệu logo - merge với dữ liệu cũ + let logoData = {}; + if (logo) { + // Nếu có logo mới, lấy dữ liệu cũ và update light + const existingHeader = await Header.findById(headerId); + logoData = { + light: logo, + dark: existingHeader?.logo?.dark || "", + alt: existingHeader?.logo?.alt || "", + }; + } + + const updateData = { + top, + offcanvas, + menu, + ctaButton, + status, + order, + }; + + if (logo) { + updateData.logo = logoData; + } + + console.log("Preparing to update header with data:", JSON.stringify(updateData, null, 2)); + + const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, { new: true, runValidators: true }); + + if (!updatedHeader) { + console.error("✗ Header not found with ID:", headerId); + return res.status(404).json({ + success: false, + message: "Header not found", + }); + } + + console.log("✓ Header updated successfully:", updatedHeader._id); + console.log("Updated header data:", JSON.stringify(updatedHeader, null, 2)); + + res.json({ + success: true, + message: "Header updated successfully", + data: updatedHeader, + }); + } catch (error) { + console.error("✗ Error updating header:", error); + res.status(400).json({ + success: false, + message: error.message, + }); + } +}; + +// Admin: Update status +exports.updateStatus = async (req, res) => { + try { + const { status } = req.body; + + if (!["active", "inactive"].includes(status)) { + return res.status(400).json({ + success: false, + message: "Invalid status", + }); + } + + const header = await Header.findByIdAndUpdate(req.params.id, { status }, { new: true }); + + if (!header) { + return res.status(404).json({ + success: false, + message: "Header not found", + }); + } + + res.json({ + success: true, + message: "Header status updated", + data: header, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// Admin: Delete header +exports.destroy = async (req, res) => { + try { + const header = await Header.findByIdAndDelete(req.params.id); + + if (!header) { + return res.status(404).json({ + success: false, + message: "Header not found", + }); + } + + res.json({ + success: true, + message: "Header deleted successfully", + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// Public API: Get active header +exports.api = async (req, res) => { + try { + const header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + res.json({ + success: true, + data: header, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// Public API: Get menu tree structure +exports.getMenuTreeAPI = async (req, res) => { + try { + const header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header || !header.menu) { + return res.status(404).json({ + success: false, + message: "No active menu found", + }); + } + + res.json({ + success: true, + data: header.menu, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; diff --git a/controllers/socialLinkController.js b/controllers/socialLinkController.js new file mode 100644 index 0000000..8c71fae --- /dev/null +++ b/controllers/socialLinkController.js @@ -0,0 +1,321 @@ +const Header = require("../models/header"); + +// Get all social links +exports.getAll = async (req, res) => { + try { + const header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + res.json({ + success: true, + data: header.top?.socialLinks || [], + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// Get single social link by platform +exports.show = async (req, res) => { + try { + const { platform } = req.params; + const header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + const socialLink = header.top?.socialLinks?.find((link) => link.platform === platform); + + if (!socialLink) { + return res.status(404).json({ + success: false, + message: "Social link not found", + }); + } + + res.json({ + success: true, + data: socialLink, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// Create social link +exports.store = async (req, res) => { + try { + let { platform, url, icon } = req.body; + + // Convert platform to lowercase + platform = platform.toLowerCase().trim(); + url = url.trim(); + icon = icon ? icon.trim() : null; + + console.log("Creating social link:", { platform, url, icon }); + + // Validate required fields + if (!platform || !url) { + console.log("Validation failed: platform or url missing"); + return res.status(400).json({ + success: false, + message: "Platform and URL are required", + }); + } + + // Validate platform is in enum + const validPlatforms = ["linkedin", "twitter", "instagram", "youtube", "facebook"]; + if (!validPlatforms.includes(platform)) { + console.log("Invalid platform:", platform); + return res.status(400).json({ + success: false, + message: `Invalid platform. Must be one of: ${validPlatforms.join(", ")}`, + }); + } + + // Find header + let header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + console.log("No active header found"); + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + console.log("Found header:", header._id); + + // Check if platform already exists + const existingLink = header.top?.socialLinks?.find((link) => link.platform === platform); + + if (existingLink) { + console.log("Platform already exists:", platform); + return res.status(400).json({ + success: false, + message: `Social link for ${platform} already exists`, + }); + } + + // Add new social link + if (!header.top) { + header.top = {}; + } + if (!header.top.socialLinks) { + header.top.socialLinks = []; + } + + // Calculate next order number + const maxOrder = + header.top.socialLinks.length > 0 ? Math.max(...header.top.socialLinks.map((link) => link.order || 0)) : 0; + + header.top.socialLinks.push({ + platform, + url, + icon: icon || `fa-brands fa-${platform}`, + order: maxOrder + 1, + }); + + console.log("Saving header with new social link"); + await header.save(); + + console.log("Social link created successfully"); + res.status(201).json({ + success: true, + message: "Social link created successfully", + data: header.top.socialLinks[header.top.socialLinks.length - 1], + }); + } catch (error) { + console.error("Error creating social link:", error); + res.status(400).json({ + success: false, + message: error.message, + }); + } +}; + +// Update social link +exports.update = async (req, res) => { + try { + let { platform } = req.params; + let { url, icon } = req.body; + + // Convert to lowercase + platform = platform.toLowerCase().trim(); + url = url.trim(); + icon = icon ? icon.trim() : null; + + // Validate required fields + if (!url) { + return res.status(400).json({ + success: false, + message: "URL is required", + }); + } + + // Find header + const header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + // Find and update social link + const socialLink = header.top?.socialLinks?.find((link) => link.platform === platform); + + if (!socialLink) { + return res.status(404).json({ + success: false, + message: "Social link not found", + }); + } + + socialLink.url = url; + if (icon) { + socialLink.icon = icon; + } + + await header.save(); + + res.json({ + success: true, + message: "Social link updated successfully", + data: socialLink, + }); + } catch (error) { + res.status(400).json({ + success: false, + message: error.message, + }); + } +}; + +// Delete social link +exports.destroy = async (req, res) => { + try { + let { platform } = req.params; + + // Convert to lowercase + platform = platform.toLowerCase().trim(); + + console.log("Deleting social link:", platform); + + // Find header + const header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + console.log("No active header found"); + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + // Find and remove social link + const index = header.top?.socialLinks?.findIndex((link) => link.platform === platform); + + if (index === -1 || index === undefined) { + console.log("Social link not found:", platform); + return res.status(404).json({ + success: false, + message: "Social link not found", + }); + } + + const deletedLink = header.top.socialLinks.splice(index, 1); + + console.log("Saving header after delete"); + await header.save(); + + console.log("Social link deleted successfully"); + res.json({ + success: true, + message: "Social link deleted successfully", + data: deletedLink[0], + }); + } catch (error) { + console.error("Error deleting social link:", error); + res.status(500).json({ + success: false, + message: error.message, + }); + } +}; + +// Bulk update social links (used for reordering and batch updates) +exports.bulkUpdate = async (req, res) => { + try { + const { socialLinks } = req.body; + + if (!Array.isArray(socialLinks)) { + return res.status(400).json({ + success: false, + message: "socialLinks must be an array", + }); + } + + // Find header + let header = await Header.findOne({ status: "active" }).sort({ order: 1 }); + + if (!header) { + return res.status(404).json({ + success: false, + message: "No active header found", + }); + } + + // Validate all social links + for (const link of socialLinks) { + if (!link.platform || !link.url) { + return res.status(400).json({ + success: false, + message: "Each social link must have platform and url", + }); + } + } + + // Update social links with order field + if (!header.top) { + header.top = {}; + } + + header.top.socialLinks = socialLinks.map((link, index) => ({ + platform: link.platform, + url: link.url, + icon: link.icon || `fa-brands fa-${link.platform}`, + order: link.order || index + 1, // Use provided order or calculate from index + })); + + await header.save(); + + res.json({ + success: true, + message: "Social links updated successfully", + data: header.top.socialLinks, + }); + } catch (error) { + res.status(400).json({ + success: false, + message: error.message, + }); + } +}; diff --git a/data/header.json b/data/header.json index 4c09c54..021e6d1 100644 --- a/data/header.json +++ b/data/header.json @@ -1,24 +1,142 @@ { - "topbar": { - "contactInfo": { - "phone": "+(123)-456-789", - "email": "info@ggcamp.com" + "top": { + "phone": "+09 378 357 5222", + "email": "info@hailearning.edu.vn", + "location": "69 Street, 5th Avenue LA, United States", + "socialLinks": [ + { + "platform": "linkedin", + "url": "https://linkedin.com", + "icon": "fa-brands fa-linkedin" + }, + { + "platform": "twitter", + "url": "https://twitter.com", + "icon": "fa-brands fa-twitter" + }, + { + "platform": "instagram", + "url": "https://instagram.com", + "icon": "fa-brands fa-instagram" + }, + { + "platform": "youtube", + "url": "https://youtube.com", + "icon": "fa-brands fa-youtube" + } + ], + "languages": [ + { + "name": "English", + "value": "1" + }, + { + "name": "Bangla", + "value": "2" + }, + { + "name": "Hindi", + "value": "3" + } + ] }, - "links": [ - { - "text": "Instagram", - "url": "https://instagram.com" - }, - { - "text": "Facebook", - "url": "https://facebook.com" - }, - { - "text": "Dribbble", - "url": "https://dribbble.com" - } + "offcanvas": { + "description": "Nullam dignissim, ante scelerisque the is euismod fermentum odio sem semper the is erat, a feugiat leo urna eget eros. Duis Aenean a imperdiet risus.", + "contactInfo": { + "address": "Main Street, Melbourne, Australia", + "email": "info@hailearning.edu.vn", + "workingHours": "Mod-Friday, 09am - 05pm", + "phone": "+09 378 357 5222" + } + }, + "menu": [ + { + "label": "Home", + "href": "/", + "children": [] + }, + { + "label": "About Us", + "href": "/about", + "children": [] + }, + { + "label": "Pages", + "href": "#", + "children": [ + { + "label": "Service", + "href": "/service", + "children": [ + { + "label": "Service", + "href": "/service" + }, + { + "label": "Service Details", + "href": "/service-details" + } + ] + }, + { + "label": "Country List", + "href": "/country-list", + "children": [ + { + "label": "Country List", + "href": "/country-list" + }, + { + "label": "Country Details", + "href": "/country-details" + } + ] + }, + { + "label": "Our Pricing", + "href": "/pricing" + }, + { + "label": "Appointment", + "href": "/appointment" + } + ] + }, + { + "label": "VISA", + "href": "#", + "children": [ + { + "label": "Visa List", + "href": "/visa-list" + }, + { + "label": "Visa Details", + "href": "/visa-details" + } + ] + }, + { + "label": "Blog", + "href": "#", + "children": [ + { + "label": "Blog Grid", + "href": "/blog-grid" + }, + { + "label": "Blog Standard", + "href": "/blog" + }, + { + "label": "Blog Details", + "href": "/blog-details" + } + ] + }, + { + "label": "Contact Us", + "href": "/contact" + } ] - }, - "logo": "/templates/yootheme/cache/c9/logo-camp-adventure-c9850ee6.png" } - diff --git a/models/header.js b/models/header.js index 740a2c3..2c03f58 100644 --- a/models/header.js +++ b/models/header.js @@ -1,129 +1,115 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); -// Schema cho các link trong topbar -const topbarLinkSchema = new mongoose.Schema({ - text: { - type: String, - required: true, - trim: true - }, - url: { - type: String, - required: true, - trim: true - } -}, { _id: false }); +const socialLinkSchema = new mongoose.Schema( + { + platform: { + type: String, + required: true, + enum: ["linkedin", "twitter", "instagram", "youtube", "facebook"], + }, + url: { + type: String, + required: true, + }, + icon: String, + order: { + type: Number, + default: 0, + }, + }, + { _id: false }, +); -// Schema cho contact info trong topbar -const contactInfoSchema = new mongoose.Schema({ - phone: { - type: String, - required: true, - trim: true - }, - email: { - type: String, - required: true, - trim: true - } -}, { _id: false }); +const languageSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + }, + value: { + type: String, + required: true, + }, + }, + { _id: false }, +); -// Schema cho topbar -const topbarSchema = new mongoose.Schema({ - contactInfo: { - type: contactInfoSchema, - required: true - }, - links: { - type: [topbarLinkSchema], - default: [] - } -}, { _id: false }); +const menuItemSchema = new mongoose.Schema( + { + label: { + type: String, + required: true, + }, + href: { + type: String, + required: true, + }, + icon: String, + order: { + type: Number, + default: 0, + }, + children: [this], + }, + { _id: false }, +); -// Main Header Schema -const headerSchema = new mongoose.Schema({ - name: { - type: String, - default: 'default', - unique: true - }, - topbar: { - type: topbarSchema, - required: true - }, - logo: { - type: String, - required: true, - trim: true - } -}, { - timestamps: true -}); +const headerSchema = new mongoose.Schema( + { + // Top bar + top: { + phone: String, + email: String, + location: String, + socialLinks: [socialLinkSchema], + languages: [languageSchema], + }, -// Method để lấy menu tree từ collection menuHeader -headerSchema.statics.getMenuTree = async function() { - try { - const Menu = require('./menuHeader'); - return await Menu.getMenuTree(); - } catch (error) { - console.error('Error getting menu tree:', error); - throw error; - } -}; + // Offcanvas + offcanvas: { + description: String, + contactInfo: { + address: String, + email: String, + workingHours: String, + phone: String, + }, + }, -// Method để lấy menu tree với programmes từ collection menuHeader -headerSchema.statics.getMenuTreeWithProgrammes = async function() { - try { - const Menu = require('./menuHeader'); - return await Menu.getMenuTreeWithProgrammes(); - } catch (error) { - console.error('Error getting menu tree with programmes:', error); - throw error; - } -}; + // Menu + menu: [menuItemSchema], -// Method để lấy programmes theo menu ID -headerSchema.statics.getProgrammesByMenuId = async function(menuId) { - try { - const Menu = require('./menuHeader'); - return await Menu.getProgrammesByMenuId(menuId); - } catch (error) { - console.error('Error getting programmes by menu ID:', error); - throw error; - } -}; + // Logo + logo: { + light: String, + dark: String, + alt: String, + }, -// Tạo migration script để import dữ liệu từ menu.json -headerSchema.statics.migrateFromJson = async function(jsonData) { - try { - // Kiểm tra xem đã có header mặc định chưa - const existingHeader = await this.findOne({ name: 'default' }); - - // Chỉ giữ lại topbar và logo, bỏ search và mainMenu - const headerData = { - topbar: jsonData.topbar, - logo: jsonData.logo - }; - - if (existingHeader) { - // Cập nhật header hiện có - Object.assign(existingHeader, headerData); - await existingHeader.save(); - console.log('Header data updated successfully'); - return existingHeader; - } else { - // Tạo header mới với dữ liệu từ JSON - const newHeader = await this.create({ - name: 'default', - ...headerData - }); - console.log('Header data imported successfully'); - return newHeader; - } - } catch (error) { - console.error('Error migrating header data:', error); - throw error; - } -}; + // CTA Button + ctaButton: { + label: String, + href: String, + style: { + type: String, + enum: ["primary", "secondary", "outline"], + default: "primary", + }, + }, -module.exports = mongoose.model('Header', headerSchema); \ No newline at end of file + // Status + status: { + type: String, + enum: ["active", "inactive"], + default: "active", + }, + + order: { + type: Number, + default: 1, + }, + }, + { timestamps: true }, +); + +module.exports = mongoose.model("Header", headerSchema); diff --git a/public/uploads/header/black-logo.svg b/public/uploads/header/black-logo.svg new file mode 100644 index 0000000..ec35168 --- /dev/null +++ b/public/uploads/header/black-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/uploads/header/white-logo.svg b/public/uploads/header/white-logo.svg new file mode 100644 index 0000000..486d3ee --- /dev/null +++ b/public/uploads/header/white-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/routes/admin.js b/routes/admin.js index d5233f5..0e412ce 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -39,8 +39,8 @@ router.post("/home/update", ensureAuthenticated, homeController.update); // 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 @@ -49,99 +49,42 @@ 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"), - // convertToWebp, // Disabled to keep original image format (JPG/PNG) - 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.post( - "/header/update-menu", - ensureAuthenticated, - headerController.updateMenu, -); -router.get( - "/header/menu-tree", - ensureAuthenticated, - headerController.getMenuTree, -); -router.get( - "/header/programmes/:menuId", - ensureAuthenticated, - headerController.getProgrammesByMenuId, -); -router.get( - "/header/menu-item/:menuId", - ensureAuthenticated, - headerController.getMenuItem, -); +router.post("/header/update-menu", ensureAuthenticated, headerController.updateMenu); +router.get("/header/menu-tree", ensureAuthenticated, headerController.getMenuTree); +router.get("/header/programmes/:menuId", ensureAuthenticated, headerController.getProgrammesByMenuId); +router.get("/header/menu-item/:menuId", ensureAuthenticated, headerController.getMenuItem); router.get("/header/data", ensureAuthenticated, headerController.getHeaderData); +router.patch("/header/:id/status", ensureAuthenticated, headerController.updateStatus); +router.delete("/header/:id", ensureAuthenticated, headerController.destroy); // Footer routes router.get("/footer", ensureAuthenticated, footerController.index); @@ -151,160 +94,60 @@ 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); @@ -413,65 +256,61 @@ router.post( // 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"); + const fs = require("fs"); + const path = require("path"); + const campLocationData = require("../data/camp-location.json"); - // Collect all image paths - const imagePaths = []; + // 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)), + // 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, }); - } - - // 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 diff --git a/routes/index.js b/routes/index.js index 9e941b9..4f02150 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,6 +5,7 @@ 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"); const footerController = require("../controllers/footerController"); const contactController = require("../controllers/contactController"); const faqController = require("../controllers/faqController"); @@ -27,10 +28,10 @@ const blogTagController = require("../controllers/blogTagController"); // Trang chủ router.get("/", (req, res) => { - res.render("index", { - title: "Welcome", - layout: "layouts/main", - }); + res.render("index", { + title: "Welcome", + layout: "layouts/main", + }); }); // API để lấy dữ liệu trang chủ @@ -50,6 +51,14 @@ router.get("/api/header", headerController.api); // Menu Tree API route (for frontend) router.get("/api/menu-tree", headerController.getMenuTreeAPI); +// Social Links API routes +router.get("/api/social-links", socialLinkController.getAll); +router.get("/api/social-links/:platform", socialLinkController.show); +router.post("/api/social-links", socialLinkController.store); +router.put("/api/social-links/:platform", socialLinkController.update); +router.delete("/api/social-links/:platform", socialLinkController.destroy); +router.post("/api/social-links/bulk-update", socialLinkController.bulkUpdate); + // Footer API route router.get("/api/footer", footerController.api); @@ -85,69 +94,57 @@ router.get("/api/terms", termsController.api); // Travel public page and API router.get("/travel", async (req, res) => { - try { - const Travel = require("../models/travel"); - const travel = await Travel.findOne(); + try { + const Travel = require("../models/travel"); + const travel = await Travel.findOne(); - if (!travel) { - return res.status(404).render("errors/404", { - title: "Page Not Found", - message: "Travel information not found", - }); + if (!travel) { + return res.status(404).render("errors/404", { + title: "Page Not Found", + message: "Travel information not found", + }); + } + + res.render("page/travel", { + title: travel.page.title, + data: travel.toObject(), + }); + } catch (error) { + console.error("Error loading travel page:", error); + res.status(500).render("errors/500", { + title: "Server Error", + message: "Error loading travel page", + }); } - - res.render("page/travel", { - title: travel.page.title, - data: travel.toObject(), - }); - } catch (error) { - console.error("Error loading travel page:", error); - res.status(500).render("errors/500", { - title: "Server Error", - message: "Error loading travel page", - }); - } }); router.get("/api/travel", travelController.api); // Booking submission APIs (public endpoints) router.post("/api/booking/submit", bookingSubmissionController.submitBooking); +router.get("/api/activity/:activityId/sessions", bookingSubmissionController.getAvailableSessions); router.get( - "/api/activity/:activityId/sessions", - bookingSubmissionController.getAvailableSessions, -); -router.get( - "/api/activity/:activityId/session/:sessionId/availability", - bookingSubmissionController.getSessionAvailability, + "/api/activity/:activityId/session/:sessionId/availability", + bookingSubmissionController.getSessionAvailability, ); // New API for creating bookings directly into camp sessions (by program) -router.post( - "/api/camps/:program/sessions/:sessionId/bookings", - activityController.createSessionBookingByProgram, -); -router.get( - "/api/camps/:program/sessions/:sessionId/bookings", - activityController.getSessionBookingsByProgram, -); +router.post("/api/camps/:program/sessions/:sessionId/bookings", activityController.createSessionBookingByProgram); +router.get("/api/camps/:program/sessions/:sessionId/bookings", activityController.getSessionBookingsByProgram); // Keep admin-style update/delete by activityId (protected) if needed -router.put( - "/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", - activityController.updateSessionBooking, -); +router.put("/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", activityController.updateSessionBooking); router.delete( - "/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", - activityController.deleteSessionBooking, + "/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", + activityController.deleteSessionBooking, ); // Demo booking form router.get("/demo/booking-form", (req, res) => { - res.sendFile(path.join(__dirname, "../views/demo/booking-form.html")); + res.sendFile(path.join(__dirname, "../views/demo/booking-form.html")); }); // Demo session booking API router.get("/demo/session-booking-api", (req, res) => { - res.sendFile(path.join(__dirname, "../views/demo/session-booking-api.html")); + res.sendFile(path.join(__dirname, "../views/demo/session-booking-api.html")); }); // Blog API Routes diff --git a/scripts/migrate-header.js b/scripts/migrate-header.js new file mode 100644 index 0000000..05b3dc7 --- /dev/null +++ b/scripts/migrate-header.js @@ -0,0 +1,73 @@ +const mongoose = require("mongoose"); +const path = require("path"); +require("dotenv").config({ path: path.join(__dirname, "../.env") }); + +const Header = require("../models/header"); +const headerData = require("../data/header.json"); + +const migrateHeader = async () => { + try { + const mongoUri = process.env.MONGODB_URI; + if (!mongoUri) { + throw new Error("MONGODB_URI not found in environment variables"); + } + await mongoose.connect(mongoUri); + console.log("Connected to MongoDB"); + + // Delete existing header + await Header.deleteMany({}); + console.log("Cleared existing headers"); + + // 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) => ({ + ...link, + order: idx, + })), + languages: headerData.top.languages, + }, + offcanvas: headerData.offcanvas, + menu: headerData.menu.map((item, idx) => ({ + ...item, + order: idx, + children: + item.children?.map((child, childIdx) => ({ + ...child, + order: childIdx, + children: + child.children?.map((subchild, subIdx) => ({ + ...subchild, + order: subIdx, + })) || [], + })) || [], + })), + logo: { + light: "/assets/img/logo/white-logo.svg", + dark: "/assets/img/logo/black-logo.svg", + alt: "Hai Learning", + }, + ctaButton: { + label: "Get Started", + href: "/contact", + style: "primary", + }, + status: "active", + order: 1, + }; + + const result = await Header.create(headerDocument); + console.log("Header migrated successfully:", result._id); + + await mongoose.connection.close(); + process.exit(0); + } catch (error) { + console.error("Migration error:", error); + process.exit(1); + } +}; + +migrateHeader(); diff --git a/scripts/update-header-data.js b/scripts/update-header-data.js new file mode 100644 index 0000000..0607352 --- /dev/null +++ b/scripts/update-header-data.js @@ -0,0 +1,106 @@ +const mongoose = require("mongoose"); +const path = require("path"); +require("dotenv").config({ path: path.join(__dirname, "../.env") }); + +const Header = require("../models/header"); + +async function updateHeaderData() { + try { + // Connect to MongoDB + await mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/hailearning"); + console.log("Connected to MongoDB"); + + // Find the first header + let header = await Header.findOne().sort({ order: 1 }); + + if (!header) { + console.log("No header found, creating new one..."); + header = new Header({ + top: { + phone: "+09 378 357 5222", + email: "info@hailearning.edu.vn", + location: "69 Street, 5th Avenue LA, United States", + socialLinks: [ + { + platform: "linkedin", + url: "https://linkedin.com", + icon: "fa-brands fa-linkedin", + }, + { + platform: "twitter", + url: "https://twitter.com", + icon: "fa-brands fa-twitter", + }, + { + platform: "instagram", + url: "https://instagram.com", + icon: "fa-brands fa-instagram", + }, + { + platform: "youtube", + url: "https://youtube.com", + icon: "fa-brands fa-youtube", + }, + ], + languages: [ + { name: "English", value: "1" }, + { name: "Bangla", value: "2" }, + { name: "Hindi", value: "3" }, + ], + }, + status: "active", + order: 1, + }); + } else { + console.log("Header found, updating..."); + // Update existing header + header.top = { + phone: header.top?.phone || "+09 378 357 5222", + email: header.top?.email || "info@hailearning.edu.vn", + location: header.top?.location || "69 Street, 5th Avenue LA, United States", + socialLinks: + header.top?.socialLinks?.length > 0 + ? header.top.socialLinks + : [ + { + platform: "linkedin", + url: "https://linkedin.com", + icon: "fa-brands fa-linkedin", + }, + { + platform: "twitter", + url: "https://twitter.com", + icon: "fa-brands fa-twitter", + }, + { + platform: "instagram", + url: "https://instagram.com", + icon: "fa-brands fa-instagram", + }, + { + platform: "youtube", + url: "https://youtube.com", + icon: "fa-brands fa-youtube", + }, + ], + languages: header.top?.languages || [ + { name: "English", value: "1" }, + { name: "Bangla", value: "2" }, + { name: "Hindi", value: "3" }, + ], + }; + } + + await header.save(); + console.log("Header updated successfully!"); + console.log("Header data:", JSON.stringify(header, null, 2)); + + await mongoose.connection.close(); + console.log("Database connection closed"); + } catch (error) { + console.error("Error updating header:", error); + process.exit(1); + } +} + +updateHeaderData(); diff --git a/server.js b/server.js index 924d8f0..760db23 100644 --- a/server.js +++ b/server.js @@ -34,24 +34,26 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); app.use( - "/assets", - (req, res, next) => { - // Cho phép mọi domain truy cập tài nguyên tĩnh - res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Methods", "GET"); - next(); - }, - express.static(path.join(__dirname, "assets")), + "/assets", + (req, res, next) => { + // Cho phép mọi domain truy cập tài nguyên tĩnh + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "GET"); + next(); + }, + express.static(path.join(__dirname, "assets")), ); + +// Serve uploads folder app.use( - "/uploads", - (req, res, next) => { - // Cho phép mọi domain truy cập tài nguyên tĩnh - res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Methods", "GET"); - next(); - }, - express.static(path.join(__dirname, "public", "uploads")), + "/uploads", + (req, res, next) => { + // Cho phép mọi domain truy cập tài nguyên tĩnh + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "GET"); + next(); + }, + express.static(path.join(__dirname, "public", "uploads")), ); // Serve other public files @@ -59,21 +61,21 @@ app.use(express.static(path.join(__dirname, "public"))); // Session configuration (using MongoDB store to avoid logout khi server restart) app.use( - session({ - secret: process.env.SESSION_SECRET || "secret", - resave: false, - saveUninitialized: false, - store: MongoStore.create({ - mongoUrl: process.env.MONGODB_URI, - collectionName: "sessions", - ttl: 60 * 60 * 24, // 24 hours (in seconds) + session({ + secret: process.env.SESSION_SECRET || "secret", + resave: false, + saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGODB_URI, + collectionName: "sessions", + ttl: 60 * 60 * 24, // 24 hours (in seconds) + }), + cookie: { + maxAge: 1000 * 60 * 60 * 24, // 24 hours + httpOnly: true, + sameSite: "lax", + }, }), - cookie: { - maxAge: 1000 * 60 * 60 * 24, // 24 hours - httpOnly: true, - sameSite: "lax", - }, - }), ); // Flash messages @@ -81,32 +83,32 @@ app.use(flash()); // Global variables app.use((req, res, next) => { - // Lấy flash messages - const success_msg = req.flash("success_msg"); - const error_msg = req.flash("error_msg"); - const error = req.flash("error"); + // Lấy flash messages + const success_msg = req.flash("success_msg"); + const error_msg = req.flash("error_msg"); + const error = req.flash("error"); - // Lưu vào res.locals để sử dụng trong views - res.locals.success_msg = success_msg.length > 0 ? success_msg[0] : null; - res.locals.error_msg = error_msg.length > 0 ? error_msg[0] : null; - res.locals.error = error.length > 0 ? error[0] : null; + // Lưu vào res.locals để sử dụng trong views + res.locals.success_msg = success_msg.length > 0 ? success_msg[0] : null; + res.locals.error_msg = error_msg.length > 0 ? error_msg[0] : null; + res.locals.error = error.length > 0 ? error[0] : null; - // Tạo object flashMessages để sử dụng trong client-side JavaScript - res.locals.flashMessagesJSON = JSON.stringify({ - success_msg: res.locals.success_msg, - error_msg: res.locals.error_msg, - error: res.locals.error, - }); + // Tạo object flashMessages để sử dụng trong client-side JavaScript + res.locals.flashMessagesJSON = JSON.stringify({ + success_msg: res.locals.success_msg, + error_msg: res.locals.error_msg, + error: res.locals.error, + }); - res.locals.user = req.session.user || null; - res.locals.currentPath = req.path; - next(); + res.locals.user = req.session.user || null; + res.locals.currentPath = req.path; + next(); }); // Kiểm tra và tạo thư mục data nếu chưa tồn tại const dataDir = path.join(__dirname, "data"); if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir); + fs.mkdirSync(dataDir); } // Frontend URL configuration @@ -114,39 +116,30 @@ const FRONTEND_URL = process.env.FRONTEND_URL; // Add frontend URL to res.locals for all requests app.use((req, res, next) => { - res.locals.frontendUrl = FRONTEND_URL; - res.locals.currentPath = req.path; - next(); + res.locals.frontendUrl = FRONTEND_URL; + res.locals.currentPath = req.path; + next(); }); // Simple CORS middleware for API endpoints app.use((req, res, next) => { - // Allow requests from configured FRONTEND_URL or allow all if not set - const origin = req.headers.origin; - const allowedOrigin = FRONTEND_URL || "*"; + // Allow requests from configured FRONTEND_URL or allow all if not set + const origin = req.headers.origin; + const allowedOrigin = FRONTEND_URL || "*"; - if (allowedOrigin === "*" || origin === allowedOrigin) { - res.setHeader( - "Access-Control-Allow-Origin", - allowedOrigin === "*" ? "*" : origin, - ); - res.setHeader( - "Access-Control-Allow-Methods", - "GET,POST,PUT,DELETE,OPTIONS", - ); - res.setHeader( - "Access-Control-Allow-Headers", - "Content-Type, Authorization", - ); - res.setHeader("Access-Control-Allow-Credentials", "true"); - } + if (allowedOrigin === "*" || origin === allowedOrigin) { + res.setHeader("Access-Control-Allow-Origin", allowedOrigin === "*" ? "*" : origin); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.setHeader("Access-Control-Allow-Credentials", "true"); + } - // Handle preflight - if (req.method === "OPTIONS") { - return res.sendStatus(204); - } + // Handle preflight + if (req.method === "OPTIONS") { + return res.sendStatus(204); + } - next(); + next(); }); // Routes @@ -160,19 +153,19 @@ app.use("/", indexRoutes); // 404 handler app.use((req, res) => { - res.status(404); - if (req.accepts("html")) - return res.render("page/404", { - title: "404 - Page Not Found", - layout: "layouts/main", - }); - if (req.accepts("json")) return res.json({ error: "Not found" }); - res.type("txt").send("Not found"); + res.status(404); + if (req.accepts("html")) + return res.render("page/404", { + title: "404 - Page Not Found", + layout: "layouts/main", + }); + if (req.accepts("json")) return res.json({ error: "Not found" }); + res.type("txt").send("Not found"); }); // Start server const PORT = process.env.PORT || 3001; const HOST = process.env.HOST || "localhost"; app.listen(PORT, HOST, () => { - console.log(`🚀 SERVER:[ http://${HOST}:${PORT} ]`); + console.log(`🚀 SERVER:[ http://${HOST}:${PORT} ]`); }); diff --git a/views/admin/header/index.ejs b/views/admin/header/index.ejs index 6ca7979..69378e5 100644 --- a/views/admin/header/index.ejs +++ b/views/admin/header/index.ejs @@ -1,36 +1,58 @@
-

Header Management

+

+ Header Management +

Edit header content and menu structure

-
+ - - - - + + + +