feat:Add blog management page and enhance admin layout

This commit is contained in:
Wini_Fy
2026-02-03 17:04:28 +07:00
parent bb539ef213
commit 538317eade
14 changed files with 3096 additions and 277 deletions

View File

@@ -4,17 +4,17 @@ const BlogTag = require('../models/blogTag');
const BlogComment = require('../models/blogComment');
const RecentPost = require('../models/recentPost');
const { addBaseUrlToImages } = require('../utils/imageHelper');
const slugify = require('slugify');
// -------------------- Helper Functions --------------------
// Generate slug from title
const generateSlug = (title) => {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
return slugify(title, {
lower: true,
strict: true,
locale: 'vi'
});
};
// Update category post counts
@@ -69,12 +69,15 @@ exports.index = async (req, res) => {
// Get categories for filter
const categories = await BlogCategory.getActive();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/blog/index', {
layout: 'layouts/main',
title: 'Blog Management',
blogs,
categories,
frontendUrl,
pagination: {
current: page,
total: totalPages,
@@ -98,13 +101,16 @@ exports.create = async (req, res) => {
const categories = await BlogCategory.getActive();
const tags = await BlogTag.getActive();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/blog/create', {
layout: 'layouts/main',
title: 'Create New Blog Post',
categories,
tags,
currentPath: req.path,
user: req.session.user
user: req.session.user,
frontendUrl
});
} catch (err) {
console.error('Blog create form error:', err);
@@ -125,7 +131,9 @@ exports.store = async (req, res) => {
status,
isFeatured,
author,
galleryImages
galleryImages,
quote,
contentAfterQuote
} = req.body;
// Generate slug
@@ -149,12 +157,14 @@ exports.store = async (req, res) => {
status: status || 'published',
isFeatured: isFeatured === 'on',
author: author || 'Admin',
galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : []
galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [],
quote: quote || '',
contentAfterQuote: contentAfterQuote || ''
};
// Handle featured image
if (req.file) {
blogData.featuredImage = `/uploads/blog/${req.file.filename}`;
// Handle featured image - using featuredImageUrl from form (uploaded via AJAX)
if (req.body.featuredImageUrl) {
blogData.featuredImage = req.body.featuredImageUrl;
}
// Create blog
@@ -188,6 +198,8 @@ exports.edit = async (req, res) => {
const categories = await BlogCategory.getActive();
const tags = await BlogTag.getActive();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.render('admin/blog/edit', {
layout: 'layouts/main',
title: 'Edit Blog Post',
@@ -195,7 +207,8 @@ exports.edit = async (req, res) => {
categories,
tags,
currentPath: req.path,
user: req.session.user
user: req.session.user,
frontendUrl
});
} catch (err) {
console.error('Blog edit form error:', err);
@@ -223,7 +236,9 @@ exports.update = async (req, res) => {
status,
isFeatured,
author,
galleryImages
galleryImages,
quote,
contentAfterQuote
} = req.body;
// Update blog data
@@ -236,10 +251,12 @@ exports.update = async (req, res) => {
blog.isFeatured = isFeatured === 'on';
blog.author = author || 'Admin';
blog.galleryImages = galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [];
blog.quote = quote || '';
blog.contentAfterQuote = contentAfterQuote || '';
// Handle featured image
if (req.file) {
blog.featuredImage = `/uploads/blog/${req.file.filename}`;
// Handle featured image - using featuredImageUrl from form (uploaded via AJAX)
if (req.body.featuredImageUrl) {
blog.featuredImage = req.body.featuredImageUrl;
}
// Generate new slug if title changed
@@ -373,11 +390,30 @@ exports.apiShow = async (req, res) => {
});
}
// Get comments for this post
const comments = await BlogComment.getApprovedByPost(blog._id);
// Get comments for this post (parent comments only)
const parentComments = await BlogComment.getApprovedByPost(blog._id);
// Get replies for each parent comment
const commentsWithReplies = await Promise.all(
parentComments.map(async (parentComment) => {
const replies = await BlogComment.getReplies(parentComment._id);
return {
...parentComment.toObject(),
replies: replies.map(reply => reply.toObject())
};
})
);
// Flatten comments array (parent + replies)
const allComments = commentsWithReplies.flatMap(comment => [
comment,
...comment.replies
]);
// Add comments to blog
blog.comments = comments;
blog.comments = allComments;
// Keep commentsCount in sync for frontend
blog.commentsCount = allComments.length;
// Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
@@ -398,6 +434,72 @@ exports.apiShow = async (req, res) => {
}
};
// Create a comment (no moderation for now: default approved)
exports.apiCreateComment = async (req, res) => {
try {
const { authorName, content, parentId } = req.body || {};
if (!authorName || !String(authorName).trim()) {
return res.status(400).json({
success: false,
message: "authorName is required",
});
}
if (!content || !String(content).trim()) {
return res.status(400).json({
success: false,
message: "content is required",
});
}
const blog = await Blog.findOne({ slug: req.params.slug, status: "published" }).lean();
if (!blog) {
return res.status(404).json({
success: false,
message: "Blog post not found",
});
}
// If replying, ensure parent exists and belongs to same post
let parentObjectId = null;
if (parentId) {
const parent = await BlogComment.findOne({ _id: parentId, postId: blog._id }).lean();
if (!parent) {
return res.status(400).json({
success: false,
message: "Invalid parentId",
});
}
parentObjectId = parentId;
}
const newComment = await BlogComment.create({
postId: blog._id,
authorName: String(authorName).trim(),
content: String(content).trim(),
parentId: parentObjectId,
status: "approved",
});
// Keep counter roughly correct (also counts replies)
await Blog.updateOne({ _id: blog._id }, { $inc: { commentsCount: 1 } });
return res.json({
success: true,
message: "Comment created successfully",
data: newComment.toJSON(),
});
} catch (err) {
console.error("Create comment API error:", err);
return res.status(500).json({
success: false,
message: "Error creating comment",
error: err.message || "Error creating comment",
});
}
};
// Get featured blogs
exports.apiFeatured = async (req, res) => {
try {
@@ -408,7 +510,7 @@ exports.apiFeatured = async (req, res) => {
.lean();
// Add base URL to images
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('port')}`;
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
res.json({