import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link, useSearchParams } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { forumAPI } from '../services/api'; import { uploadFiles, getPublicImageUrl } from '../services/uploadService'; import { ForumPost, ForumComment } from '../types'; import CategoryBadge from '../components/CategoryBadge'; import PostStatusBadge from '../components/PostStatusBadge'; import CommentThread from '../components/CommentThread'; import CommentForm from '../components/CommentForm'; import AuthButton from '../components/AuthButton'; 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' | 'closePost' | 'reopenPost'; id?: string; } | null>(null); const [deletionReason, setDeletionReason] = useState(''); // Read filter from URL query param const filter = searchParams.get('filter') || 'active'; const isAdmin = user?.role === 'admin'; useEffect(() => { if (id) { fetchPost(); } }, [id]); const fetchPost = async () => { try { setLoading(true); const response = await forumAPI.getPost(id!); setPost(response.data); } catch (err: any) { setError(err.response?.data?.error || 'Failed to fetch post'); } finally { setLoading(false); } }; const handleAddComment = async (content: string, images: File[]) => { if (!user) { alert('Please log in to comment'); return; } try { // Upload images to S3 first (if any) let imageFilenames: string[] = []; if (images.length > 0) { const uploadResults = await uploadFiles("forum", images); imageFilenames = uploadResults.map((result) => result.key); } await forumAPI.createComment(id!, { content, imageFilenames: imageFilenames.length > 0 ? imageFilenames : undefined, }); await fetchPost(); // Refresh to get new comment } catch (err: any) { throw new Error(err.response?.data?.error || err.message || 'Failed to post comment'); } }; const handleReply = async (parentCommentId: string, content: string, images: File[] = []) => { if (!user) { alert('Please log in to reply'); return; } try { // Upload images to S3 first (if any) let imageFilenames: string[] = []; if (images.length > 0) { const uploadResults = await uploadFiles("forum", images); imageFilenames = uploadResults.map((result) => result.key); } await forumAPI.createComment(id!, { content, parentId: parentCommentId, imageFilenames: imageFilenames.length > 0 ? imageFilenames : undefined, }); await fetchPost(); // Refresh to get new reply } catch (err: any) { throw new Error(err.response?.data?.error || err.message || 'Failed to post reply'); } }; const handleEditComment = async ( commentId: string, content: string, existingImageKeys: string[], newImageFiles: File[] ) => { try { // Upload new images to S3 let newImageFilenames: string[] = []; if (newImageFiles.length > 0) { const uploadResults = await uploadFiles("forum", newImageFiles); newImageFilenames = uploadResults.map((result) => result.key); } // Combine existing and new image keys const allImageKeys = [...existingImageKeys, ...newImageFilenames]; await forumAPI.updateComment(commentId, { content, imageFilenames: allImageKeys, }); await fetchPost(); // Refresh to get updated comment } catch (err: any) { throw new Error(err.response?.data?.error || 'Failed to update comment'); } }; const handleDeleteComment = async (commentId: string) => { try { await forumAPI.deleteComment(commentId); await fetchPost(); // Refresh to remove deleted comment } catch (err: any) { alert(err.response?.data?.error || 'Failed to delete comment'); } }; const handleStatusChange = async (newStatus: string) => { try { setActionLoading(true); await forumAPI.updatePostStatus(id!, newStatus); await fetchPost(); } catch (err: any) { alert(err.response?.data?.error || 'Failed to update status'); } finally { setActionLoading(false); } }; const handleDeletePost = async () => { if (!window.confirm('Are you sure you want to delete this post? This action cannot be undone.')) { return; } try { setActionLoading(true); await forumAPI.deletePost(id!); navigate('/forum'); } catch (err: any) { alert(err.response?.data?.error || 'Failed to delete post'); setActionLoading(false); } }; const handleMarkAsAnswer = async (commentId: string) => { try { // If this comment is already the accepted answer, unmark it const newCommentId = post?.acceptedAnswerId === commentId ? null : commentId; await forumAPI.acceptAnswer(id!, newCommentId); await fetchPost(); // Refresh to get updated post } catch (err: any) { alert(err.response?.data?.error || 'Failed to mark answer'); } }; const handleAdminDeletePost = async () => { setAdminAction({ type: 'deletePost' }); setShowAdminModal(true); }; const handleAdminRestorePost = async () => { setAdminAction({ type: 'restorePost' }); setShowAdminModal(true); }; const handleAdminClosePost = async () => { setAdminAction({ type: 'closePost' }); setShowAdminModal(true); }; const handleAdminReopenPost = async () => { setAdminAction({ type: 'reopenPost' }); 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; // Validate deletion reason for delete actions if ((adminAction.type === 'deletePost' || adminAction.type === 'deleteComment') && !deletionReason.trim()) { alert('Please provide a reason for deletion'); return; } try { setActionLoading(true); setShowAdminModal(false); switch (adminAction.type) { case 'deletePost': await forumAPI.adminDeletePost(id!, deletionReason.trim()); break; case 'restorePost': await forumAPI.adminRestorePost(id!); break; case 'closePost': await forumAPI.adminClosePost(id!); break; case 'reopenPost': await forumAPI.adminReopenPost(id!); break; case 'deleteComment': await forumAPI.adminDeleteComment(adminAction.id!, deletionReason.trim()); 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); setDeletionReason(''); } }; const cancelAdminAction = () => { setShowAdminModal(false); setAdminAction(null); setDeletionReason(''); }; const handleCopyLink = async () => { const shareUrl = `${window.location.origin}/forum/${post?.id}`; try { await navigator.clipboard.writeText(shareUrl); } catch (err) { console.error("Copy to clipboard failed:", err); } }; const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: '2-digit' }); }; if (loading) { return (
Loading...
); } if (error || !post) { return (
{error || 'Post not found'}
Back to Forum
); } const isAuthor = user?.id === post.authorId; return (
{/* 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 && ( Pinned )}

{post.title}

{(post.tags || []).map((tag) => ( #{tag.tagName} ))}
By {post.author?.firstName || 'Unknown'} {post.author?.lastName || ''} {' • '} Posted {formatDate(post.createdAt)} {post.updatedAt !== post.createdAt && ' (edited)'} {' • '} {post.commentCount || 0} comments
{post.content}
{post.imageFilenames && post.imageFilenames.length > 0 && (
{post.imageFilenames.map((image, index) => (
{`Post window.open(getPublicImageUrl(image), '_blank')} />
))}
)} {isAuthor && (
Edit {post.status !== 'closed' && ( )} {post.status === 'closed' && ( )}
)} {isAdmin && (
{!post.isDeleted ? ( ) : ( )}
)}
{/* Comments Section */}
Comments ({post.commentCount || 0})
{post.comments && post.comments.length > 0 ? (
{[...post.comments] .sort((a, b) => { // Sort accepted answer to the top if (a.id === post.acceptedAnswerId) return -1; 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) => ( ))}
) : (

No comments yet. Be the first to comment!

)}
{/* Admin Close/Reopen Controls */} {isAdmin && (
{post.status === 'closed' ? ( ) : ( )}
)} {post.status !== 'closed' && user ? (
Add a comment
) : post.status !== 'closed' && !user ? (
Log in to join the discussion.
) : null} {/* Show closed banner at the bottom for all users */} {post.status === 'closed' && post.closedBy && (
Closed by {post.closer ? `${post.closer.firstName} ${post.closer.lastName}` : 'Unknown'} {post.closedAt && ` on ${formatDate(post.closedAt)}`} {' - '}No new comments can be added
)}
{/* 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.