forked from UKSOURCE/cms.hailearning.edu.vn
refactor: restructure home model and update homeController
This commit is contained in:
@@ -1,315 +1,249 @@
|
||||
const { addBaseUrlToImages } = require('../utils/imageHelper');
|
||||
const Home = require('../models/home');
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Home = require("../models/home");
|
||||
|
||||
// -------------------- Helper Functions --------------------
|
||||
// -------------------- Helpers --------------------
|
||||
|
||||
// Get home data from MongoDB
|
||||
const getHomeData = async () => {
|
||||
const home = await Home.findOne().sort({ updatedAt: -1 }).lean();
|
||||
return home || {};
|
||||
const getHomeDoc = async () => {
|
||||
// Keep newest document as the source of truth
|
||||
return await Home.findOne().sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
// Get default home data structure
|
||||
const getHomeData = async () => {
|
||||
const doc = await Home.findOne().sort({ updatedAt: -1 }).lean();
|
||||
return doc || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Default structure used by the current CMS Home admin UI (`views/admin/home/index.ejs`).
|
||||
* This is intentionally permissive; the Home model itself also supports the Next.js
|
||||
* structure from `hailearning.edu.vn/app/home.json`.
|
||||
*/
|
||||
const getDefaultHomeData = () => ({
|
||||
hero: {
|
||||
title: '',
|
||||
description: '',
|
||||
backgroundImage: '',
|
||||
button: { label: 'Book Your Adventure', href: '/booking' },
|
||||
title: "",
|
||||
description: "",
|
||||
backgroundImage: "",
|
||||
button: { label: "Book Your Adventure", href: "/booking" },
|
||||
contactBox: {
|
||||
welcomeText: '',
|
||||
phone: { label: 'Call us', number: '', href: '' },
|
||||
email: { label: 'Email', address: '', href: '' },
|
||||
workingHours: { label: 'Working Hours', hours: '' }
|
||||
}
|
||||
welcomeText: "",
|
||||
phone: { label: "Call us", number: "", href: "" },
|
||||
email: { label: "Email", address: "", href: "" },
|
||||
workingHours: { label: "Working Hours", hours: "" },
|
||||
},
|
||||
},
|
||||
about: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
images: { mainImage1: '', mainImage2: '', avatars: [] },
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
images: { mainImage1: "", mainImage2: "", avatars: [] },
|
||||
features: [],
|
||||
quote: '',
|
||||
button: { label: '', href: '' },
|
||||
stats: { customerCount: 0, customerLabel: '' }
|
||||
quote: "",
|
||||
button: { label: "", href: "" },
|
||||
stats: { customerCount: 0, customerLabel: "" },
|
||||
},
|
||||
missionVision: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
backgroundImage: '',
|
||||
cards: []
|
||||
title: "",
|
||||
subtitle: "",
|
||||
backgroundImage: "",
|
||||
cards: [],
|
||||
},
|
||||
whyChooseUs: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
button: { label: '', href: '' },
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
button: { label: "", href: "" },
|
||||
features: [],
|
||||
tags: [],
|
||||
cta: { text: '', linkText: '', linkHref: '' }
|
||||
},
|
||||
activities: {
|
||||
cards: []
|
||||
cta: { text: "", linkText: "", linkHref: "" },
|
||||
},
|
||||
activities: { cards: [] },
|
||||
faq: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
image: '',
|
||||
contact: { title: '', info: '' },
|
||||
questions: []
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
image: "",
|
||||
contact: { title: "", info: "" },
|
||||
questions: [],
|
||||
},
|
||||
partners: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
backgroundImage: '',
|
||||
title: "",
|
||||
subtitle: "",
|
||||
backgroundImage: "",
|
||||
logos: [],
|
||||
cta: { badge: '', text: '', linkText: '', linkHref: '' }
|
||||
cta: { badge: "", text: "", linkText: "", linkHref: "" },
|
||||
},
|
||||
programs: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
button: { label: '', href: '' },
|
||||
title: "",
|
||||
subtitle: "",
|
||||
button: { label: "", href: "" },
|
||||
card: {
|
||||
pricePrefix: 'from',
|
||||
priceSuffix: 'USD',
|
||||
buttonLabel: 'Camp Detail',
|
||||
buttonHref: '/camp-profiles'
|
||||
pricePrefix: "from",
|
||||
priceSuffix: "USD",
|
||||
buttonLabel: "Camp Detail",
|
||||
buttonHref: "/camp-profiles",
|
||||
},
|
||||
items: []
|
||||
items: [],
|
||||
},
|
||||
newsletter: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
image: '',
|
||||
decorativeImage: '',
|
||||
button: {
|
||||
label: '',
|
||||
placeholder: '',
|
||||
href: ''
|
||||
}
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
image: "",
|
||||
decorativeImage: "",
|
||||
button: { label: "", placeholder: "", href: "" },
|
||||
},
|
||||
latestPosts: {
|
||||
title: '',
|
||||
subtitle: '',
|
||||
searchPlaceholder: '',
|
||||
sidebarTitle: '',
|
||||
title: "",
|
||||
subtitle: "",
|
||||
searchPlaceholder: "",
|
||||
sidebarTitle: "",
|
||||
blogPosts: [],
|
||||
sidebarPosts: [],
|
||||
featuredCard: { image: '', title: '', description: '' }
|
||||
}
|
||||
featuredCard: { image: "", title: "", description: "" },
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------- Admin --------------------
|
||||
|
||||
|
||||
// -------------------- Admin Exports --------------------
|
||||
|
||||
|
||||
// Display home management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
// Fetch Home data
|
||||
let data = await getHomeData();
|
||||
|
||||
// If no data exists, use default
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
data = getDefaultHomeData();
|
||||
} else {
|
||||
// Merge with defaults to ensure all fields exist
|
||||
const defaultData = getDefaultHomeData();
|
||||
|
||||
// Ensure all sections exist with defaults
|
||||
data.hero = data.hero || defaultData.hero;
|
||||
data.about = data.about || defaultData.about;
|
||||
data.missionVision = data.missionVision || defaultData.missionVision;
|
||||
data.whyChooseUs = data.whyChooseUs || defaultData.whyChooseUs;
|
||||
data.activities = data.activities || defaultData.activities;
|
||||
data.faq = data.faq || defaultData.faq;
|
||||
data.partners = data.partners || defaultData.partners;
|
||||
data.programs = data.programs || defaultData.programs;
|
||||
data.newsletter = data.newsletter || defaultData.newsletter;
|
||||
data.latestPosts = data.latestPosts || defaultData.latestPosts;
|
||||
// Merge minimal defaults to keep the view safe
|
||||
const defaults = getDefaultHomeData();
|
||||
data.hero = data.hero || defaults.hero;
|
||||
data.about = data.about || defaults.about;
|
||||
data.missionVision = data.missionVision || defaults.missionVision;
|
||||
data.whyChooseUs = data.whyChooseUs || defaults.whyChooseUs;
|
||||
data.activities = data.activities || defaults.activities;
|
||||
data.faq = data.faq || defaults.faq;
|
||||
data.partners = data.partners || defaults.partners;
|
||||
data.programs = data.programs || defaults.programs;
|
||||
data.newsletter = data.newsletter || defaults.newsletter;
|
||||
data.latestPosts = data.latestPosts || defaults.latestPosts;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||
|
||||
res.render('admin/home/index', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Home Management',
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
return res.render("admin/home/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Home Management",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Home index error:', err);
|
||||
req.flash('error_msg', 'Error loading home data');
|
||||
res.redirect('/admin/dashboard');
|
||||
console.error("Home index error:", err);
|
||||
req.flash("error_msg", "Error loading home data");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
};
|
||||
|
||||
// Update home data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
// Get current data
|
||||
const currentData = await getHomeData();
|
||||
|
||||
// Create updated data object
|
||||
const updatedData = { ...(currentData.toObject ? currentData.toObject() : currentData) };
|
||||
|
||||
// Update Hero section data (from Welcome tab)
|
||||
const currentDoc = await getHomeDoc();
|
||||
const currentData = currentDoc ? currentDoc.toObject() : {};
|
||||
const updatedData = { ...currentData };
|
||||
|
||||
// Quick fields (Hero) from classic form fields
|
||||
if (req.body.heroTitle || req.body.heroDescription || req.body.heroBackgroundImage) {
|
||||
updatedData.hero = {
|
||||
title: req.body.heroTitle || '',
|
||||
description: req.body.heroDescription || '',
|
||||
backgroundImage: req.body.heroBackgroundImage || '',
|
||||
title: req.body.heroTitle || "",
|
||||
description: req.body.heroDescription || "",
|
||||
backgroundImage: req.body.heroBackgroundImage || "",
|
||||
button: {
|
||||
label: req.body.heroButtonLabel || 'Book Your Adventure',
|
||||
href: req.body.heroButtonHref || '/booking'
|
||||
label: req.body.heroButtonLabel || "Book Your Adventure",
|
||||
href: req.body.heroButtonHref || "/booking",
|
||||
},
|
||||
contactBox: {
|
||||
welcomeText: req.body.heroContactWelcome || '',
|
||||
welcomeText: req.body.heroContactWelcome || "",
|
||||
phone: {
|
||||
label: 'Call us',
|
||||
number: req.body.heroContactPhone || '',
|
||||
href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : ''
|
||||
label: "Call us",
|
||||
number: req.body.heroContactPhone || "",
|
||||
href: req.body.heroContactPhone ? `tel:${req.body.heroContactPhone}` : "",
|
||||
},
|
||||
email: {
|
||||
label: 'Email',
|
||||
address: req.body.heroContactEmail || '',
|
||||
href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : ''
|
||||
label: "Email",
|
||||
address: req.body.heroContactEmail || "",
|
||||
href: req.body.heroContactEmail ? `mailto:${req.body.heroContactEmail}` : "",
|
||||
},
|
||||
workingHours: {
|
||||
label: 'Working Hours',
|
||||
hours: req.body.heroContactHours || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Update Why Choose Us section
|
||||
if (req.body.whyChooseUsTitle || req.body.whyChooseUsSubtitle) {
|
||||
updatedData.whyChooseUs = {
|
||||
...(updatedData.whyChooseUs || {}),
|
||||
title: req.body.whyChooseUsTitle || '',
|
||||
subtitle: req.body.whyChooseUsSubtitle || '',
|
||||
description: req.body.whyChooseUsDescription || '',
|
||||
button: {
|
||||
label: req.body.whyChooseUsButtonLabel || '',
|
||||
href: req.body.whyChooseUsButtonHref || ''
|
||||
label: "Working Hours",
|
||||
hours: req.body.heroContactHours || "",
|
||||
},
|
||||
},
|
||||
features: updatedData.whyChooseUs?.features || [],
|
||||
tags: updatedData.whyChooseUs?.tags || [],
|
||||
cta: updatedData.whyChooseUs?.cta || { text: '', linkText: '', linkHref: '' }
|
||||
};
|
||||
}
|
||||
|
||||
// Handle Home sections (new camp structure only)
|
||||
const sections = ['hero', 'about', 'missionVision', 'whyChooseUs', 'activities', 'faq',
|
||||
'partners', 'programs', 'newsletter', 'latestPosts'];
|
||||
const errors = [];
|
||||
|
||||
// Handle sections sent as JSON payloads
|
||||
const sections = [
|
||||
"hero",
|
||||
"about",
|
||||
"missionVision",
|
||||
"whyChooseUs",
|
||||
"activities",
|
||||
"faq",
|
||||
"partners",
|
||||
"programs",
|
||||
"newsletter",
|
||||
"latestPosts",
|
||||
];
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
// Process each section
|
||||
|
||||
for (const section of sections) {
|
||||
if (!req.body[section]) continue;
|
||||
|
||||
try {
|
||||
if (!req.body[section]) {
|
||||
console.warn(`No data for section: ${section}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse JSON data from form
|
||||
const newSectionData = JSON.parse(req.body[section]);
|
||||
|
||||
// Check for changes
|
||||
const currentSectionData = currentData[section];
|
||||
const sectionHasChanges = JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData);
|
||||
|
||||
if (sectionHasChanges) {
|
||||
const currentSectionData = currentData?.[section];
|
||||
|
||||
if (JSON.stringify(newSectionData) !== JSON.stringify(currentSectionData)) {
|
||||
updatedData[section] = newSectionData;
|
||||
hasChanges = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing section ${section}:`, error);
|
||||
errors.push(`Error processing ${section} data: ${error.message}`);
|
||||
} catch (e) {
|
||||
console.error(`Error processing section "${section}":`, e);
|
||||
req.flash("error_msg", `Invalid JSON for section "${section}": ${e.message}`);
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if (errors.length > 0) {
|
||||
req.flash('error_msg', `Data processing error: ${errors[0]}`);
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
}
|
||||
|
||||
// Check if there are changes
|
||||
|
||||
if (!hasChanges) {
|
||||
req.flash('info_msg', 'No changes were made');
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
req.flash("info_msg", "No changes were made");
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
}
|
||||
|
||||
// Update or create document
|
||||
try {
|
||||
if (currentData._id) {
|
||||
await Home.findByIdAndUpdate(currentData._id, updatedData, { new: true });
|
||||
} else {
|
||||
await Home.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash('success_msg', 'Home data updated successfully');
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
} catch (dbError) {
|
||||
console.error('Database error:', dbError);
|
||||
req.flash('error_msg', `Database error: ${dbError.message || 'Unknown'}`);
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
|
||||
if (currentDoc?._id) {
|
||||
await Home.findByIdAndUpdate(currentDoc._id, updatedData, { new: true });
|
||||
} else {
|
||||
await Home.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Home data updated successfully");
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
} catch (err) {
|
||||
console.error('Update error:', err);
|
||||
req.flash('error_msg', `Update error: ${err.message || 'Unknown'}`);
|
||||
return req.session.save(() => res.redirect('/admin/home'));
|
||||
console.error("Home update error:", err);
|
||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
||||
return req.session.save(() => res.redirect("/admin/home"));
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Public API Exports --------------------
|
||||
// -------------------- Public API --------------------
|
||||
|
||||
// API to get home data for frontend
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const homeData = await getHomeData();
|
||||
|
||||
|
||||
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(homeData, baseUrl);
|
||||
|
||||
res.json(processedData);
|
||||
return res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error('Home API error:', err);
|
||||
res.status(500).json({ error: 'Error loading home data' });
|
||||
console.error("Home API error:", err);
|
||||
return res.status(500).json({ error: "Error loading home data" });
|
||||
}
|
||||
};
|
||||
|
||||
// API to get hero data for frontend
|
||||
exports.apiHero = async (req, res) => {
|
||||
try {
|
||||
const homeData = await getHomeData();
|
||||
const heroData = homeData?.hero;
|
||||
|
||||
if (!heroData) {
|
||||
return res.status(404).json({
|
||||
error: 'Hero data not found',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedData = addBaseUrlToImages(heroData, baseUrl);
|
||||
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
console.error('Hero API error:', err);
|
||||
res.status(500).json({ error: 'Error loading hero data' });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user