forked from UKSOURCE/cms.hailearning.edu.vn
554 lines
14 KiB
JavaScript
554 lines
14 KiB
JavaScript
const Blog = require('../models/blog');
|
|
const BlogCategory = require('../models/blogCategory');
|
|
const BlogTag = require('../models/blogTag');
|
|
const BlogComment = require('../models/blogComment');
|
|
const RecentPost = require('../models/recentPost');
|
|
const { addBaseUrlToImages } = require('../utils/imageHelper');
|
|
|
|
// -------------------- Helper Functions --------------------
|
|
|
|
// Generate slug from title
|
|
const generateSlug = (title) => {
|
|
return title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.trim('-');
|
|
};
|
|
|
|
// Update category post counts
|
|
const updateCategoryPostCounts = async () => {
|
|
const categories = await BlogCategory.find();
|
|
for (const category of categories) {
|
|
await category.updatePostCount();
|
|
}
|
|
};
|
|
|
|
// Update tag post counts
|
|
const updateTagPostCounts = async () => {
|
|
const tags = await BlogTag.find();
|
|
for (const tag of tags) {
|
|
await tag.updatePostCount();
|
|
}
|
|
};
|
|
|
|
// -------------------- Admin Controllers --------------------
|
|
|
|
// Display blog management page
|
|
exports.index = async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
// Build filter
|
|
const filter = {};
|
|
if (req.query.status) {
|
|
filter.status = req.query.status;
|
|
}
|
|
if (req.query.category) {
|
|
filter.category = req.query.category;
|
|
}
|
|
if (req.query.search) {
|
|
filter.$or = [
|
|
{ title: { $regex: req.query.search, $options: 'i' } },
|
|
{ excerpt: { $regex: req.query.search, $options: 'i' } }
|
|
];
|
|
}
|
|
|
|
// Get blogs with pagination
|
|
const blogs = await Blog.find(filter)
|
|
.sort({ createdAt: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.lean();
|
|
|
|
const totalBlogs = await Blog.countDocuments(filter);
|
|
const totalPages = Math.ceil(totalBlogs / limit);
|
|
|
|
// Get categories for filter
|
|
const categories = await BlogCategory.getActive();
|
|
|
|
res.render('admin/blog/index', {
|
|
layout: 'layouts/main',
|
|
title: 'Blog Management',
|
|
blogs,
|
|
categories,
|
|
pagination: {
|
|
current: page,
|
|
total: totalPages,
|
|
limit,
|
|
totalItems: totalBlogs
|
|
},
|
|
query: req.query,
|
|
currentPath: req.path,
|
|
user: req.session.user
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog index error:', err);
|
|
req.flash('error_msg', 'Error loading blogs');
|
|
res.redirect('/admin/dashboard');
|
|
}
|
|
};
|
|
|
|
// Show create blog form
|
|
exports.create = async (req, res) => {
|
|
try {
|
|
const categories = await BlogCategory.getActive();
|
|
const tags = await BlogTag.getActive();
|
|
|
|
res.render('admin/blog/create', {
|
|
layout: 'layouts/main',
|
|
title: 'Create New Blog Post',
|
|
categories,
|
|
tags,
|
|
currentPath: req.path,
|
|
user: req.session.user
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog create form error:', err);
|
|
req.flash('error_msg', 'Error loading create form');
|
|
res.redirect('/admin/blog');
|
|
}
|
|
};
|
|
|
|
// Store new blog
|
|
exports.store = async (req, res) => {
|
|
try {
|
|
const {
|
|
title,
|
|
excerpt,
|
|
content,
|
|
category,
|
|
tags,
|
|
status,
|
|
isFeatured,
|
|
author,
|
|
galleryImages
|
|
} = req.body;
|
|
|
|
// Generate slug
|
|
const slug = generateSlug(title);
|
|
|
|
// Check if slug exists
|
|
const existingBlog = await Blog.findOne({ slug });
|
|
if (existingBlog) {
|
|
req.flash('error_msg', 'A blog post with this title already exists');
|
|
return res.redirect('/admin/blog/create');
|
|
}
|
|
|
|
// Create blog data
|
|
const blogData = {
|
|
title,
|
|
slug,
|
|
excerpt,
|
|
content,
|
|
category: category ? (Array.isArray(category) ? category : [category]) : [], // Array categories
|
|
tags: tags ? (Array.isArray(tags) ? tags : [tags]) : [],
|
|
status: status || 'published',
|
|
isFeatured: isFeatured === 'on',
|
|
author: author || 'Admin',
|
|
galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : []
|
|
};
|
|
|
|
// Handle featured image
|
|
if (req.file) {
|
|
blogData.featuredImage = `/uploads/blog/${req.file.filename}`;
|
|
}
|
|
|
|
// Create blog
|
|
const blog = new Blog(blogData);
|
|
await blog.save();
|
|
|
|
// Update counts
|
|
await updateCategoryPostCounts();
|
|
await updateTagPostCounts();
|
|
await RecentPost.syncFromBlogs();
|
|
|
|
req.flash('success_msg', 'Blog post created successfully');
|
|
res.redirect('/admin/blog');
|
|
} catch (err) {
|
|
console.error('Blog store error:', err);
|
|
req.flash('error_msg', 'Error creating blog post');
|
|
res.redirect('/admin/blog/create');
|
|
}
|
|
};
|
|
|
|
// Show edit blog form
|
|
exports.edit = async (req, res) => {
|
|
try {
|
|
const blog = await Blog.findById(req.params.id);
|
|
|
|
if (!blog) {
|
|
req.flash('error_msg', 'Blog post not found');
|
|
return res.redirect('/admin/blog');
|
|
}
|
|
|
|
const categories = await BlogCategory.getActive();
|
|
const tags = await BlogTag.getActive();
|
|
|
|
res.render('admin/blog/edit', {
|
|
layout: 'layouts/main',
|
|
title: 'Edit Blog Post',
|
|
blog,
|
|
categories,
|
|
tags,
|
|
currentPath: req.path,
|
|
user: req.session.user
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog edit form error:', err);
|
|
req.flash('error_msg', 'Error loading blog post');
|
|
res.redirect('/admin/blog');
|
|
}
|
|
};
|
|
|
|
// Update blog
|
|
exports.update = async (req, res) => {
|
|
try {
|
|
const blog = await Blog.findById(req.params.id);
|
|
|
|
if (!blog) {
|
|
req.flash('error_msg', 'Blog post not found');
|
|
return res.redirect('/admin/blog');
|
|
}
|
|
|
|
const {
|
|
title,
|
|
excerpt,
|
|
content,
|
|
category,
|
|
tags,
|
|
status,
|
|
isFeatured,
|
|
author,
|
|
galleryImages
|
|
} = req.body;
|
|
|
|
// Update blog data
|
|
blog.title = title;
|
|
blog.excerpt = excerpt;
|
|
blog.content = content;
|
|
blog.category = category ? (Array.isArray(category) ? category : [category]) : []; // Array categories
|
|
blog.tags = tags ? (Array.isArray(tags) ? tags : [tags]) : [];
|
|
blog.status = status || 'published';
|
|
blog.isFeatured = isFeatured === 'on';
|
|
blog.author = author || 'Admin';
|
|
blog.galleryImages = galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [];
|
|
|
|
// Handle featured image
|
|
if (req.file) {
|
|
blog.featuredImage = `/uploads/blog/${req.file.filename}`;
|
|
}
|
|
|
|
// Generate new slug if title changed
|
|
const newSlug = generateSlug(title);
|
|
if (newSlug !== blog.slug) {
|
|
const existingBlog = await Blog.findOne({ slug: newSlug, _id: { $ne: blog._id } });
|
|
if (existingBlog) {
|
|
req.flash('error_msg', 'A blog post with this title already exists');
|
|
return res.redirect(`/admin/blog/${blog._id}/edit`);
|
|
}
|
|
blog.slug = newSlug;
|
|
}
|
|
|
|
await blog.save();
|
|
|
|
// Update counts
|
|
await updateCategoryPostCounts();
|
|
await updateTagPostCounts();
|
|
await RecentPost.syncFromBlogs();
|
|
|
|
req.flash('success_msg', 'Blog post updated successfully');
|
|
res.redirect('/admin/blog');
|
|
} catch (err) {
|
|
console.error('Blog update error:', err);
|
|
req.flash('error_msg', 'Error updating blog post');
|
|
res.redirect(`/admin/blog/${req.params.id}/edit`);
|
|
}
|
|
};
|
|
|
|
// Delete blog
|
|
exports.destroy = async (req, res) => {
|
|
try {
|
|
const blog = await Blog.findById(req.params.id);
|
|
|
|
if (!blog) {
|
|
req.flash('error_msg', 'Blog post not found');
|
|
return res.redirect('/admin/blog');
|
|
}
|
|
|
|
await Blog.findByIdAndDelete(req.params.id);
|
|
|
|
// Update counts
|
|
await updateCategoryPostCounts();
|
|
await updateTagPostCounts();
|
|
await RecentPost.syncFromBlogs();
|
|
|
|
req.flash('success_msg', 'Blog post deleted successfully');
|
|
res.redirect('/admin/blog');
|
|
} catch (err) {
|
|
console.error('Blog delete error:', err);
|
|
req.flash('error_msg', 'Error deleting blog post');
|
|
res.redirect('/admin/blog');
|
|
}
|
|
};
|
|
|
|
// -------------------- Public API Controllers --------------------
|
|
|
|
// Get all published blogs for frontend
|
|
exports.api = async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
const skip = (page - 1) * limit;
|
|
|
|
// Build filter
|
|
const filter = { status: 'published' };
|
|
|
|
if (req.query.category) {
|
|
filter.category = { $in: [req.query.category] }; // Tìm trong array categories
|
|
}
|
|
|
|
if (req.query.tag) {
|
|
filter.tags = { $in: [req.query.tag] }; // Tìm trong array tags
|
|
}
|
|
|
|
if (req.query.search) {
|
|
filter.$or = [
|
|
{ title: { $regex: req.query.search, $options: 'i' } },
|
|
{ excerpt: { $regex: req.query.search, $options: 'i' } }
|
|
];
|
|
}
|
|
|
|
// Get blogs
|
|
const blogs = await Blog.find(filter)
|
|
.sort({ createdAt: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.lean();
|
|
|
|
const totalBlogs = await Blog.countDocuments(filter);
|
|
|
|
// Add base URL to images
|
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
|
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Blogs fetched successfully',
|
|
data: {
|
|
blogs: processedBlogs,
|
|
pagination: {
|
|
current: page,
|
|
total: Math.ceil(totalBlogs / limit),
|
|
limit,
|
|
totalItems: totalBlogs
|
|
}
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog API error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error loading blogs',
|
|
error: err.message || 'Error loading blogs'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get single blog by slug
|
|
exports.apiShow = async (req, res) => {
|
|
try {
|
|
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'
|
|
});
|
|
}
|
|
|
|
// Get comments for this post
|
|
const comments = await BlogComment.getApprovedByPost(blog._id);
|
|
|
|
// Add comments to blog
|
|
blog.comments = comments;
|
|
|
|
// Add base URL to images
|
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
|
const processedBlog = addBaseUrlToImages(blog, baseUrl);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Blog post fetched successfully',
|
|
data: processedBlog
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog show API error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error loading blog post',
|
|
error: err.message || 'Error loading blog post'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get featured blogs
|
|
exports.apiFeatured = async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 3;
|
|
|
|
const blogs = await Blog.getFeatured()
|
|
.limit(limit)
|
|
.lean();
|
|
|
|
// Add base URL to images
|
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
|
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Featured blogs fetched successfully',
|
|
data: processedBlogs
|
|
});
|
|
} catch (err) {
|
|
console.error('Featured blogs API error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error loading featured blogs',
|
|
error: err.message || 'Error loading featured blogs'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get recent blogs
|
|
exports.apiRecent = async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 5;
|
|
|
|
// Try to get from RecentPost first
|
|
let recentPosts = await RecentPost.getRecent(limit);
|
|
|
|
// If no recent posts, sync from blogs
|
|
if (recentPosts.length === 0) {
|
|
await RecentPost.syncFromBlogs(limit);
|
|
recentPosts = await RecentPost.getRecent(limit);
|
|
}
|
|
|
|
// Add base URL to images
|
|
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
|
const processedPosts = recentPosts.map(post => addBaseUrlToImages(post, baseUrl));
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Recent blogs fetched successfully',
|
|
data: processedPosts
|
|
});
|
|
} catch (err) {
|
|
console.error('Recent blogs API error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error loading recent blogs',
|
|
error: err.message || 'Error loading recent blogs'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get categories of a specific blog post
|
|
exports.apiCategories = async (req, res) => {
|
|
try {
|
|
const mongoose = require('mongoose');
|
|
let query;
|
|
|
|
// Check if it's a valid ObjectId
|
|
if (mongoose.Types.ObjectId.isValid(req.params.id)) {
|
|
query = { _id: req.params.id };
|
|
} else {
|
|
query = { slug: req.params.id };
|
|
}
|
|
|
|
query.status = 'published';
|
|
|
|
const blog = await Blog.findOne(query).lean();
|
|
|
|
if (!blog) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
// Get category details
|
|
const BlogCategory = require('../models/blogCategory');
|
|
const categories = await BlogCategory.find({
|
|
name: { $in: blog.category },
|
|
isActive: true
|
|
}).lean();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Blog categories fetched successfully',
|
|
data: categories
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog categories API error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error loading blog categories',
|
|
error: err.message || 'Error loading blog categories'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get tags of a specific blog post
|
|
exports.apiTags = async (req, res) => {
|
|
try {
|
|
const mongoose = require('mongoose');
|
|
let query;
|
|
|
|
// Check if it's a valid ObjectId
|
|
if (mongoose.Types.ObjectId.isValid(req.params.id)) {
|
|
query = { _id: req.params.id };
|
|
} else {
|
|
query = { slug: req.params.id };
|
|
}
|
|
|
|
query.status = 'published';
|
|
|
|
const blog = await Blog.findOne(query).lean();
|
|
|
|
if (!blog) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: 'Blog post not found'
|
|
});
|
|
}
|
|
|
|
// Get tag details
|
|
const BlogTag = require('../models/blogTag');
|
|
const tags = await BlogTag.find({
|
|
name: { $in: blog.tags },
|
|
isActive: true
|
|
}).lean();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Blog tags fetched successfully',
|
|
data: tags
|
|
});
|
|
} catch (err) {
|
|
console.error('Blog tags API error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Error loading blog tags',
|
|
error: err.message || 'Error loading blog tags'
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = exports; |