forked from UKSOURCE/cms.hailearning.edu.vn
fix conflig pull request
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const BlogCategory = require('../models/blogCategory');
|
||||
const slugify = require('slugify');
|
||||
|
||||
// -------------------- Admin Controllers --------------------
|
||||
|
||||
@@ -49,12 +50,11 @@ exports.store = async (req, res) => {
|
||||
} = req.body;
|
||||
|
||||
// Generate slug
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
const slug = slugify(name, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'vi'
|
||||
});
|
||||
|
||||
// Check if slug exists
|
||||
const existingCategory = await BlogCategory.findOne({ slug });
|
||||
@@ -130,12 +130,11 @@ exports.update = async (req, res) => {
|
||||
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('-');
|
||||
const newSlug = slugify(name, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'vi'
|
||||
});
|
||||
|
||||
if (newSlug !== category.slug) {
|
||||
const existingCategory = await BlogCategory.findOne({
|
||||
@@ -166,6 +165,14 @@ exports.destroy = async (req, res) => {
|
||||
const category = await BlogCategory.findById(req.params.id);
|
||||
|
||||
if (!category) {
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
});
|
||||
}
|
||||
req.flash('error_msg', 'Category not found');
|
||||
return res.redirect('/admin/blog/categories');
|
||||
}
|
||||
@@ -175,16 +182,44 @@ exports.destroy = async (req, res) => {
|
||||
const postCount = await Blog.countDocuments({ category: { $in: [category.name] } });
|
||||
|
||||
if (postCount > 0) {
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Cannot delete category that has blog posts'
|
||||
});
|
||||
}
|
||||
req.flash('error_msg', 'Cannot delete category that has blog posts');
|
||||
return res.redirect('/admin/blog/categories');
|
||||
}
|
||||
|
||||
await BlogCategory.findByIdAndDelete(req.params.id);
|
||||
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Category deleted successfully'
|
||||
});
|
||||
}
|
||||
|
||||
req.flash('success_msg', 'Category deleted successfully');
|
||||
res.redirect('/admin/blog/categories');
|
||||
} catch (err) {
|
||||
console.error('Category delete error:', err);
|
||||
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting category',
|
||||
error: err.message || 'Error deleting category'
|
||||
});
|
||||
}
|
||||
|
||||
req.flash('error_msg', 'Error deleting category');
|
||||
res.redirect('/admin/blog/categories');
|
||||
}
|
||||
@@ -202,10 +237,18 @@ exports.api = async (req, res) => {
|
||||
await category.updatePostCount();
|
||||
}
|
||||
|
||||
res.json(categories);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Categories fetched successfully',
|
||||
data: categories
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Categories API error:', err);
|
||||
res.status(500).json({ error: 'Error loading categories' });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading categories',
|
||||
error: err.message || 'Error loading categories'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,13 +261,81 @@ exports.apiShow = async (req, res) => {
|
||||
}).lean();
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({ error: 'Category not found' });
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(category);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Category fetched successfully',
|
||||
data: category
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Category show API error:', err);
|
||||
res.status(500).json({ error: 'Error loading category' });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading category',
|
||||
error: err.message || 'Error loading category'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Quick create category (for inline creation in blog form)
|
||||
exports.quickCreate = async (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category name is required'
|
||||
});
|
||||
}
|
||||
|
||||
const categoryName = name.trim();
|
||||
|
||||
// Generate slug
|
||||
const slug = slugify(categoryName, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'vi'
|
||||
});
|
||||
|
||||
// Check if category already exists
|
||||
let category = await BlogCategory.findOne({ slug });
|
||||
|
||||
if (category) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Category already exists',
|
||||
data: category.toObject()
|
||||
});
|
||||
}
|
||||
|
||||
// Create new category
|
||||
category = new BlogCategory({
|
||||
name: categoryName,
|
||||
slug,
|
||||
description: description || '',
|
||||
isActive: true
|
||||
});
|
||||
|
||||
await category.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Category created successfully',
|
||||
data: category.toObject()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Quick create category error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error creating category',
|
||||
error: err.message || 'Error creating category'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const BlogTag = require('../models/blogTag');
|
||||
const slugify = require('slugify');
|
||||
|
||||
// -------------------- Admin Controllers --------------------
|
||||
|
||||
@@ -48,12 +49,11 @@ exports.store = async (req, res) => {
|
||||
} = req.body;
|
||||
|
||||
// Generate slug
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
const slug = slugify(name, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'vi'
|
||||
});
|
||||
|
||||
// Check if slug exists
|
||||
const existingTag = await BlogTag.findOne({ slug });
|
||||
@@ -126,12 +126,11 @@ exports.update = async (req, res) => {
|
||||
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('-');
|
||||
const newSlug = slugify(name, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'vi'
|
||||
});
|
||||
|
||||
if (newSlug !== tag.slug) {
|
||||
const existingTag = await BlogTag.findOne({
|
||||
@@ -162,6 +161,14 @@ exports.destroy = async (req, res) => {
|
||||
const tag = await BlogTag.findById(req.params.id);
|
||||
|
||||
if (!tag) {
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tag not found'
|
||||
});
|
||||
}
|
||||
req.flash('error_msg', 'Tag not found');
|
||||
return res.redirect('/admin/blog/tags');
|
||||
}
|
||||
@@ -171,16 +178,44 @@ exports.destroy = async (req, res) => {
|
||||
const postCount = await Blog.countDocuments({ tags: { $in: [tag.name] } });
|
||||
|
||||
if (postCount > 0) {
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Cannot delete tag that is used in blog posts'
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Tag deleted successfully'
|
||||
});
|
||||
}
|
||||
|
||||
req.flash('success_msg', 'Tag deleted successfully');
|
||||
res.redirect('/admin/blog/tags');
|
||||
} catch (err) {
|
||||
console.error('Tag delete error:', err);
|
||||
|
||||
// Check if it's an AJAX request
|
||||
const isAjax = req.xhr || req.headers['accept']?.includes('application/json') || req.headers['content-type']?.includes('application/json');
|
||||
if (isAjax) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting tag',
|
||||
error: err.message || 'Error deleting tag'
|
||||
});
|
||||
}
|
||||
|
||||
req.flash('error_msg', 'Error deleting tag');
|
||||
res.redirect('/admin/blog/tags');
|
||||
}
|
||||
@@ -198,10 +233,18 @@ exports.api = async (req, res) => {
|
||||
await tag.updatePostCount();
|
||||
}
|
||||
|
||||
res.json(tags);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Tags fetched successfully',
|
||||
data: tags
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tags API error:', err);
|
||||
res.status(500).json({ error: 'Error loading tags' });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading tags',
|
||||
error: err.message || 'Error loading tags'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,10 +253,19 @@ exports.apiPopular = async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const tags = await BlogTag.getPopular(limit);
|
||||
res.json(tags);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Popular tags fetched successfully',
|
||||
data: tags
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Popular tags API error:', err);
|
||||
res.status(500).json({ error: 'Error loading popular tags' });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading popular tags',
|
||||
error: err.message || 'Error loading popular tags'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -226,13 +278,80 @@ exports.apiShow = async (req, res) => {
|
||||
}).lean();
|
||||
|
||||
if (!tag) {
|
||||
return res.status(404).json({ error: 'Tag not found' });
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tag not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(tag);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Tag fetched successfully',
|
||||
data: tag
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tag show API error:', err);
|
||||
res.status(500).json({ error: 'Error loading tag' });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading tag',
|
||||
error: err.message || 'Error loading tag'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Quick create tag (for inline creation in blog form)
|
||||
exports.quickCreate = async (req, res) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tag name is required'
|
||||
});
|
||||
}
|
||||
|
||||
const tagName = name.trim();
|
||||
|
||||
// Generate slug
|
||||
const slug = slugify(tagName, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: 'vi'
|
||||
});
|
||||
|
||||
// Check if tag already exists
|
||||
let tag = await BlogTag.findOne({ slug });
|
||||
|
||||
if (tag) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Tag already exists',
|
||||
data: tag.toObject()
|
||||
});
|
||||
}
|
||||
|
||||
// Create new tag
|
||||
tag = new BlogTag({
|
||||
name: tagName,
|
||||
slug,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
await tag.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Tag created successfully',
|
||||
data: tag.toObject()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Quick create tag error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error creating tag',
|
||||
error: err.message || 'Error creating tag'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
360
controllers/serviceController.js
Normal file
360
controllers/serviceController.js
Normal file
@@ -0,0 +1,360 @@
|
||||
const { getServiceData } = require("../services/service.service");
|
||||
const Service = require("../models/service");
|
||||
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
|
||||
const slugify = require("slugify");
|
||||
|
||||
// Admin page - Service list
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const data = await getServiceData();
|
||||
console.log(data.services.items.image);
|
||||
res.render("admin/service/index", {
|
||||
title: "Service Management",
|
||||
data,
|
||||
layout: "layouts/main",
|
||||
getFullImageUrl, // Truyền helper function vào view
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading service data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Admin page - Service edit
|
||||
exports.edit = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const data = await getServiceData();
|
||||
|
||||
const service = data.services?.items?.find((item) => item.slug === slug);
|
||||
if (!service) {
|
||||
req.flash("error_msg", "Service not found");
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
res.render("admin/service/edit", {
|
||||
title: `Edit Service - ${service.name}`,
|
||||
service,
|
||||
layout: "layouts/main",
|
||||
getFullImageUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading service for editing");
|
||||
res.redirect("/admin/service");
|
||||
}
|
||||
};
|
||||
|
||||
// Update single service
|
||||
exports.updateService = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const currentData = await getServiceData();
|
||||
|
||||
const serviceIndex = currentData.services?.items?.findIndex(
|
||||
(item) => item.slug === slug,
|
||||
);
|
||||
if (serviceIndex === -1) {
|
||||
req.flash("error_msg", "Service not found");
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
// Update service data
|
||||
const updatedData = { ...currentData.toObject?.() };
|
||||
updatedData.services.items[serviceIndex] = {
|
||||
...updatedData.services.items[serviceIndex],
|
||||
name: req.body.name,
|
||||
slug: req.body.slug,
|
||||
description: req.body.description,
|
||||
image: req.body.image,
|
||||
layout: req.body.layout,
|
||||
};
|
||||
|
||||
if (currentData._id) {
|
||||
await Service.findByIdAndUpdate(currentData._id, updatedData);
|
||||
} else {
|
||||
await Service.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Service updated successfully");
|
||||
res.redirect("/admin/service");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", err.message);
|
||||
res.redirect("/admin/service");
|
||||
}
|
||||
};
|
||||
|
||||
// Admin page - Service details
|
||||
exports.details = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const data = await getServiceData();
|
||||
|
||||
const service = data.services?.items?.find((item) => item.slug === slug);
|
||||
if (!service) {
|
||||
req.flash("error_msg", "Service not found");
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
res.render("admin/service/details", {
|
||||
title: `Service Details - ${service.name}`,
|
||||
service,
|
||||
layout: "layouts/main",
|
||||
getFullImageUrl, // Truyền helper function vào view
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", "Error loading service details");
|
||||
res.redirect("/admin/service");
|
||||
}
|
||||
};
|
||||
|
||||
// Update service list
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const currentData = await getServiceData();
|
||||
const sections = [
|
||||
"pageTitle",
|
||||
"services",
|
||||
"destinations",
|
||||
"visas",
|
||||
"reviews",
|
||||
];
|
||||
|
||||
let updatedData = { ...currentData.toObject?.() };
|
||||
let hasChanges = false;
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (!req.body[section]) return;
|
||||
|
||||
const newData = JSON.parse(req.body[section]);
|
||||
if (JSON.stringify(newData) !== JSON.stringify(currentData[section])) {
|
||||
updatedData[section] = newData;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasChanges) {
|
||||
req.flash("info_msg", "No changes were made");
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
if (currentData._id) {
|
||||
await Service.findByIdAndUpdate(currentData._id, updatedData);
|
||||
} else {
|
||||
await Service.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Service updated successfully");
|
||||
res.redirect("/admin/service");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", err.message);
|
||||
res.redirect("/admin/service");
|
||||
}
|
||||
};
|
||||
|
||||
// Update service details
|
||||
exports.updateDetails = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
const currentData = await getServiceData();
|
||||
|
||||
const serviceIndex = currentData.services?.items?.findIndex(
|
||||
(item) => item.slug === slug,
|
||||
);
|
||||
if (serviceIndex === -1) {
|
||||
req.flash("error_msg", "Service not found");
|
||||
return res.redirect("/admin/service");
|
||||
}
|
||||
|
||||
// Parse features and FAQ from JSON strings
|
||||
const features = req.body.features ? JSON.parse(req.body.features) : [];
|
||||
const faq = req.body.faq ? JSON.parse(req.body.faq) : [];
|
||||
|
||||
// Update service details
|
||||
const updatedData = { ...currentData.toObject?.() };
|
||||
updatedData.services.items[serviceIndex].details = {
|
||||
title: req.body.title,
|
||||
description: req.body.description,
|
||||
mainImage: req.body.mainImage,
|
||||
overviewTitle: req.body.overviewTitle,
|
||||
overviewDescription: req.body.overviewDescription,
|
||||
additionalDescription: req.body.additionalDescription,
|
||||
keyFeaturesTitle: req.body.keyFeaturesTitle,
|
||||
keyFeaturesImage: req.body.keyFeaturesImage,
|
||||
features: features,
|
||||
faqTitle: req.body.faqTitle,
|
||||
faqImage: req.body.faqImage,
|
||||
faq: faq,
|
||||
};
|
||||
|
||||
if (currentData._id) {
|
||||
await Service.findByIdAndUpdate(currentData._id, updatedData);
|
||||
} else {
|
||||
await Service.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Service details updated successfully");
|
||||
res.redirect(`/admin/service/${slug}/details`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
req.flash("error_msg", err.message);
|
||||
res.redirect("/admin/service");
|
||||
}
|
||||
};
|
||||
|
||||
// API endpoint
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const serviceData = await getServiceData();
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
const processedData = addBaseUrlToImages(serviceData, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: "Error loading service data" });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get service details by slug - API endpoint
|
||||
*/
|
||||
exports.getServiceBySlug = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
|
||||
const serviceDoc = await Service.findOne().lean();
|
||||
|
||||
if (!serviceDoc) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Service data not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Find service by slug
|
||||
const service = serviceDoc.services?.items?.find(
|
||||
(item) => item.slug === slug,
|
||||
);
|
||||
|
||||
if (!service) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Service with slug '${slug}' not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
// Return service details in the expected format
|
||||
const responseData = {
|
||||
pageTitle: serviceDoc.pageTitle,
|
||||
breadcrumb: {
|
||||
...serviceDoc.breadcrumb,
|
||||
title: "Service Details",
|
||||
items: [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Services", href: "/services" },
|
||||
{ label: service.name, href: `/services/${slug}` },
|
||||
],
|
||||
},
|
||||
serviceDetails: {
|
||||
content: service.details,
|
||||
keyFeatures: {
|
||||
title: service.details.keyFeaturesTitle || "Key Features",
|
||||
sideImage: service.details.keyFeaturesImage || "img/default.jpg",
|
||||
items: service.details.features || [],
|
||||
},
|
||||
faq: {
|
||||
title: service.details.faqTitle || "Frequently Asked Questions",
|
||||
sideImage: service.details.faqImage || "img/default.jpg",
|
||||
items: service.details.faq || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const processedData = addBaseUrlToImages(responseData, baseUrl);
|
||||
res.json(processedData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching service by slug:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate slug from text - API endpoint
|
||||
*/
|
||||
exports.generateSlug = async (req, res) => {
|
||||
try {
|
||||
const { text } = req.body;
|
||||
|
||||
if (!text || typeof text !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Text is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate slug using slugify library with Vietnamese support
|
||||
const slug = slugify(text, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
locale: "vi",
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
slug: slug,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating slug:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all service slugs - API endpoint
|
||||
*/
|
||||
exports.getServiceSlugs = async (req, res) => {
|
||||
try {
|
||||
const serviceDoc = await Service.findOne().lean();
|
||||
|
||||
if (!serviceDoc?.services?.items) {
|
||||
return res.json({
|
||||
success: true,
|
||||
slugs: [],
|
||||
});
|
||||
}
|
||||
|
||||
const slugs = serviceDoc.services.items.map((item) => ({
|
||||
slug: item.slug,
|
||||
name: item.name,
|
||||
id: item.id,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
slugs,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching service slugs:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user