diff --git a/controllers/blogController.js b/controllers/blogController.js index 22ce42b..cbb8155 100644 --- a/controllers/blogController.js +++ b/controllers/blogController.js @@ -198,6 +198,21 @@ exports.edit = async (req, res) => { const categories = await BlogCategory.getActive(); const tags = await BlogTag.getActive(); + // Get all comments for this blog post (including pending, approved, rejected) + const allComments = await BlogComment.find({ postId: blog._id }) + .sort({ createdAt: -1 }) + .lean(); + + // Organize comments with replies + const parentComments = allComments.filter(c => !c.parentId); + const commentsWithReplies = parentComments.map(parent => { + const replies = allComments.filter(c => c.parentId && c.parentId.toString() === parent._id.toString()); + return { + ...parent, + replies: replies + }; + }); + const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; res.render('admin/blog/edit', { @@ -206,6 +221,8 @@ exports.edit = async (req, res) => { blog, categories, tags, + comments: commentsWithReplies, + commentsCount: allComments.length, currentPath: req.path, user: req.session.user, frontendUrl @@ -437,7 +454,7 @@ 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 || {}; + const { authorName, authorEmail, authorPhone, authorAddress, authorDate, content, parentId } = req.body || {}; if (!authorName || !String(authorName).trim()) { return res.status(400).json({ @@ -477,6 +494,10 @@ exports.apiCreateComment = async (req, res) => { const newComment = await BlogComment.create({ postId: blog._id, authorName: String(authorName).trim(), + ...(authorEmail ? { authorEmail: String(authorEmail).trim() } : {}), + ...(authorPhone ? { authorPhone: String(authorPhone).trim() } : {}), + ...(authorAddress ? { authorAddress: String(authorAddress).trim() } : {}), + ...(authorDate ? { authorDate: String(authorDate).trim() } : {}), content: String(content).trim(), parentId: parentObjectId, status: "approved", @@ -653,4 +674,129 @@ exports.apiTags = async (req, res) => { } }; +// -------------------- Comment Management Controllers -------------------- + +// Approve a comment +exports.approveComment = async (req, res) => { + try { + const { blogId, commentId } = req.params; + + const blog = await Blog.findById(blogId); + if (!blog) { + return res.status(404).json({ + success: false, + message: 'Blog post not found' + }); + } + + const comment = await BlogComment.findById(commentId); + if (!comment || comment.postId.toString() !== blogId) { + return res.status(404).json({ + success: false, + message: 'Comment not found' + }); + } + + comment.status = 'approved'; + await comment.save(); + + res.json({ + success: true, + message: 'Comment approved successfully' + }); + } catch (err) { + console.error('Approve comment error:', err); + res.status(500).json({ + success: false, + message: 'Error approving comment', + error: err.message || 'Error approving comment' + }); + } +}; + +// Reject a comment +exports.rejectComment = async (req, res) => { + try { + const { blogId, commentId } = req.params; + + const blog = await Blog.findById(blogId); + if (!blog) { + return res.status(404).json({ + success: false, + message: 'Blog post not found' + }); + } + + const comment = await BlogComment.findById(commentId); + if (!comment || comment.postId.toString() !== blogId) { + return res.status(404).json({ + success: false, + message: 'Comment not found' + }); + } + + comment.status = 'rejected'; + await comment.save(); + + res.json({ + success: true, + message: 'Comment rejected successfully' + }); + } catch (err) { + console.error('Reject comment error:', err); + res.status(500).json({ + success: false, + message: 'Error rejecting comment', + error: err.message || 'Error rejecting comment' + }); + } +}; + +// Delete a comment +exports.deleteComment = async (req, res) => { + try { + const { blogId, commentId } = req.params; + + const blog = await Blog.findById(blogId); + if (!blog) { + return res.status(404).json({ + success: false, + message: 'Blog post not found' + }); + } + + const comment = await BlogComment.findById(commentId); + if (!comment || comment.postId.toString() !== blogId) { + return res.status(404).json({ + success: false, + message: 'Comment not found' + }); + } + + // Delete the comment and all its replies + await BlogComment.deleteMany({ + $or: [ + { _id: commentId }, + { parentId: commentId } + ] + }); + + // Update blog comment count + const remainingComments = await BlogComment.countDocuments({ postId: blogId }); + await Blog.updateOne({ _id: blogId }, { commentsCount: remainingComments }); + + res.json({ + success: true, + message: 'Comment deleted successfully' + }); + } catch (err) { + console.error('Delete comment error:', err); + res.status(500).json({ + success: false, + message: 'Error deleting comment', + error: err.message || 'Error deleting comment' + }); + } +}; + module.exports = exports; \ No newline at end of file diff --git a/models/blogComment.js b/models/blogComment.js index 80e24b1..8b6995d 100644 --- a/models/blogComment.js +++ b/models/blogComment.js @@ -11,6 +11,26 @@ const blogCommentSchema = new mongoose.Schema({ required: true, trim: true // "Frank Flores" }, + authorEmail: { + type: String, + default: '', + trim: true + }, + authorPhone: { + type: String, + default: '', + trim: true + }, + authorAddress: { + type: String, + default: '', + trim: true + }, + authorDate: { + type: String, + default: '', + trim: true + }, authorAvatar: { type: String, default: '' // "/assets/img/inner-page/news-details/comment-1.png" diff --git a/routes/admin.js b/routes/admin.js index 3339dc3..d5233f5 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -508,6 +508,11 @@ router.get("/blog/:id/edit", ensureAuthenticated, blogController.edit); router.post("/blog/:id/edit", ensureAuthenticated, blogController.update); router.post("/blog/:id/delete", ensureAuthenticated, blogController.destroy); +// Comment management routes +router.post("/blog/:blogId/comments/:commentId/approve", ensureAuthenticated, blogController.approveComment); +router.post("/blog/:blogId/comments/:commentId/reject", ensureAuthenticated, blogController.rejectComment); +router.post("/blog/:blogId/comments/:commentId/delete", ensureAuthenticated, blogController.deleteComment); + // Blog Categories Management router.get( "/blog/categories", diff --git a/server.js b/server.js index 01be70a..1378ddd 100644 --- a/server.js +++ b/server.js @@ -60,7 +60,7 @@ app.use(express.static(path.join(__dirname, "public"))); app.use( session({ secret: process.env.SESSION_SECRET || "secret", - resave: false, + resave: true, saveUninitialized: false, cookie: { maxAge: 1000 * 60 * 60 * 24 }, // 24 hours }), diff --git a/views/admin/blog/create.ejs b/views/admin/blog/create.ejs index 0a839d4..b3bc9c5 100644 --- a/views/admin/blog/create.ejs +++ b/views/admin/blog/create.ejs @@ -15,7 +15,7 @@
-
+
@@ -60,7 +60,9 @@
Upload a featured image for this blog post or - enter image URL.
+ enter image URL. +
Recommended size: 852 x 400 px +
@@ -96,6 +98,7 @@
Exactly 2 images required (row, 2 columns) +
Recommended size: 410 x 264 px each
@@ -287,7 +290,8 @@
-
+ +
Cancel @@ -332,10 +336,7 @@ const tools = { header: { class: Header, - config: { - levels: [2, 3, 4], - defaultLevel: 2 - } + inlineToolbar: true }, paragraph: { class: Paragraph, @@ -349,10 +350,33 @@ } }, image: { - class: Image, + class: ImageTool, config: { endpoints: { byFile: '/admin/upload/image?imageType=blog' + }, + // Map backend response (success:true, path/url) to EditorJS expected shape + uploader: { + async uploadByFile(file) { + const formData = new FormData(); + formData.append('image', file); + + const response = await fetch('/admin/upload/image?imageType=blog', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + if (!response.ok || !result.success || !(result.url || result.path)) { + throw new Error(result.error || 'Upload failed'); + } + + const url = result.url || result.path; + return { + success: 1, + file: { url } + }; + } } } }, @@ -839,6 +863,24 @@ #deleteCategoryModal.show, #deleteTagModal.show { display: block !important; } + + /* EditorJS delimiter -> render as HR line */ + .codex-editor .ce-delimiter { + line-height: 0; + margin: 16px 0; + } + + .codex-editor .ce-delimiter::before { + content: ""; + display: block; + width: 100%; + border-top: 2px solid rgba(0,0,0,0.75); + margin: 0; + } + + .codex-editor .ce-delimiter::after { + content: none !important; /* hide the *** */ + } \ No newline at end of file diff --git a/views/admin/blog/index.ejs b/views/admin/blog/index.ejs index 68adadb..4648400 100644 --- a/views/admin/blog/index.ejs +++ b/views/admin/blog/index.ejs @@ -84,7 +84,7 @@ <% if (blog.featuredImage) { %> - <%= blog.title %> <% } else { %>