diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 80ab961..e7af06e 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -127,4 +127,23 @@ const requireVerifiedEmail = (req, res, next) => { next(); }; -module.exports = { authenticateToken, optionalAuth, requireVerifiedEmail }; +// Require admin role middleware - must be used after authenticateToken +const requireAdmin = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + error: "Authentication required", + code: "NO_AUTH", + }); + } + + if (req.user.role !== "admin") { + return res.status(403).json({ + error: "Admin access required", + code: "INSUFFICIENT_PERMISSIONS", + }); + } + + next(); +}; + +module.exports = { authenticateToken, optionalAuth, requireVerifiedEmail, requireAdmin }; diff --git a/backend/models/ForumComment.js b/backend/models/ForumComment.js index 976de15..1495ae9 100644 --- a/backend/models/ForumComment.js +++ b/backend/models/ForumComment.js @@ -43,6 +43,18 @@ const ForumComment = sequelize.define('ForumComment', { type: DataTypes.ARRAY(DataTypes.TEXT), allowNull: true, defaultValue: [] + }, + deletedBy: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'Users', + key: 'id' + } + }, + deletedAt: { + type: DataTypes.DATE, + allowNull: true } }); diff --git a/backend/models/ForumPost.js b/backend/models/ForumPost.js index 98cca13..8890c86 100644 --- a/backend/models/ForumPost.js +++ b/backend/models/ForumPost.js @@ -56,6 +56,22 @@ const ForumPost = sequelize.define('ForumPost', { type: DataTypes.ARRAY(DataTypes.TEXT), allowNull: true, defaultValue: [] + }, + isDeleted: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + deletedBy: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'Users', + key: 'id' + } + }, + deletedAt: { + type: DataTypes.DATE, + allowNull: true } }); diff --git a/backend/models/User.js b/backend/models/User.js index b0b53a5..f791c78 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -137,6 +137,11 @@ const User = sequelize.define( defaultValue: 0, allowNull: false, }, + role: { + type: DataTypes.ENUM("user", "admin"), + defaultValue: "user", + allowNull: false, + }, }, { hooks: { diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 0e60b89..90c016b 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -173,6 +173,7 @@ router.post( firstName: user.firstName, lastName: user.lastName, isVerified: user.isVerified, + role: user.role, }, verificationEmailSent, // Don't send token in response body for security @@ -269,6 +270,7 @@ router.post( firstName: user.firstName, lastName: user.lastName, isVerified: user.isVerified, + role: user.role, }, // Don't send token in response body for security }); @@ -318,15 +320,36 @@ router.post( const { sub: googleId, email, - given_name: firstName, - family_name: lastName, + given_name: givenName, + family_name: familyName, picture, } = payload; - if (!email || !firstName || !lastName) { + if (!email) { return res .status(400) - .json({ error: "Required user information not provided by Google" }); + .json({ error: "Email not provided by Google" }); + } + + // Handle cases where Google doesn't provide name fields + // Generate fallback values from email or use placeholder + let firstName = givenName; + let lastName = familyName; + + if (!firstName || !lastName) { + const emailUsername = email.split('@')[0]; + // Try to split email username by common separators + const nameParts = emailUsername.split(/[._-]/); + + if (!firstName) { + firstName = nameParts[0] ? nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1) : 'Google'; + } + + if (!lastName) { + lastName = nameParts.length > 1 + ? nameParts[nameParts.length - 1].charAt(0).toUpperCase() + nameParts[nameParts.length - 1].slice(1) + : 'User'; + } } // Check if user exists by Google ID first @@ -422,6 +445,7 @@ router.post( lastName: user.lastName, profileImage: user.profileImage, isVerified: user.isVerified, + role: user.role, }, // Don't send token in response body for security }); @@ -658,6 +682,7 @@ router.post("/refresh", async (req, res) => { firstName: user.firstName, lastName: user.lastName, isVerified: user.isVerified, + role: user.role, }, }); } catch (error) { diff --git a/backend/routes/forum.js b/backend/routes/forum.js index b01a9bb..07a2e27 100644 --- a/backend/routes/forum.js +++ b/backend/routes/forum.js @@ -1,20 +1,28 @@ const express = require('express'); const { Op } = require('sequelize'); const { ForumPost, ForumComment, PostTag, User } = require('../models'); -const { authenticateToken } = require('../middleware/auth'); +const { authenticateToken, requireAdmin, optionalAuth } = require('../middleware/auth'); const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload'); const logger = require('../utils/logger'); const emailServices = require('../services/email'); const router = express.Router(); // Helper function to build nested comment tree -const buildCommentTree = (comments) => { +const buildCommentTree = (comments, isAdmin = false) => { const commentMap = {}; const rootComments = []; // Create a map of all comments comments.forEach(comment => { - commentMap[comment.id] = { ...comment.toJSON(), replies: [] }; + const commentJson = comment.toJSON(); + + // Sanitize deleted comments for non-admin users + if (commentJson.isDeleted && !isAdmin) { + commentJson.content = ''; + commentJson.images = []; + } + + commentMap[comment.id] = { ...commentJson, replies: [] }; }); // Build the tree structure @@ -30,7 +38,7 @@ const buildCommentTree = (comments) => { }; // GET /api/forum/posts - Browse all posts -router.get('/posts', async (req, res) => { +router.get('/posts', optionalAuth, async (req, res) => { try { const { search, @@ -44,6 +52,11 @@ router.get('/posts', async (req, res) => { const where = {}; + // Filter out deleted posts unless user is admin + if (!req.user || req.user.role !== 'admin') { + where.isDeleted = false; + } + if (category) { where.category = category; } @@ -86,6 +99,12 @@ router.get('/posts', async (req, res) => { model: PostTag, as: 'tags', attributes: ['id', 'tagName'] + }, + { + model: ForumComment, + as: 'comments', + attributes: ['id', 'isDeleted'], + required: false } ]; @@ -104,6 +123,15 @@ router.get('/posts', async (req, res) => { distinct: true }); + // Add hasDeletedComments flag to each post + const postsWithFlags = rows.map(post => { + const postJson = post.toJSON(); + postJson.hasDeletedComments = postJson.comments?.some(c => c.isDeleted) || false; + // Remove comments array to reduce response size (only needed the flag) + delete postJson.comments; + return postJson; + }); + const reqLogger = logger.withRequestId(req.id); reqLogger.info("Forum posts fetched", { search, @@ -116,7 +144,7 @@ router.get('/posts', async (req, res) => { }); res.json({ - posts: rows, + posts: postsWithFlags, totalPages: Math.ceil(count / limit), currentPage: parseInt(page), totalPosts: count @@ -133,14 +161,14 @@ router.get('/posts', async (req, res) => { }); // GET /api/forum/posts/:id - Get single post with all comments -router.get('/posts/:id', async (req, res) => { +router.get('/posts/:id', optionalAuth, async (req, res) => { try { const post = await ForumPost.findByPk(req.params.id, { include: [ { model: User, as: 'author', - attributes: ['id', 'username', 'firstName', 'lastName'] + attributes: ['id', 'username', 'firstName', 'lastName', 'role'] }, { model: PostTag, @@ -150,13 +178,12 @@ router.get('/posts/:id', async (req, res) => { { model: ForumComment, as: 'comments', - where: { isDeleted: false }, required: false, include: [ { model: User, as: 'author', - attributes: ['id', 'username', 'firstName', 'lastName'] + attributes: ['id', 'username', 'firstName', 'lastName', 'role'] } ] } @@ -170,12 +197,18 @@ router.get('/posts/:id', async (req, res) => { return res.status(404).json({ error: 'Post not found' }); } + // Hide deleted posts from non-admins + if (post.isDeleted && (!req.user || req.user.role !== 'admin')) { + return res.status(404).json({ error: 'Post not found' }); + } + // Increment view count await post.increment('viewCount'); // Build nested comment tree + const isAdmin = req.user && req.user.role === 'admin'; const postData = post.toJSON(); - postData.comments = buildCommentTree(post.comments); + postData.comments = buildCommentTree(post.comments, isAdmin); postData.viewCount += 1; // Reflect the increment const reqLogger = logger.withRequestId(req.id); @@ -765,8 +798,8 @@ router.delete('/comments/:id', authenticateToken, async (req, res) => { return res.status(403).json({ error: 'Unauthorized' }); } - // Soft delete - await comment.update({ isDeleted: true, content: '[deleted]' }); + // Soft delete (preserve content for potential restoration) + await comment.update({ isDeleted: true }); // Decrement comment count const post = await ForumPost.findByPk(comment.postId); @@ -871,4 +904,173 @@ router.get('/tags', async (req, res) => { } }); +// ============ ADMIN ROUTES ============ + +// DELETE /api/forum/admin/posts/:id - Admin soft-delete post +router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, res) => { + try { + const post = await ForumPost.findByPk(req.params.id); + + if (!post) { + return res.status(404).json({ error: 'Post not found' }); + } + + // Soft delete the post + await post.update({ + isDeleted: true, + deletedBy: req.user.id, + deletedAt: new Date() + }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Admin deleted post", { + postId: req.params.id, + adminId: req.user.id, + originalAuthorId: post.authorId + }); + + res.status(200).json({ message: 'Post deleted successfully', post }); + } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Admin post deletion failed", { + error: error.message, + stack: error.stack, + postId: req.params.id, + adminId: req.user.id + }); + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/forum/admin/posts/:id/restore - Admin restore deleted post +router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async (req, res) => { + try { + const post = await ForumPost.findByPk(req.params.id); + + if (!post) { + return res.status(404).json({ error: 'Post not found' }); + } + + // Restore the post + await post.update({ + isDeleted: false, + deletedBy: null, + deletedAt: null + }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Admin restored post", { + postId: req.params.id, + adminId: req.user.id, + originalAuthorId: post.authorId + }); + + res.status(200).json({ message: 'Post restored successfully', post }); + } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Admin post restoration failed", { + error: error.message, + stack: error.stack, + postId: req.params.id, + adminId: req.user.id + }); + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/forum/admin/comments/:id - Admin soft-delete comment +router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req, res) => { + try { + const comment = await ForumComment.findByPk(req.params.id); + + if (!comment) { + return res.status(404).json({ error: 'Comment not found' }); + } + + // Soft delete the comment (preserve original content for potential restoration) + await comment.update({ + isDeleted: true, + deletedBy: req.user.id, + deletedAt: new Date() + }); + + // Decrement comment count if not already deleted + if (!comment.isDeleted) { + const post = await ForumPost.findByPk(comment.postId); + if (post && post.commentCount > 0) { + await post.decrement('commentCount'); + } + } + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Admin deleted comment", { + commentId: req.params.id, + adminId: req.user.id, + originalAuthorId: comment.authorId, + postId: comment.postId + }); + + res.status(200).json({ message: 'Comment deleted successfully', comment }); + } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Admin comment deletion failed", { + error: error.message, + stack: error.stack, + commentId: req.params.id, + adminId: req.user.id + }); + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/forum/admin/comments/:id/restore - Admin restore deleted comment +router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, async (req, res) => { + try { + const comment = await ForumComment.findByPk(req.params.id); + + if (!comment) { + return res.status(404).json({ error: 'Comment not found' }); + } + + if (!comment.isDeleted) { + return res.status(400).json({ error: 'Comment is not deleted' }); + } + + // Restore the comment with its original content + await comment.update({ + isDeleted: false, + deletedBy: null, + deletedAt: null + }); + + // Increment comment count + const post = await ForumPost.findByPk(comment.postId); + if (post) { + await post.increment('commentCount'); + } + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Admin restored comment", { + commentId: req.params.id, + adminId: req.user.id, + originalAuthorId: comment.authorId, + postId: comment.postId + }); + + res.status(200).json({ + message: 'Comment restored successfully', + comment + }); + } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Admin comment restoration failed", { + error: error.message, + stack: error.stack, + commentId: req.params.id, + adminId: req.user.id + }); + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/frontend/src/components/AuthModal.tsx b/frontend/src/components/AuthModal.tsx index 7277f61..414e484 100644 --- a/frontend/src/components/AuthModal.tsx +++ b/frontend/src/components/AuthModal.tsx @@ -44,7 +44,7 @@ const AuthModal: React.FC = ({ const handleGoogleLogin = () => { const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID; const redirectUri = `${window.location.origin}/auth/google/callback`; - const scope = 'email profile'; + const scope = 'openid email profile'; const responseType = 'code'; const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?` + diff --git a/frontend/src/components/CommentThread.tsx b/frontend/src/components/CommentThread.tsx index 177d887..07a5f04 100644 --- a/frontend/src/components/CommentThread.tsx +++ b/frontend/src/components/CommentThread.tsx @@ -13,6 +13,9 @@ interface CommentThreadProps { isPostAuthor?: boolean; acceptedAnswerId?: string; depth?: number; + isAdmin?: boolean; + onAdminDelete?: (commentId: string) => Promise; + onAdminRestore?: (commentId: string) => Promise; } const CommentThread: React.FC = ({ @@ -25,6 +28,9 @@ const CommentThread: React.FC = ({ isPostAuthor = false, acceptedAnswerId, depth = 0, + isAdmin = false, + onAdminDelete, + onAdminRestore, }) => { const [showReplyForm, setShowReplyForm] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -89,7 +95,7 @@ const CommentThread: React.FC = ({ } }; - if (comment.isDeleted) { + if (comment.isDeleted && !isAdmin) { return (
0 ? "ms-4" : ""} mb-3`}>
@@ -111,6 +117,9 @@ const CommentThread: React.FC = ({ isPostAuthor={isPostAuthor} acceptedAnswerId={acceptedAnswerId} depth={depth + 1} + isAdmin={isAdmin} + onAdminDelete={onAdminDelete} + onAdminRestore={onAdminRestore} /> ))}
@@ -121,8 +130,17 @@ const CommentThread: React.FC = ({ return (
0 ? "ms-4" : ""} mb-3`}> -
+
+ {comment.isDeleted && isAdmin && ( +
+ + + Deleted by Admin + {comment.deletedAt && ` on ${formatDate(comment.deletedAt)}`} + +
+ )} {isAcceptedAnswer && (
@@ -264,6 +282,24 @@ const CommentThread: React.FC = ({ Delete )} + {isAdmin && onAdminDelete && !comment.isDeleted && !isEditing && ( + + )} + {isAdmin && onAdminRestore && comment.isDeleted && !isEditing && ( + + )}
@@ -294,6 +330,9 @@ const CommentThread: React.FC = ({ isPostAuthor={isPostAuthor} acceptedAnswerId={acceptedAnswerId} depth={depth + 1} + isAdmin={isAdmin} + onAdminDelete={onAdminDelete} + onAdminRestore={onAdminRestore} /> ))}
diff --git a/frontend/src/components/ForumPostListItem.tsx b/frontend/src/components/ForumPostListItem.tsx index 80195a7..9bb2380 100644 --- a/frontend/src/components/ForumPostListItem.tsx +++ b/frontend/src/components/ForumPostListItem.tsx @@ -3,12 +3,16 @@ import { Link } from "react-router-dom"; import { ForumPost } from "../types"; import CategoryBadge from "./CategoryBadge"; import PostStatusBadge from "./PostStatusBadge"; +import { useAuth } from "../contexts/AuthContext"; interface ForumPostListItemProps { post: ForumPost; + filter?: string; } -const ForumPostListItem: React.FC = ({ post }) => { +const ForumPostListItem: React.FC = ({ post, filter }) => { + const { user } = useAuth(); + const isAdmin = user?.role === 'admin'; const formatDate = (dateString: string) => { const date = new Date(dateString); const now = new Date(); @@ -38,9 +42,12 @@ const ForumPostListItem: React.FC = ({ post }) => { : text; }; + // Build link with filter param if admin and filter is set + const linkTo = filter && isAdmin ? `/forum/${post.id}?filter=${filter}` : `/forum/${post.id}`; + return (
- +
{/* Main content - 60% */}
@@ -51,6 +58,12 @@ const ForumPostListItem: React.FC = ({ post }) => { )} + {post.isDeleted && isAdmin && ( + + + Deleted + + )} {post.tags && diff --git a/frontend/src/pages/ForumPostDetail.tsx b/frontend/src/pages/ForumPostDetail.tsx index d3d9d12..7feb008 100644 --- a/frontend/src/pages/ForumPostDetail.tsx +++ b/frontend/src/pages/ForumPostDetail.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useParams, useNavigate, Link, useSearchParams } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { forumAPI, getForumImageUrl } from '../services/api'; import { ForumPost, ForumComment } from '../types'; @@ -13,10 +13,20 @@ const ForumPostDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const { user } = useAuth(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [post, setPost] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [actionLoading, setActionLoading] = useState(false); + const [showAdminModal, setShowAdminModal] = useState(false); + const [adminAction, setAdminAction] = useState<{ + type: 'deletePost' | 'deleteComment' | 'restorePost' | 'restoreComment'; + id?: string; + } | null>(null); + + // Read filter from URL query param + const filter = searchParams.get('filter') || 'active'; + const isAdmin = user?.role === 'admin'; useEffect(() => { if (id) { @@ -131,6 +141,62 @@ const ForumPostDetail: React.FC = () => { } }; + const handleAdminDeletePost = async () => { + setAdminAction({ type: 'deletePost' }); + setShowAdminModal(true); + }; + + const handleAdminRestorePost = async () => { + setAdminAction({ type: 'restorePost' }); + setShowAdminModal(true); + }; + + const handleAdminDeleteComment = async (commentId: string) => { + setAdminAction({ type: 'deleteComment', id: commentId }); + setShowAdminModal(true); + }; + + const handleAdminRestoreComment = async (commentId: string) => { + setAdminAction({ type: 'restoreComment', id: commentId }); + setShowAdminModal(true); + }; + + const confirmAdminAction = async () => { + if (!adminAction) return; + + try { + setActionLoading(true); + setShowAdminModal(false); + + switch (adminAction.type) { + case 'deletePost': + await forumAPI.adminDeletePost(id!); + break; + case 'restorePost': + await forumAPI.adminRestorePost(id!); + break; + case 'deleteComment': + await forumAPI.adminDeleteComment(adminAction.id!); + break; + case 'restoreComment': + await forumAPI.adminRestoreComment(adminAction.id!); + break; + } + + await fetchPost(); // Refresh to show updated status + } catch (err: any) { + alert(err.response?.data?.error || 'Failed to perform admin action'); + } finally { + setActionLoading(false); + setAdminAction(null); + } + }; + + const cancelAdminAction = () => { + setShowAdminModal(false); + setAdminAction(null); + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString(); @@ -180,8 +246,15 @@ const ForumPostDetail: React.FC = () => {
{/* Post Content */} -
+
+ {post.isDeleted && isAdmin && ( +
+ + Deleted by Admin - This post is deleted and hidden from regular users + {post.deletedAt && ` on ${formatDate(post.deletedAt)}`} +
+ )} {post.isPinned && ( @@ -275,6 +348,30 @@ const ForumPostDetail: React.FC = () => {
)} + {isAdmin && ( +
+ {!post.isDeleted ? ( + + ) : ( + + )} +
+ )} +
{/* Comments Section */} @@ -293,6 +390,12 @@ const ForumPostDetail: React.FC = () => { if (b.id === post.acceptedAnswerId) return 1; return 0; }) + .filter((comment: ForumComment) => { + // Filter comments based on deletion status (admin only) + if (!isAdmin) return true; // Non-admins see all non-deleted (backend already filters) + if (filter === 'deleted') return comment.isDeleted; // Only show deleted comments + return true; // 'active' and 'all' show all comments + }) .map((comment: ForumComment) => ( { currentUserId={user?.id} isPostAuthor={isAuthor} acceptedAnswerId={post.acceptedAnswerId} + isAdmin={isAdmin} + onAdminDelete={handleAdminDeleteComment} + onAdminRestore={handleAdminRestoreComment} /> ))}
@@ -336,6 +442,69 @@ const ForumPostDetail: React.FC = () => {
+ {/* Admin Action Confirmation Modal */} + {showAdminModal && ( +
+
+
+
+
+ + Confirm Admin Action +
+ +
+
+ {adminAction?.type === 'deletePost' && ( +

Are you sure you want to delete this post? It will be deleted and hidden from regular users but can be restored later.

+ )} + {adminAction?.type === 'restorePost' && ( +

Are you sure you want to restore this post? It will become visible to all users again.

+ )} + {adminAction?.type === 'deleteComment' && ( +

Are you sure you want to delete this comment? It will be deleted and hidden from regular users but can be restored later.

+ )} + {adminAction?.type === 'restoreComment' && ( +

Are you sure you want to restore this comment? It will become visible to all users again.

+ )} +
+
+ + +
+
+
+
+ )} +
); diff --git a/frontend/src/pages/ForumPosts.tsx b/frontend/src/pages/ForumPosts.tsx index 1c37e49..a6beff8 100644 --- a/frontend/src/pages/ForumPosts.tsx +++ b/frontend/src/pages/ForumPosts.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { forumAPI } from '../services/api'; import { ForumPost } from '../types'; @@ -8,6 +8,8 @@ import AuthButton from '../components/AuthButton'; const ForumPosts: React.FC = () => { const { user } = useAuth(); + const isAdmin = user?.role === 'admin'; + const [searchParams, setSearchParams] = useSearchParams(); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -15,12 +17,16 @@ const ForumPosts: React.FC = () => { const [totalPages, setTotalPages] = useState(1); const [totalPosts, setTotalPosts] = useState(0); + // Initialize filter from URL param or default to 'active' + const initialFilter = searchParams.get('filter') || 'active'; + const [filters, setFilters] = useState({ search: '', category: '', tag: '', status: '', - sort: 'recent' + sort: 'recent', + deletionFilter: initialFilter // all, active, deleted }); const categories = [ @@ -65,6 +71,11 @@ const ForumPosts: React.FC = () => { const { name, value } = e.target; setFilters(prev => ({ ...prev, [name]: value })); setCurrentPage(1); + + // Update URL param when deletion filter changes + if (name === 'deletionFilter' && isAdmin) { + setSearchParams({ filter: value }); + } }; const handleSearch = (e: React.FormEvent) => { @@ -81,6 +92,16 @@ const ForumPosts: React.FC = () => { setCurrentPage(1); }; + // Filter posts based on deletion status (admin only) + const filteredPosts = isAdmin ? posts.filter(post => { + if (filters.deletionFilter === 'active') { + return !post.isDeleted; // Show active posts regardless of deleted comments + } else if (filters.deletionFilter === 'deleted') { + return post.isDeleted || post.hasDeletedComments; // Show deleted posts OR posts with deleted comments + } + return true; // 'all' shows everything + }) : posts; + return (
@@ -112,7 +133,7 @@ const ForumPosts: React.FC = () => { {/* Filters */}
-
+
{
-
+ {isAdmin && ( +
+ +
+ )} +
-
+