admin soft delete functionality, also fixed google sign in when user doesn't have first and last name

This commit is contained in:
jackiettran
2025-11-17 11:21:52 -05:00
parent 3a6da3d47d
commit e260992ef2
13 changed files with 580 additions and 33 deletions

View File

@@ -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<ForumPost | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 = () => {
<div className="row">
<div className="col-lg-8">
{/* Post Content */}
<div className="card mb-4">
<div className={`card mb-4 ${post.isDeleted && isAdmin ? 'border-danger' : ''}`}>
<div className="card-body">
{post.isDeleted && isAdmin && (
<div className="alert alert-danger mb-3">
<i className="bi bi-eye-slash me-2"></i>
<strong>Deleted by Admin</strong> - This post is deleted and hidden from regular users
{post.deletedAt && ` on ${formatDate(post.deletedAt)}`}
</div>
)}
{post.isPinned && (
<span className="badge bg-danger me-2 mb-2">
<i className="bi bi-pin-angle-fill me-1"></i>
@@ -275,6 +348,30 @@ const ForumPostDetail: React.FC = () => {
</div>
)}
{isAdmin && (
<div className="d-flex gap-2 flex-wrap mb-3">
{!post.isDeleted ? (
<button
className="btn btn-danger"
onClick={handleAdminDeletePost}
disabled={actionLoading}
>
<i className="bi bi-trash me-1"></i>
Delete Post (Admin)
</button>
) : (
<button
className="btn btn-success"
onClick={handleAdminRestorePost}
disabled={actionLoading}
>
<i className="bi bi-arrow-counterclockwise me-1"></i>
Restore Post (Admin)
</button>
)}
</div>
)}
<hr />
{/* 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) => (
<CommentThread
key={comment.id}
@@ -304,6 +407,9 @@ const ForumPostDetail: React.FC = () => {
currentUserId={user?.id}
isPostAuthor={isAuthor}
acceptedAnswerId={post.acceptedAnswerId}
isAdmin={isAdmin}
onAdminDelete={handleAdminDeleteComment}
onAdminRestore={handleAdminRestoreComment}
/>
))}
</div>
@@ -336,6 +442,69 @@ const ForumPostDetail: React.FC = () => {
</div>
</div>
{/* Admin Action Confirmation Modal */}
{showAdminModal && (
<div className="modal fade show d-block" tabIndex={-1} style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">
<i className="bi bi-shield-exclamation me-2"></i>
Confirm Admin Action
</h5>
<button
type="button"
className="btn-close"
onClick={cancelAdminAction}
disabled={actionLoading}
></button>
</div>
<div className="modal-body">
{adminAction?.type === 'deletePost' && (
<p>Are you sure you want to delete this post? It will be deleted and hidden from regular users but can be restored later.</p>
)}
{adminAction?.type === 'restorePost' && (
<p>Are you sure you want to restore this post? It will become visible to all users again.</p>
)}
{adminAction?.type === 'deleteComment' && (
<p>Are you sure you want to delete this comment? It will be deleted and hidden from regular users but can be restored later.</p>
)}
{adminAction?.type === 'restoreComment' && (
<p>Are you sure you want to restore this comment? It will become visible to all users again.</p>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={cancelAdminAction}
disabled={actionLoading}
>
Cancel
</button>
<button
type="button"
className={`btn ${adminAction?.type.includes('delete') ? 'btn-danger' : 'btn-success'}`}
onClick={confirmAdminAction}
disabled={actionLoading}
>
{actionLoading ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
Processing...
</>
) : (
<>
{adminAction?.type.includes('delete') ? 'Delete' : 'Restore'}
</>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -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<ForumPost[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="container mt-4">
<div className="d-flex justify-content-between align-items-center mb-4">
@@ -112,7 +133,7 @@ const ForumPosts: React.FC = () => {
{/* Filters */}
<div className="row mb-4">
<div className="col-md-6">
<div className={isAdmin ? "col-md-5" : "col-md-6"}>
<form onSubmit={handleSearch}>
<div className="input-group">
<input
@@ -129,7 +150,21 @@ const ForumPosts: React.FC = () => {
</div>
</form>
</div>
<div className="col-md-3">
{isAdmin && (
<div className="col-md-2">
<select
className="form-select"
name="deletionFilter"
value={filters.deletionFilter}
onChange={handleFilterChange}
>
<option value="active">Active</option>
<option value="all">All</option>
<option value="deleted">Deleted</option>
</select>
</div>
)}
<div className={isAdmin ? "col-md-2" : "col-md-3"}>
<select
className="form-select"
name="status"
@@ -142,7 +177,7 @@ const ForumPosts: React.FC = () => {
<option value="closed">Closed</option>
</select>
</div>
<div className="col-md-3">
<div className={isAdmin ? "col-md-3" : "col-md-3"}>
<select
className="form-select"
name="sort"
@@ -171,11 +206,11 @@ const ForumPosts: React.FC = () => {
<>
<div className="d-flex justify-content-between align-items-center mb-3">
<p className="text-muted mb-0">
Showing {posts.length} of {totalPosts} posts
Showing {filteredPosts.length} of {totalPosts} posts
</p>
</div>
{posts.length === 0 ? (
{filteredPosts.length === 0 ? (
<div className="text-center py-5">
<i className="bi bi-inbox display-1 text-muted"></i>
<h3 className="mt-3">No posts found</h3>
@@ -194,8 +229,8 @@ const ForumPosts: React.FC = () => {
) : (
<>
<div className="list-group list-group-flush mb-4">
{posts.map((post) => (
<ForumPostListItem key={post.id} post={post} />
{filteredPosts.map((post) => (
<ForumPostListItem key={post.id} post={post} filter={isAdmin ? filters.deletionFilter : undefined} />
))}
</div>