From f531cc4a92df69854302de400de3b89c24190d32 Mon Sep 17 00:00:00 2001 From: Wini_Fy Date: Mon, 2 Feb 2026 17:01:42 +0700 Subject: [PATCH] feat: Add blog category, blog, and tag controllers with CRUD operations --- controllers/blogCategoryController.js | 231 +++++++++++ controllers/blogController.js | 554 ++++++++++++++++++++++++++ controllers/blogTagController.js | 239 +++++++++++ 3 files changed, 1024 insertions(+) create mode 100644 controllers/blogCategoryController.js create mode 100644 controllers/blogController.js create mode 100644 controllers/blogTagController.js diff --git a/controllers/blogCategoryController.js b/controllers/blogCategoryController.js new file mode 100644 index 0000000..4043d22 --- /dev/null +++ b/controllers/blogCategoryController.js @@ -0,0 +1,231 @@ +const BlogCategory = require('../models/blogCategory'); + +// -------------------- Admin Controllers -------------------- + +// Display category management page +exports.index = async (req, res) => { + try { + const categories = await BlogCategory.find() + .sort({ name: 1 }) + .lean(); + + res.render('admin/blog/categories/index', { + layout: 'layouts/main', + title: 'Blog Categories', + categories, + currentPath: req.path, + user: req.session.user + }); + } catch (err) { + console.error('Category index error:', err); + req.flash('error_msg', 'Error loading categories'); + res.redirect('/admin/dashboard'); + } +}; + +// Show create category form +exports.create = async (req, res) => { + try { + res.render('admin/blog/categories/create', { + layout: 'layouts/main', + title: 'Create New Category', + currentPath: req.path, + user: req.session.user + }); + } catch (err) { + console.error('Category create form error:', err); + req.flash('error_msg', 'Error loading create form'); + res.redirect('/admin/blog/categories'); + } +}; + +// Store new category +exports.store = async (req, res) => { + try { + const { + name, + description, + isActive + } = req.body; + + // Generate slug + const slug = name + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim('-'); + + // Check if slug exists + const existingCategory = await BlogCategory.findOne({ slug }); + if (existingCategory) { + req.flash('error_msg', 'A category with this name already exists'); + return res.redirect('/admin/blog/categories/create'); + } + + // Create category data + const categoryData = { + name, + slug, + description, + isActive: isActive === 'on' + }; + + // Create category + const category = new BlogCategory(categoryData); + await category.save(); + + req.flash('success_msg', 'Category created successfully'); + res.redirect('/admin/blog/categories'); + } catch (err) { + console.error('Category store error:', err); + req.flash('error_msg', 'Error creating category'); + res.redirect('/admin/blog/categories/create'); + } +}; + +// Show edit category form +exports.edit = async (req, res) => { + try { + const category = await BlogCategory.findById(req.params.id); + + if (!category) { + req.flash('error_msg', 'Category not found'); + return res.redirect('/admin/blog/categories'); + } + + res.render('admin/blog/categories/edit', { + layout: 'layouts/main', + title: 'Edit Category', + category, + currentPath: req.path, + user: req.session.user + }); + } catch (err) { + console.error('Category edit form error:', err); + req.flash('error_msg', 'Error loading category'); + res.redirect('/admin/blog/categories'); + } +}; + +// Update category +exports.update = async (req, res) => { + try { + const category = await BlogCategory.findById(req.params.id); + + if (!category) { + req.flash('error_msg', 'Category not found'); + return res.redirect('/admin/blog/categories'); + } + + const { + name, + description, + isActive + } = req.body; + + // Update category data + category.name = name; + category.description = description; + category.isActive = isActive === 'on'; + + // Generate new slug if name changed + const newSlug = name + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim('-'); + + if (newSlug !== category.slug) { + const existingCategory = await BlogCategory.findOne({ + slug: newSlug, + _id: { $ne: category._id } + }); + if (existingCategory) { + req.flash('error_msg', 'A category with this name already exists'); + return res.redirect(`/admin/blog/categories/${category._id}/edit`); + } + category.slug = newSlug; + } + + await category.save(); + + req.flash('success_msg', 'Category updated successfully'); + res.redirect('/admin/blog/categories'); + } catch (err) { + console.error('Category update error:', err); + req.flash('error_msg', 'Error updating category'); + res.redirect(`/admin/blog/categories/${req.params.id}/edit`); + } +}; + +// Delete category +exports.destroy = async (req, res) => { + try { + const category = await BlogCategory.findById(req.params.id); + + if (!category) { + req.flash('error_msg', 'Category not found'); + return res.redirect('/admin/blog/categories'); + } + + // Check if category has posts + const Blog = require('../models/blog'); + const postCount = await Blog.countDocuments({ category: { $in: [category.name] } }); + + if (postCount > 0) { + req.flash('error_msg', 'Cannot delete category that has blog posts'); + return res.redirect('/admin/blog/categories'); + } + + await BlogCategory.findByIdAndDelete(req.params.id); + + req.flash('success_msg', 'Category deleted successfully'); + res.redirect('/admin/blog/categories'); + } catch (err) { + console.error('Category delete error:', err); + req.flash('error_msg', 'Error deleting category'); + res.redirect('/admin/blog/categories'); + } +}; + +// -------------------- Public API Controllers -------------------- + +// Get all active categories +exports.api = async (req, res) => { + try { + const categories = await BlogCategory.getActive(); + + // Update post counts + for (const category of categories) { + await category.updatePostCount(); + } + + res.json(categories); + } catch (err) { + console.error('Categories API error:', err); + res.status(500).json({ error: 'Error loading categories' }); + } +}; + +// Get category by slug +exports.apiShow = async (req, res) => { + try { + const category = await BlogCategory.findOne({ + slug: req.params.slug, + isActive: true + }).lean(); + + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + + res.json(category); + } catch (err) { + console.error('Category show API error:', err); + res.status(500).json({ error: 'Error loading category' }); + } +}; + +module.exports = exports; \ No newline at end of file diff --git a/controllers/blogController.js b/controllers/blogController.js new file mode 100644 index 0000000..bc02bc4 --- /dev/null +++ b/controllers/blogController.js @@ -0,0 +1,554 @@ +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; \ No newline at end of file diff --git a/controllers/blogTagController.js b/controllers/blogTagController.js new file mode 100644 index 0000000..f4232ef --- /dev/null +++ b/controllers/blogTagController.js @@ -0,0 +1,239 @@ +const BlogTag = require('../models/blogTag'); + +// -------------------- Admin Controllers -------------------- + +// Display tag management page +exports.index = async (req, res) => { + try { + const tags = await BlogTag.find() + .sort({ name: 1 }) + .lean(); + + res.render('admin/blog/tags/index', { + layout: 'layouts/main', + title: 'Blog Tags', + tags, + currentPath: req.path, + user: req.session.user + }); + } catch (err) { + console.error('Tag index error:', err); + req.flash('error_msg', 'Error loading tags'); + res.redirect('/admin/dashboard'); + } +}; + +// Show create tag form +exports.create = async (req, res) => { + try { + res.render('admin/blog/tags/create', { + layout: 'layouts/main', + title: 'Create New Tag', + currentPath: req.path, + user: req.session.user + }); + } catch (err) { + console.error('Tag create form error:', err); + req.flash('error_msg', 'Error loading create form'); + res.redirect('/admin/blog/tags'); + } +}; + +// Store new tag +exports.store = async (req, res) => { + try { + const { + name, + isActive + } = req.body; + + // Generate slug + const slug = name + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim('-'); + + // Check if slug exists + const existingTag = await BlogTag.findOne({ slug }); + if (existingTag) { + req.flash('error_msg', 'A tag with this name already exists'); + return res.redirect('/admin/blog/tags/create'); + } + + // Create tag data + const tagData = { + name, + slug, + isActive: isActive === 'on' + }; + + // Create tag + const tag = new BlogTag(tagData); + await tag.save(); + + req.flash('success_msg', 'Tag created successfully'); + res.redirect('/admin/blog/tags'); + } catch (err) { + console.error('Tag store error:', err); + req.flash('error_msg', 'Error creating tag'); + res.redirect('/admin/blog/tags/create'); + } +}; + +// Show edit tag form +exports.edit = async (req, res) => { + try { + const tag = await BlogTag.findById(req.params.id); + + if (!tag) { + req.flash('error_msg', 'Tag not found'); + return res.redirect('/admin/blog/tags'); + } + + res.render('admin/blog/tags/edit', { + layout: 'layouts/main', + title: 'Edit Tag', + tag, + currentPath: req.path, + user: req.session.user + }); + } catch (err) { + console.error('Tag edit form error:', err); + req.flash('error_msg', 'Error loading tag'); + res.redirect('/admin/blog/tags'); + } +}; + +// Update tag +exports.update = async (req, res) => { + try { + const tag = await BlogTag.findById(req.params.id); + + if (!tag) { + req.flash('error_msg', 'Tag not found'); + return res.redirect('/admin/blog/tags'); + } + + const { + name, + isActive + } = req.body; + + // Update tag data + tag.name = name; + tag.isActive = isActive === 'on'; + + // Generate new slug if name changed + const newSlug = name + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .trim('-'); + + if (newSlug !== tag.slug) { + const existingTag = await BlogTag.findOne({ + slug: newSlug, + _id: { $ne: tag._id } + }); + if (existingTag) { + req.flash('error_msg', 'A tag with this name already exists'); + return res.redirect(`/admin/blog/tags/${tag._id}/edit`); + } + tag.slug = newSlug; + } + + await tag.save(); + + req.flash('success_msg', 'Tag updated successfully'); + res.redirect('/admin/blog/tags'); + } catch (err) { + console.error('Tag update error:', err); + req.flash('error_msg', 'Error updating tag'); + res.redirect(`/admin/blog/tags/${req.params.id}/edit`); + } +}; + +// Delete tag +exports.destroy = async (req, res) => { + try { + const tag = await BlogTag.findById(req.params.id); + + if (!tag) { + req.flash('error_msg', 'Tag not found'); + return res.redirect('/admin/blog/tags'); + } + + // Check if tag has posts + const Blog = require('../models/blog'); + const postCount = await Blog.countDocuments({ tags: { $in: [tag.name] } }); + + if (postCount > 0) { + req.flash('error_msg', 'Cannot delete tag that is used in blog posts'); + return res.redirect('/admin/blog/tags'); + } + + await BlogTag.findByIdAndDelete(req.params.id); + + req.flash('success_msg', 'Tag deleted successfully'); + res.redirect('/admin/blog/tags'); + } catch (err) { + console.error('Tag delete error:', err); + req.flash('error_msg', 'Error deleting tag'); + res.redirect('/admin/blog/tags'); + } +}; + +// -------------------- Public API Controllers -------------------- + +// Get all active tags +exports.api = async (req, res) => { + try { + const tags = await BlogTag.getActive(); + + // Update post counts + for (const tag of tags) { + await tag.updatePostCount(); + } + + res.json(tags); + } catch (err) { + console.error('Tags API error:', err); + res.status(500).json({ error: 'Error loading tags' }); + } +}; + +// Get popular tags +exports.apiPopular = async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 10; + const tags = await BlogTag.getPopular(limit); + res.json(tags); + } catch (err) { + console.error('Popular tags API error:', err); + res.status(500).json({ error: 'Error loading popular tags' }); + } +}; + +// Get tag by slug +exports.apiShow = async (req, res) => { + try { + const tag = await BlogTag.findOne({ + slug: req.params.slug, + isActive: true + }).lean(); + + if (!tag) { + return res.status(404).json({ error: 'Tag not found' }); + } + + res.json(tag); + } catch (err) { + console.error('Tag show API error:', err); + res.status(500).json({ error: 'Error loading tag' }); + } +}; + +module.exports = exports; \ No newline at end of file