diff --git a/controllers/footerController.js b/controllers/footerController.js index 6387224..d6fda12 100644 --- a/controllers/footerController.js +++ b/controllers/footerController.js @@ -1,178 +1,143 @@ const { addBaseUrlToImages } = require("../utils/imageHelper"); const Footer = require("../models/footer"); -// Get footer data from MongoDB -const getFooterData = async () => { - const footer = await Footer.findOne({ name: "default" }); +// GET /api/footer - Public API cho website và CMS load dữ liệu +exports.getFooter = async (req, res) => { + try { + const footer = await Footer.getSingle(); + const processedData = addBaseUrlToImages(footer.toObject()); - if (!footer) { - return { - logo: { - src: '', - alt: '' - }, - about: { - title: "About GGC", - description: "", - mapLink: { - text: "Check on google map", - url: "", - }, - }, - address: { - text: "", - address2: "", - mapUrl: "", - }, - contact: { - phone: "", - hours: "", - email: "", - }, - columns: [], - social: { - links: [], - }, - copyright: { - text: "", - }, - }; - } - - return footer.toObject(); + res.json(processedData); + } catch (error) { + console.error("Error getting footer:", error); + res.status(500).json({ + error: "Failed to get footer data", + }); + } }; -// API to get footer data -exports.api = async (req, res) => { - try { - // Lấy footer data - const footer = await getFooterData(); +// PUT /api/admin/footer - Update toàn bộ footer cho CMS +exports.updateFooter = async (req, res) => { + try { + let updateData = req.body; - // Xử lý URL cho logo và các hình ảnh khác - const processedData = addBaseUrlToImages(footer); + console.log("=== FOOTER UPDATE REQUEST RECEIVED ==="); + console.log("Raw body:", JSON.stringify(req.body, null, 2)); - res.json(processedData); - } catch (err) { - console.error("API Error:", err); - res.status(500).json({ error: "Error loading footer data" }); - } + // Nếu có footerJson, parse nó (tương tự Header logic) + if (updateData.footerJson && typeof updateData.footerJson === "string") { + try { + const parsedData = JSON.parse(updateData.footerJson); + console.log("✓ Parsed footerJson successfully:", parsedData); + updateData = parsedData; + } catch (e) { + console.error("✗ Error parsing footerJson:", e.message); + return res.status(400).json({ + success: false, + message: "Invalid JSON in footerJson: " + e.message, + }); + } + } + + // Lấy footer hiện tại hoặc tạo mới (giống Header logic) + let footer = await Footer.findOne(); + + if (!footer) { + console.log("No existing footer found, creating new one"); + footer = new Footer(updateData); + await footer.save(); + console.log("✓ Footer created:", footer._id); + } else { + console.log("✓ Found existing footer:", footer._id); + // Merge với dữ liệu cũ thay vì overwrite (giống Header) + Object.assign(footer, updateData); + await footer.save(); + console.log("✓ Footer updated successfully"); + } + + const processedData = addBaseUrlToImages(footer.toObject()); + + console.log("Updated footer data:", JSON.stringify(processedData, null, 2)); + + res.json({ + success: true, + message: "Footer updated successfully", + data: processedData, + }); + } catch (error) { + console.error("✗ Error updating footer:", error); + res.status(500).json({ + success: false, + error: "Failed to update footer: " + error.message, + }); + } }; -// API để lấy toàn bộ footer data -exports.getFooterData = async (req, res) => { - try { - const footerData = await getFooterData(); - const processed = addBaseUrlToImages(footerData); - res.json(processed); - } catch (error) { - console.error("Error getting footer data:", error); - res.status(500).json({ error: "Error loading footer data" }); - } -}; - -// Render admin view +// Render admin view (giữ lại cho UI hiện tại) exports.index = async (req, res) => { - try { - const data = await getFooterData(); - const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; + try { + const data = await Footer.getSingle(); + const processedData = addBaseUrlToImages(data.toObject()); - // Ensure image paths are absolute for admin preview - const processedData = addBaseUrlToImages(data); - - res.render("admin/footer/index", { - - title: "Footer Management", - data - }); - } catch (error) { - console.error("Error in footer index:", error); - req.flash("error_msg", "An error occurred while loading the page"); - res.redirect("/admin/dashboard"); - } + res.render("admin/footer/index", { + title: "Footer Management", + data: processedData, + }); + } catch (error) { + console.error("Error in footer index:", error); + req.flash("error_msg", "An error occurred while loading the page"); + res.redirect("/admin/dashboard"); + } }; -// Cập nhật dữ liệu footer +// Update method cho form hiện tại (giống Header pattern) exports.update = async (req, res) => { - try { - let footerData = req.body; - if (footerData.footerJson) { - try { - footerData = JSON.parse(footerData.footerJson); - } catch (err) { - console.warn('Invalid footerJson payload, falling back to req.body'); - } + try { + let updateData = req.body; + + console.log("=== FOOTER FORM UPDATE REQUEST RECEIVED ==="); + console.log("Raw body:", JSON.stringify(req.body, null, 2)); + + // Nếu có footerJson, parse nó (giống Header logic) + if (updateData.footerJson && typeof updateData.footerJson === "string") { + try { + const parsedData = JSON.parse(updateData.footerJson); + console.log("✓ Parsed footerJson successfully:", parsedData); + updateData = parsedData; + } catch (e) { + console.error("✗ Error parsing footerJson:", e.message); + req.flash("error_msg", "Invalid JSON in footerJson: " + e.message); + return res.redirect("/admin/footer"); + } + } + + // Lấy footer hiện tại hoặc tạo mới (giống Header) + let footer = await Footer.findOne(); + + if (!footer) { + console.log("No existing footer found, creating new one"); + footer = new Footer(updateData); + await footer.save(); + console.log("✓ Footer created:", footer._id); + req.flash("success_msg", "Footer created successfully"); + } else { + console.log("✓ Found existing footer:", footer._id); + // Merge với dữ liệu cũ (giống Header) + Object.assign(footer, updateData); + await footer.save(); + console.log("✓ Footer updated successfully"); + req.flash("success_msg", "Footer updated successfully"); + } + + const activeTab = req.body.activeTab || "about"; + res.redirect(`/admin/footer?activeTab=${activeTab}`); + } catch (err) { + console.error("✗ Error updating footer:", err); + req.flash("error_msg", err.message || "Error updating footer"); + res.redirect("/admin/footer"); } - - // Tìm footer hiện có hoặc tạo mới nếu không tồn tại - let footer = await Footer.findOne({ name: "default" }); - - if (!footer) { - footer = new Footer({ - name: "default", - about: footerData.about || { - title: "About GGC", - description: "", - mapLink: { text: "Check on google map", url: "" }, - }, - address: footerData.address || { - text: "", - address2: "", - mapUrl: "", - link2: "", - }, - contact: footerData.contact || { phone: "", hours: "", email: "" }, - columns: Array.isArray(footerData.columns) ? footerData.columns : [], - social: footerData.social || { links: [] }, - copyright: footerData.copyright || { text: "" }, - }); - } else { - // Cập nhật các trường - if (footerData.about) { - footer.about = { - title: footerData.about.title || footer.about?.title || "About GGC", - description: footerData.about.description || footer.about?.description || "", - mapLink: { - text: footerData.about.mapLink?.text || footer.about?.mapLink?.text || "Check on google map", - url: footerData.about.mapLink?.url || footer.about?.mapLink?.url || "" - } - }; - } - if (footerData.address) { - // Đảm bảo address2 tồn tại để tránh undefined trong view/schema - footer.address = { - ...(footer.address?.toObject - ? footer.address.toObject() - : footer.address), - ...footerData.address, - address2: - footerData.address.address2 !== undefined - ? footerData.address.address2 - : footer.address?.address2 || "", - link2: - footerData.address.link2 !== undefined - ? footerData.address.link2 - : footer.address?.link2 || "", - }; - } - if (footerData.contact) footer.contact = footerData.contact; - if (Array.isArray(footerData.columns)) - footer.columns = footerData.columns; - if (footerData.social && Array.isArray(footerData.social.links)) - footer.social = footerData.social; - if (footerData.copyright && typeof footerData.copyright.text === "string") - footer.copyright = footerData.copyright; - } - - await footer.save(); - - req.flash("success_msg", "Footer updated successfully"); - - // Redirect back to the active tab - const activeTab = req.body.activeTab || 'about'; - res.redirect(`/admin/footer?activeTab=${activeTab}`); - } catch (err) { - console.error("Error updating footer:", err); - req.flash("error_msg", err.message || "Error updating footer"); - res.redirect("/admin/footer"); - } }; + +// Legacy API endpoints (giữ lại cho tương thích) +exports.api = exports.getFooter; +exports.getFooterData = exports.getFooter; diff --git a/data/footer.json b/data/footer.json index 113645c..5a617f5 100644 --- a/data/footer.json +++ b/data/footer.json @@ -1,80 +1,80 @@ { - "about": { - "title": "About GGC", - "description": "Welcome to Go and Grow Camp, where adventure, learning, and friendships come together. Join us for unforgettable experiences that inspire confidence, creativity, and connection", - "mapLink": { - "text": "Check on google map", - "url": "https://share.google/8G4LFRq82OwOmlFlN" + "top": { + "bgImage": "/assets/img/home-1/footer-bg.jpg", + "phone": { + "display": "+84 961 83 4040", + "href": "tel:+84961834040" + }, + "address": "734 Luy Ban Bich St, Tan Thanh Ward, Tan Phu Dist, HCMC", + "logo": { + "src": "/assets/img/logo/white-logo.svg", + "alt": "logo", + "href": "/" + }, + "menuLinks": [ + { + "label": "Home", + "href": "/" + }, + { + "label": "About Us", + "href": "/about" + }, + { + "label": "Visa", + "href": "/country-details" + }, + { + "label": "Pages", + "href": "/news-details" + }, + { + "label": "Article", + "href": "/news" + }, + { + "label": "Contact Us", + "href": "/contact" + } + ], + "socialLinks": [ + { + "icon": "fa-brands fa-twitter", + "href": "#" + }, + { + "icon": "fa-brands fa-instagram", + "href": "#" + }, + { + "icon": "fa-brands fa-linkedin", + "href": "#" + }, + { + "icon": "fa-brands fa-youtube", + "href": "#" + } + ] + }, + "bottom": { + "copyright": { + "text": "Copyright©", + "brand": "GRAMENTHEME", + "rights": "All Rights Reserved." + }, + "menuLinks": [ + { + "label": "Terms & Conditions", + "href": "/contact" + }, + { + "label": "Privacy Policy", + "href": "/contact" + }, + { + "label": "Contact Us", + "href": "/contact" + } + ] } - }, - "address": { - "text": "Poblacion, Madridejos 22, Cebu City, Philippines", - "address2": "", - "link2": "", - "mapUrl": "https://share.google/8G4LFRq82OwOmlFlN" - }, - "contact": { - "phone": "+123456789", - "hours": "Mon - Fri 8.00-18.00", - "email": "office@ggcamp.org" - }, - "columns": [ - { - "title": "Explore", - "links": [ - { - "title": "Home", - "url": "/" - }, - { - "title": "Activities", - "url": "/destinations" - }, - { - "title": "Camp Locations", - "url": "/camp-profiles" - }, - { - "title": "About", - "url": "/info/about" - }, - { - "title": "Contact", - "url": "/info/contact" - } - ] - } - ], - "social": { - "links": [ - { - "platform": "Facebook", - "url": "https://www.facebook.com/campadventuregermany/", - "icon": "facebook" - }, - { - "platform": "Twitter", - "url": "#", - "icon": "twitter" - }, - { - "platform": "Instagram", - "url": "https://www.instagram.com/campadventuregermany/", - "icon": "instagram" - }, - { - "platform": "Behance", - "url": "#", - "icon": "behance" - }, - { - "platform": "LinkedIn", - "url": "#", - "icon": "linkedin" - } - ] - }, - "copyright": { - "text": "© 2025 GGC @ All Rights Reserved" - } } diff --git a/models/footer.js b/models/footer.js index 895c089..ade280c 100644 --- a/models/footer.js +++ b/models/footer.js @@ -1,234 +1,213 @@ const mongoose = require("mongoose"); +// Schema cho menu links +const menuLinkSchema = new mongoose.Schema( + { + label: { + type: String, + required: true, + trim: true, + }, + href: { + type: String, + required: true, + trim: true, + }, + order: { + type: Number, + required: false, + default: 0, + }, + }, + { _id: false }, +); + // Schema cho social links const socialLinkSchema = new mongoose.Schema( - { - platform: { - type: String, - required: true, - trim: true, + { + icon: { + type: String, + required: true, + trim: true, + }, + href: { + type: String, + required: true, + trim: true, + }, }, - url: { - type: String, - required: true, - trim: true, - }, - icon: { - type: String, - required: true, - trim: true, - }, - }, - { _id: false } + { _id: false }, ); -// Schema cho footer links -const footerLinkSchema = new mongoose.Schema( - { - title: { - type: String, - required: true, - trim: true, +// Schema cho phone +const phoneSchema = new mongoose.Schema( + { + display: { + type: String, + required: false, + trim: true, + default: "", + }, + href: { + type: String, + required: false, + trim: true, + default: "", + }, }, - url: { - type: String, - required: true, - trim: true, - }, - }, - { _id: false } -); - -// Schema cho footer columns -const footerColumnSchema = new mongoose.Schema( - { - title: { - type: String, - required: true, - trim: true, - }, - links: { - type: [footerLinkSchema], - default: [], - }, - }, - { _id: false } + { _id: false }, ); // Schema cho logo const logoSchema = new mongoose.Schema( - { - src: { - type: String, - required: true, - trim: true, + { + src: { + type: String, + required: false, + trim: true, + default: "", + }, + alt: { + type: String, + required: false, + trim: true, + default: "", + }, + href: { + type: String, + required: false, + trim: true, + default: "/", + }, }, - alt: { - type: String, - required: true, - trim: true, - }, - }, - { _id: false } -); - -// Schema cho address -const addressSchema = new mongoose.Schema( - { - text: { - type: String, - required: true, - trim: true, - }, - // Thêm address2 (địa chỉ dòng 2) không bắt buộc - address2: { - type: String, - trim: true, - default: "", - }, - // Optional secondary link (e.g., a second map or external resource) - link2: { - type: String, - trim: true, - default: "", - }, - mapUrl: { - type: String, - required: true, - trim: true, - }, - }, - { _id: false } -); - -// Schema cho contact info -const contactInfoSchema = new mongoose.Schema( - { - phone: { - type: String, - required: true, - trim: true, - }, - hours: { - type: String, - required: true, - trim: true, - }, - email: { - type: String, - required: true, - trim: true, - }, - }, - { _id: false } + { _id: false }, ); // Schema cho copyright const copyrightSchema = new mongoose.Schema( - { - text: { - type: String, - required: true, - trim: true, + { + text: { + type: String, + required: false, + trim: true, + default: "Copyright©", + }, + brand: { + type: String, + required: false, + trim: true, + default: "", + }, + rights: { + type: String, + required: false, + trim: true, + default: "All Rights Reserved.", + }, }, - }, - { _id: false } + { _id: false }, ); -// Schema cho about section (column đầu tiên) -const aboutSchema = new mongoose.Schema( - { - title: { - type: String, - required: true, - trim: true, +// Schema cho top section +const topSchema = new mongoose.Schema( + { + bgImage: { + type: String, + required: false, + trim: true, + default: "", + }, + phone: { + type: phoneSchema, + default: () => ({ display: "", href: "" }), + }, + address: { + type: String, + required: false, + trim: true, + default: "", + }, + logo: { + type: logoSchema, + default: () => ({ src: "", alt: "", href: "/" }), + }, + menuLinks: { + type: [menuLinkSchema], + default: [], + }, + socialLinks: { + type: [socialLinkSchema], + default: [], + }, }, - description: { - type: String, - required: true, - trim: true, - }, - mapLink: { - text: { - type: String, - required: true, - trim: true, - }, - url: { - type: String, - required: true, - trim: true, - }, - }, - }, - { _id: false } + { _id: false }, ); -// Main Footer Schema +// Schema cho bottom section +const bottomSchema = new mongoose.Schema( + { + copyright: { + type: copyrightSchema, + default: () => ({ text: "Copyright©", brand: "", rights: "All Rights Reserved." }), + }, + menuLinks: { + type: [menuLinkSchema], + default: [], + }, + }, + { _id: false }, +); + +// Main Footer Schema - khớp 100% với footer.json const footerSchema = new mongoose.Schema( - { - name: { - type: String, - default: "default", - unique: true, + { + top: { + type: topSchema, + default: () => ({ + bgImage: "", + phone: { display: "", href: "" }, + address: "", + logo: { src: "", alt: "", href: "/" }, + menuLinks: [], + socialLinks: [], + }), + }, + bottom: { + type: bottomSchema, + default: () => ({ + copyright: { text: "Copyright©", brand: "", rights: "All Rights Reserved." }, + menuLinks: [], + }), + }, }, - about: { - type: aboutSchema, - required: true, + { + timestamps: true, }, - address: { - type: addressSchema, - required: true, - }, - contact: { - type: contactInfoSchema, - required: true, - }, - columns: { - type: [footerColumnSchema], - default: [], - }, - social: { - links: { - type: [socialLinkSchema], - default: [], - }, - }, - copyright: { - type: copyrightSchema, - required: true, - }, - }, - { - timestamps: true, - } ); -// Tạo migration script để import dữ liệu từ footer.json -footerSchema.statics.migrateFromJson = async function (jsonData) { - try { - // Kiểm tra xem đã có footer mặc định chưa - const existingFooter = await this.findOne({ name: "default" }); - - if (existingFooter) { - // Cập nhật footer hiện có - Object.assign(existingFooter, jsonData); - await existingFooter.save(); - console.log("Footer data updated successfully"); - return existingFooter; - } else { - // Tạo footer mới với dữ liệu từ JSON - const newFooter = await this.create({ - name: "default", - ...jsonData, - }); - console.log("Footer data imported successfully"); - return newFooter; +// Static method để lấy hoặc tạo footer duy nhất +footerSchema.statics.getSingle = async function () { + let footer = await this.findOne(); + if (!footer) { + footer = await this.create({}); + } + return footer; +}; + +// Migration method để import từ JSON hiện tại +footerSchema.statics.migrateFromJson = async function (jsonData) { + try { + // Xóa tất cả documents hiện có + await this.deleteMany({}); + + // Tạo document mới + const footer = await this.create(jsonData); + console.log("Footer data migrated successfully"); + return footer; + } catch (error) { + console.error("Error migrating footer data:", error); + throw error; } - } catch (error) { - console.error("Error migrating footer data:", error); - throw error; - } }; module.exports = mongoose.model("Footer", footerSchema); diff --git a/models/visa.js b/models/visa.js index 60855be..b0589d1 100644 --- a/models/visa.js +++ b/models/visa.js @@ -6,221 +6,221 @@ const mongoose = require("mongoose"); // VisaItem Schema const VisaItemSchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - description: { type: String, default: "" }, - }, - { _id: false }, + { + title: { type: String, default: "" }, + description: { type: String, default: "" }, + }, + { _id: false }, ); // VisaTypeCategory Schema const VisaTypeCategorySchema = new mongoose.Schema( - { - category: { type: String, default: "" }, - items: [VisaItemSchema], - }, - { _id: false }, + { + category: { type: String, default: "" }, + items: [VisaItemSchema], + }, + { _id: false }, ); // VisaProcessStep Schema const VisaProcessStepSchema = new mongoose.Schema( - { - number: { type: String, default: "" }, - title: { type: String, default: "" }, - description: { type: String, default: "" }, - }, - { _id: false }, + { + number: { type: String, default: "" }, + title: { type: String, default: "" }, + description: { type: String, default: "" }, + }, + { _id: false }, ); // VisaProcess Schema const VisaProcessSchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - steps: [VisaProcessStepSchema], - }, - { _id: false }, + { + title: { type: String, default: "" }, + steps: [VisaProcessStepSchema], + }, + { _id: false }, ); // VisaCategory Schema const VisaCategorySchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - steps: { - type: [[String]], - default: [], + { + title: { type: String, default: "" }, + steps: { + type: [[String]], + default: [], + }, }, - }, - { _id: false }, + { _id: false }, ); // VisaService Schema const VisaServiceSchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - steps: [VisaProcessStepSchema], - }, - { _id: false }, + { + title: { type: String, default: "" }, + steps: [VisaProcessStepSchema], + }, + { _id: false }, ); // RelatedCountry Schema const RelatedCountrySchema = new mongoose.Schema( - { - id: { type: Number, default: 0 }, - name: { type: String, default: "" }, - icon: { type: String, default: "" }, - }, - { _id: false }, + { + id: { type: Number, default: 0 }, + name: { type: String, default: "" }, + icon: { type: String, default: "" }, + }, + { _id: false }, ); // Phone Schema const PhoneSchema = new mongoose.Schema( - { - label: { type: String, default: "" }, - value: { type: String, default: "" }, - link: { type: String, default: "" }, - }, - { _id: false }, + { + label: { type: String, default: "" }, + value: { type: String, default: "" }, + link: { type: String, default: "" }, + }, + { _id: false }, ); // Email Schema const EmailSchema = new mongoose.Schema( - { - label: { type: String, default: "" }, - value: { type: String, default: "" }, - link: { type: String, default: "" }, - }, - { _id: false }, + { + label: { type: String, default: "" }, + value: { type: String, default: "" }, + link: { type: String, default: "" }, + }, + { _id: false }, ); // Location Schema const LocationSchema = new mongoose.Schema( - { - label: { type: String, default: "" }, - address: { type: String, default: "" }, - }, - { _id: false }, + { + label: { type: String, default: "" }, + address: { type: String, default: "" }, + }, + { _id: false }, ); // ContactInfo Schema const ContactInfoSchema = new mongoose.Schema( - { - img: { type: String, default: "" }, - sectionTitle: { type: String, default: "" }, - helpText: { type: String, default: "" }, + { + img: { type: String, default: "" }, + sectionTitle: { type: String, default: "" }, + helpText: { type: String, default: "" }, - phone: { - type: PhoneSchema, - default: () => ({}), + phone: { + type: PhoneSchema, + default: () => ({}), + }, + email: { + type: EmailSchema, + default: () => ({}), + }, + location: { + type: LocationSchema, + default: () => ({}), + }, }, - email: { - type: EmailSchema, - default: () => ({}), - }, - location: { - type: LocationSchema, - default: () => ({}), - }, - }, - { _id: false }, + { _id: false }, ); // ActiveCountry Schema const ActiveCountrySchema = new mongoose.Schema( - { - id: { type: Number, default: 0 }, - name: { type: String, default: "" }, - title: { type: String, default: "" }, - mainImage: { type: String, default: "" }, - description: { type: String, default: "" }, - additionalInfo: { type: String, default: "" }, - tagline: { type: String, default: "" }, - visaTypes: [VisaTypeCategorySchema], - visaProcess: { - type: VisaProcessSchema, - default: null, + { + id: { type: Number, default: 0 }, + name: { type: String, default: "" }, + title: { type: String, default: "" }, + mainImage: { type: String, default: "" }, + description: { type: String, default: "" }, + additionalInfo: { type: String, default: "" }, + tagline: { type: String, default: "" }, + visaTypes: [VisaTypeCategorySchema], + visaProcess: { + type: VisaProcessSchema, + default: null, + }, + gallery: { + type: [String], + default: [], + }, + visaCategories: { + type: VisaCategorySchema, + default: null, + }, + visaService: { + type: VisaServiceSchema, + default: null, + }, }, - gallery: { - type: [String], - default: [], - }, - visaCategories: { - type: VisaCategorySchema, - default: null, - }, - visaService: { - type: VisaServiceSchema, - default: null, - }, - }, - { _id: false }, + { _id: false }, ); // DetailedView Schema const DetailedViewSchema = new mongoose.Schema( - { - activeCountry: { - type: ActiveCountrySchema, - default: null, + { + activeCountry: { + type: ActiveCountrySchema, + default: null, + }, + relatedCountries: { + type: [RelatedCountrySchema], + default: [], + }, + contactInfo: { + type: ContactInfoSchema, + default: null, + }, }, - relatedCountries: { - type: [RelatedCountrySchema], - default: [], - }, - contactInfo: { - type: ContactInfoSchema, - default: null, - }, - }, - { _id: false }, + { _id: false }, ); // ==================== MAIN VISA COUNTRY SCHEMA ==================== // Main VisaCountry Schema (Individual country object) const VisaCountrySchema = new mongoose.Schema( - { - // Không dùng `index: true` ở đây vì đã tạo index riêng cho hero.summaryList.* bên dưới - id: { type: Number, required: true }, - name: { type: String, required: true }, - slug: { type: String, required: true, unique: true }, - icon: { type: String, default: "" }, - services: { - type: [String], - default: [], + { + // Không dùng `index: true` ở đây vì đã tạo index riêng cho hero.summaryList.* bên dưới + id: { type: Number, required: true }, + name: { type: String, required: true }, + slug: { type: String, required: true }, + icon: { type: String, default: "" }, + services: { + type: [String], + default: [], + }, + detailedView: { + type: DetailedViewSchema, + default: null, + }, }, - detailedView: { - type: DetailedViewSchema, - default: null, - }, - }, - { _id: false }, + { _id: false }, ); // ==================== HERO SCHEMA ==================== const HeroSchema = new mongoose.Schema( - { - title: { type: String, default: "Visa" }, - summaryList: { - type: [VisaCountrySchema], - default: [], + { + title: { type: String, default: "Visa" }, + summaryList: { + type: [VisaCountrySchema], + default: [], + }, }, - }, - { _id: false }, + { _id: false }, ); // ==================== MAIN VISA SCHEMA ==================== const visaDataSchema = new mongoose.Schema( - { - hero: { - type: HeroSchema, - default: () => ({ title: "Visa", summaryList: [] }), + { + hero: { + type: HeroSchema, + default: () => ({ title: "Visa", summaryList: [] }), + }, + }, + { + timestamps: true, }, - }, - { - timestamps: true, - }, ); // ==================== INDEXES ==================== diff --git a/routes/admin.js b/routes/admin.js index 8a7b199..f83fd2b 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -41,8 +41,8 @@ router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs); // Middleware chuẩn hóa code router.param("code", (req, res, next, code) => { - req.params.code = code.toUpperCase(); - next(); + req.params.code = code.toUpperCase(); + next(); }); // About @@ -51,125 +51,51 @@ router.post("/about/update", ensureAuthenticated, aboutController.update); // AboutUs admin CRUD router.get("/about-us", ensureAuthenticated, aboutUsController.index); -router.get( - "/about-us/create", - ensureAuthenticated, - aboutUsController.createForm, -); +router.get("/about-us/create", ensureAuthenticated, aboutUsController.createForm); router.post("/about-us/create", ensureAuthenticated, aboutUsController.create); -router.get( - "/about-us/:id/edit", - ensureAuthenticated, - aboutUsController.editForm, -); -router.post( - "/about-us/:id/update", - ensureAuthenticated, - aboutUsController.update, -); -router.post( - "/about-us/:id/delete", - ensureAuthenticated, - aboutUsController.delete, -); -router.get( - "/about-us/:id/preview", - ensureAuthenticated, - aboutUsController.preview, -); +router.get("/about-us/:id/edit", ensureAuthenticated, aboutUsController.editForm); +router.post("/about-us/:id/update", ensureAuthenticated, aboutUsController.update); +router.post("/about-us/:id/delete", ensureAuthenticated, aboutUsController.delete); +router.get("/about-us/:id/preview", ensureAuthenticated, aboutUsController.preview); // Booking admin CRUD removed // Form Management router.get("/form", ensureAuthenticated, formController.index); -router.post( - "/form/update", - ensureAuthenticated, - formController.updateDefaultForm, -); +router.post("/form/update", ensureAuthenticated, formController.updateDefaultForm); // Upload routes router.get("/upload", ensureAuthenticated, (req, res) => { - res.render("admin/upload/index", { - layout: "layouts/admin", - title: "Quản lý Upload Ảnh", - user: req.session.user, - }); + res.render("admin/upload/index", { + layout: "layouts/admin", + title: "Quản lý Upload Ảnh", + user: req.session.user, + }); }); -router.post( - "/upload/image", - ensureAuthenticated, - upload.single("image"), - uploadController.uploadImage, -); -router.post( - "/upload/video", - ensureAuthenticated, - uploadVideo.single("video"), - uploadController.uploadVideo, -); -router.post( - "/upload/update-path", - ensureAuthenticated, - uploadController.updateImagePath, -); -router.post( - "/upload/delete", - ensureAuthenticated, - uploadController.deleteImage, -); +router.post("/upload/image", ensureAuthenticated, upload.single("image"), uploadController.uploadImage); +router.post("/upload/video", ensureAuthenticated, uploadVideo.single("video"), uploadController.uploadVideo); +router.post("/upload/update-path", ensureAuthenticated, uploadController.updateImagePath); +router.post("/upload/delete", ensureAuthenticated, uploadController.deleteImage); // Header routes router.get("/header", ensureAuthenticated, headerController.index); router.post("/header/update", ensureAuthenticated, headerController.update); router.get("/header/data", ensureAuthenticated, headerController.api); // Normalized from getHeaderData -router.patch( - "/header/:id/status", - ensureAuthenticated, - headerController.updateStatus, -); +router.patch("/header/:id/status", ensureAuthenticated, headerController.updateStatus); router.delete("/header/:id", ensureAuthenticated, headerController.destroy); // Header Menu INTEGRATED routes -router.post( - "/header/menu/create", - ensureAuthenticated, - headerMenuController.store, -); -router.post( - "/header/menu/update/:id", - ensureAuthenticated, - headerMenuController.update, -); -router.post( - "/header/menu/delete", - ensureAuthenticated, - headerMenuController.destroy, -); -router.post( - "/header/menu/reorder", - ensureAuthenticated, - headerMenuController.reorder, -); +router.post("/header/menu/create", ensureAuthenticated, headerMenuController.store); +router.post("/header/menu/update/:id", ensureAuthenticated, headerMenuController.update); +router.post("/header/menu/delete", ensureAuthenticated, headerMenuController.destroy); +router.post("/header/menu/reorder", ensureAuthenticated, headerMenuController.reorder); // Social Links routes router.get("/social-links", ensureAuthenticated, socialLinkController.index); router.post("/social-links", ensureAuthenticated, socialLinkController.store); -router.put( - "/social-links/:platform", - ensureAuthenticated, - socialLinkController.update, -); -router.delete( - "/social-links/:platform", - ensureAuthenticated, - socialLinkController.destroy, -); -router.post( - "/social-links/reorder", - ensureAuthenticated, - socialLinkController.reorder, -); +router.put("/social-links/:platform", ensureAuthenticated, socialLinkController.update); +router.delete("/social-links/:platform", ensureAuthenticated, socialLinkController.destroy); +router.post("/social-links/reorder", ensureAuthenticated, socialLinkController.reorder); // Footer routes router.get("/footer", ensureAuthenticated, footerController.index); @@ -179,160 +105,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); @@ -342,16 +168,8 @@ router.get("/faq/api", faqController.api); // API routes cho quản lý FAQ items (AJAX calls) router.post("/faq/api/add-faq", ensureAuthenticated, faqController.addFAQ); -router.put( - "/faq/api/update-faq-item/:sectionId/:faqId", - ensureAuthenticated, - faqController.updateFAQItem, -); -router.delete( - "/faq/api/delete-faq-item/:sectionId/:faqId", - ensureAuthenticated, - faqController.deleteFAQItem, -); +router.put("/faq/api/update-faq-item/:sectionId/:faqId", ensureAuthenticated, faqController.updateFAQItem); +router.delete("/faq/api/delete-faq-item/:sectionId/:faqId", ensureAuthenticated, faqController.deleteFAQItem); router.get("/terms-conditions", ensureAuthenticated, termsController.index); router.post("/terms/update", ensureAuthenticated, termsController.update); router.get("/terms/data", ensureAuthenticated, termsController.getTermsData); @@ -367,33 +185,13 @@ router.get("/travel/api", travelController.api); router.get("/travel/seed", ensureAuthenticated, travelController.seed); // API routes cho quản lý FAQ sections (AJAX calls) -router.post( - "/faq/api/add-section", - ensureAuthenticated, - faqController.addFAQSection, -); -router.put( - "/faq/api/update-section/:sectionId", - ensureAuthenticated, - faqController.updateFAQSection, -); -router.delete( - "/faq/api/delete-section/:sectionId", - ensureAuthenticated, - faqController.deleteFAQSection, -); -router.post( - "/faq/api/reorder-sections", - ensureAuthenticated, - faqController.reorderFAQSection, -); +router.post("/faq/api/add-section", ensureAuthenticated, faqController.addFAQSection); +router.put("/faq/api/update-section/:sectionId", ensureAuthenticated, faqController.updateFAQSection); +router.delete("/faq/api/delete-section/:sectionId", ensureAuthenticated, faqController.deleteFAQSection); +router.post("/faq/api/reorder-sections", ensureAuthenticated, faqController.reorderFAQSection); // API routes cho sidebar navigation (AJAX calls) -router.put( - "/faq/api/update-sidebar", - ensureAuthenticated, - faqController.updateSidebarNav, -); +router.put("/faq/api/update-sidebar", ensureAuthenticated, faqController.updateSidebarNav); // Safety routes router.get("/safety", ensureAuthenticated, safetyController.index); @@ -401,98 +199,74 @@ router.post("/safety/update", ensureAuthenticated, safetyController.update); //Insurance routes router.get("/insurance", ensureAuthenticated, insuranceController.index); -router.post( - "/insurance/update", - ensureAuthenticated, - insuranceController.update, -); +router.post("/insurance/update", ensureAuthenticated, insuranceController.update); // Service routes router.get("/service", ensureAuthenticated, serviceController.index); router.post("/service/update", ensureAuthenticated, serviceController.update); -router.post( - "/service/generate-slug", - ensureAuthenticated, - serviceController.generateSlug, -); +router.post("/service/generate-slug", ensureAuthenticated, serviceController.generateSlug); router.get("/service/:slug/edit", ensureAuthenticated, serviceController.edit); -router.post( - "/service/:slug/edit", - ensureAuthenticated, - serviceController.updateService, -); -router.get( - "/service/:slug/details", - ensureAuthenticated, - serviceController.details, -); -router.post( - "/service/:slug/details/update", - ensureAuthenticated, - serviceController.updateDetails, -); +router.post("/service/:slug/edit", ensureAuthenticated, serviceController.updateService); +router.get("/service/:slug/details", ensureAuthenticated, serviceController.details); +router.post("/service/:slug/details/update", ensureAuthenticated, serviceController.updateDetails); // Test Image Paths route router.get("/test-images", ensureAuthenticated, (req, res) => { - const fs = require("fs"); - const path = require("path"); - const campLocationData = require("../data/camp-location.json"); + 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 @@ -508,18 +282,10 @@ router.post("/visa/update", ensureAuthenticated, visaController.updateCountry); router.post("/visa/add", ensureAuthenticated, visaController.addCountry); // Update single country -router.put( - "/visa/update/:id", - ensureAuthenticated, - visaController.updateCountry, -); +router.put("/visa/update/:id", ensureAuthenticated, visaController.updateCountry); // Delete country -router.delete( - "/visa/delete/:id", - ensureAuthenticated, - visaController.deleteCountry, -); +router.delete("/visa/delete/:id", ensureAuthenticated, visaController.deleteCountry); // Blog routes // Blog Management Routes router.get("/blog", ensureAuthenticated, blogController.index); @@ -530,78 +296,26 @@ router.post("/blog/:id/edit", ensureAuthenticated, blogController.update); router.post("/blog/:id/delete", ensureAuthenticated, blogController.destroy); // Comment management routes -router.post( - "/blog/:blogId/comments/:commentId/approve", - ensureAuthenticated, - blogController.approveComment, -); -router.post( - "/blog/:blogId/comments/:commentId/reject", - ensureAuthenticated, - blogController.rejectComment, -); -router.post( - "/blog/:blogId/comments/:commentId/delete", - ensureAuthenticated, - blogController.deleteComment, -); +router.post("/blog/:blogId/comments/:commentId/approve", ensureAuthenticated, blogController.approveComment); +router.post("/blog/:blogId/comments/:commentId/reject", ensureAuthenticated, blogController.rejectComment); +router.post("/blog/:blogId/comments/:commentId/delete", ensureAuthenticated, blogController.deleteComment); // Blog Categories Management -router.get( - "/blog/categories", - ensureAuthenticated, - blogCategoryController.index, -); -router.get( - "/blog/categories/create", - ensureAuthenticated, - blogCategoryController.create, -); -router.post( - "/blog/categories/create", - ensureAuthenticated, - blogCategoryController.store, -); -router.get( - "/blog/categories/:id/edit", - ensureAuthenticated, - blogCategoryController.edit, -); -router.post( - "/blog/categories/:id/edit", - ensureAuthenticated, - blogCategoryController.update, -); -router.post( - "/blog/categories/:id/delete", - ensureAuthenticated, - blogCategoryController.destroy, -); -router.post( - "/blog/categories/quick-create", - ensureAuthenticated, - blogCategoryController.quickCreate, -); +router.get("/blog/categories", ensureAuthenticated, blogCategoryController.index); +router.get("/blog/categories/create", ensureAuthenticated, blogCategoryController.create); +router.post("/blog/categories/create", ensureAuthenticated, blogCategoryController.store); +router.get("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.edit); +router.post("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.update); +router.post("/blog/categories/:id/delete", ensureAuthenticated, blogCategoryController.destroy); +router.post("/blog/categories/quick-create", ensureAuthenticated, blogCategoryController.quickCreate); // Blog Tags Management router.get("/blog/tags", ensureAuthenticated, blogTagController.index); router.get("/blog/tags/create", ensureAuthenticated, blogTagController.create); router.post("/blog/tags/create", ensureAuthenticated, blogTagController.store); router.get("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.edit); -router.post( - "/blog/tags/:id/edit", - ensureAuthenticated, - blogTagController.update, -); -router.post( - "/blog/tags/:id/delete", - ensureAuthenticated, - blogTagController.destroy, -); -router.post( - "/blog/tags/quick-create", - ensureAuthenticated, - blogTagController.quickCreate, -); +router.post("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.update); +router.post("/blog/tags/:id/delete", ensureAuthenticated, blogTagController.destroy); +router.post("/blog/tags/quick-create", ensureAuthenticated, blogTagController.quickCreate); module.exports = router; diff --git a/routes/index.js b/routes/index.js index 8090a59..ed05417 100644 --- a/routes/index.js +++ b/routes/index.js @@ -58,8 +58,9 @@ router.get("/api/header-menu", headerMenuController.api); router.get("/api/social-links", socialLinkController.index); router.get("/api/social-links/:platform", socialLinkController.show); -// Footer API route -router.get("/api/footer", footerController.api); +// Footer API routes +router.get("/api/footer", footerController.getFooter); +router.put("/api/admin/footer", footerController.updateFooter); // Contact API route router.get("/api/contact", contactController.api); @@ -175,4 +176,12 @@ router.get("/api/service-slugs", serviceController.getServiceSlugs); router.get("/api/visa", visaController.api); router.get("/api/visa/country", visaController.apiCountries); +// Test route for footer +router.get("/test-footer", (req, res) => { + res.render("test-footer", { + title: "Footer Test", + layout: "layouts/main", + }); +}); + module.exports = router; diff --git a/scripts/2026_02_05_120000_footer.js b/scripts/2026_02_05_120000_footer.js new file mode 100644 index 0000000..5b8111b --- /dev/null +++ b/scripts/2026_02_05_120000_footer.js @@ -0,0 +1,68 @@ +const mongoose = require("mongoose"); +const path = require("path"); +const fs = require("fs"); + +// Import model +const Footer = require("../models/footer"); + +/** + * Migration script để import dữ liệu footer từ JSON + */ +async function up() { + try { + console.log("Starting footer migration..."); + + // Đọc dữ liệu từ file JSON + const jsonPath = path.join(__dirname, "../data/footer.json"); + + if (!fs.existsSync(jsonPath)) { + throw new Error("Footer JSON file not found"); + } + + const footerData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); + + // Sử dụng static method từ model để migrate + const result = await Footer.migrateFromJson(footerData); + + console.log("Footer migration completed successfully"); + return result; + } catch (error) { + console.error("Footer migration failed:", error); + throw error; + } +} + +/** + * Rollback migration + */ +async function down() { + try { + console.log("Rolling back footer migration..."); + + // Xóa footer data + await Footer.deleteMany({}); + + console.log("Footer rollback completed"); + } catch (error) { + console.error("Footer rollback failed:", error); + throw error; + } +} + +module.exports = { up, down }; + +// Chạy migration nếu file được gọi trực tiếp +if (require.main === module) { + const connectDB = require("../config/database"); + + connectDB() + .then(() => up()) + .then(() => { + console.log("Migration completed successfully"); + process.exit(0); + }) + .catch((error) => { + console.error("Migration failed:", error); + process.exit(1); + }); +} diff --git a/scripts/2026_02_05_130000_add_footer_menu_order.js b/scripts/2026_02_05_130000_add_footer_menu_order.js new file mode 100644 index 0000000..e2c2f2f --- /dev/null +++ b/scripts/2026_02_05_130000_add_footer_menu_order.js @@ -0,0 +1,90 @@ +const mongoose = require("mongoose"); +const Footer = require("../models/footer"); +const footerData = require("../data/footer.json"); + +async function addFooterMenuOrder() { + try { + console.log("=== Adding order field to Footer Menu Links ==="); + + // Connect to database + await mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/hailearning"); + console.log("✓ Connected to MongoDB"); + + // Get existing footer or create from JSON + let footer = await Footer.findOne(); + + if (!footer) { + console.log("No existing footer found, creating from JSON data..."); + + // Add order to bottom menu links + if (footerData.bottom && footerData.bottom.menuLinks) { + footerData.bottom.menuLinks = footerData.bottom.menuLinks.map((link, index) => ({ + ...link, + order: index + 1, + })); + } + + // Add order to top menu links + if (footerData.top && footerData.top.menuLinks) { + footerData.top.menuLinks = footerData.top.menuLinks.map((link, index) => ({ + ...link, + order: index + 1, + })); + } + + footer = await Footer.create(footerData); + console.log("✓ Footer created with order fields"); + } else { + console.log("Found existing footer, adding order fields..."); + + // Add order to bottom menu links + if (footer.bottom && footer.bottom.menuLinks) { + footer.bottom.menuLinks = footer.bottom.menuLinks.map((link, index) => ({ + label: link.label, + href: link.href, + order: link.order || index + 1, + })); + } + + // Add order to top menu links + if (footer.top && footer.top.menuLinks) { + footer.top.menuLinks = footer.top.menuLinks.map((link, index) => ({ + label: link.label, + href: link.href, + order: link.order || index + 1, + })); + } + + await footer.save(); + console.log("✓ Footer updated with order fields"); + } + + console.log("Bottom Menu Links with order:"); + footer.bottom.menuLinks.forEach((link, index) => { + console.log(` ${index + 1}. ${link.label} (order: ${link.order}) -> ${link.href}`); + }); + + console.log("=== Footer Menu Order Migration Completed ==="); + } catch (error) { + console.error("✗ Migration failed:", error); + throw error; + } finally { + await mongoose.disconnect(); + console.log("✓ Disconnected from MongoDB"); + } +} + +// Run migration if called directly +if (require.main === module) { + addFooterMenuOrder() + .then(() => { + console.log("Migration completed successfully"); + process.exit(0); + }) + .catch((error) => { + console.error("Migration failed:", error); + process.exit(1); + }); +} + +module.exports = addFooterMenuOrder; diff --git a/views/admin/footer/index.ejs b/views/admin/footer/index.ejs index a1a2597..08257ef 100644 --- a/views/admin/footer/index.ejs +++ b/views/admin/footer/index.ejs @@ -1,764 +1,1613 @@
-
-
-

- Footer Management -

-

Edit footer content and structure

+
+
+

Footer Management

+

Edit footer content and structure

+
- -
-
-
-
- - - - -
- +
+
+
+ + + -
-
- -
-
-
-
About GGC Section
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
- - - - - -
-
-
-
Contact & Address Information
-
-
- -
-
-
Address
-
-
- - -
-
- - -
-
- - -
-
- - -
+ +
+ - -
-
-
Contact Information
-
-
- - -
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
Footer Columns
- -
-
-
- <% data.columns.forEach((column, columnIndex)=> { %> -
-
-
-
Column <%= columnIndex + 1 %> -
-
- -
-
-
-
-
-
- - -
-
- -
-
-
Links
- -
- -
-
-
-
Social Media Links
- -
-
- -
-
-
- - -
-
- - -
- - -
-
-
+
+ + + + + + + + + \ No newline at end of file + } + diff --git a/views/layouts/main.ejs b/views/layouts/main.ejs index 1345671..b1231d1 100644 --- a/views/layouts/main.ejs +++ b/views/layouts/main.ejs @@ -820,45 +820,18 @@ -