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'); const slugify = require('slugify'); // -------------------- Helper Functions -------------------- // Generate slug from title const generateSlug = (title) => { return slugify(title, { lower: true, strict: true, locale: 'vi' }); }; // 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(); 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, 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(); 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, frontendUrl }); } 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, quote, contentAfterQuote } = 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]) : [], quote: quote || '', contentAfterQuote: contentAfterQuote || '' }; // Handle featured image - using featuredImageUrl from form (uploaded via AJAX) if (req.body.featuredImageUrl) { blogData.featuredImage = req.body.featuredImageUrl; } // 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(); const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; res.render('admin/blog/edit', { layout: 'layouts/main', title: 'Edit Blog Post', blog, categories, tags, currentPath: req.path, user: req.session.user, frontendUrl }); } 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, quote, contentAfterQuote } = 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]) : []; blog.quote = quote || ''; blog.contentAfterQuote = contentAfterQuote || ''; // 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 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 (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 = 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')}`; 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' }); } }; // 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 { 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('port')}`; 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;