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