Can mark a comment as the answer, some layout changes

This commit is contained in:
jackiettran
2025-11-11 18:23:11 -05:00
parent 825389228d
commit b045fbeb01
11 changed files with 379 additions and 323 deletions

View File

@@ -109,6 +109,17 @@ const ForumPostDetail: React.FC = () => {
}
};
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 formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString();
@@ -160,86 +171,47 @@ const ForumPostDetail: React.FC = () => {
{/* Post Content */}
<div className="card mb-4">
<div className="card-body">
<div className="d-flex justify-content-between align-items-start mb-3">
<div className="flex-grow-1">
{post.isPinned && (
<span className="badge bg-danger me-2">
<i className="bi bi-pin-angle-fill me-1"></i>
Pinned
</span>
)}
<h1 className="h3 mb-2">{post.title}</h1>
<div className="d-flex gap-2 mb-2">
<CategoryBadge category={post.category} />
<PostStatusBadge status={post.status} />
</div>
</div>
</div>
{post.tags && post.tags.length > 0 && (
<div className="mb-3">
{post.tags.map((tag) => (
<Link
key={tag.id}
to={`/forum?tag=${tag.tagName}`}
className="badge bg-light text-dark me-1 mb-1 text-decoration-none"
>
#{tag.tagName}
</Link>
))}
</div>
{post.isPinned && (
<span className="badge bg-danger me-2 mb-2">
<i className="bi bi-pin-angle-fill me-1"></i>
Pinned
</span>
)}
<h1 className="h3 mb-2">{post.title}</h1>
<div className="mb-3">
<div className="d-flex align-items-center">
<div className="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2"
style={{ width: '40px', height: '40px' }}>
{post.author?.firstName?.charAt(0) || '?'}
</div>
<div>
<strong>
{post.author?.firstName || 'Unknown'} {post.author?.lastName || ''}
</strong>
<br />
<small className="text-muted">
Posted {formatDate(post.createdAt)}
{post.updatedAt !== post.createdAt && ' (edited)'}
</small>
</div>
</div>
<div className="d-flex gap-2 mb-2 flex-wrap">
<CategoryBadge category={post.category} />
<PostStatusBadge status={post.status} />
{post.tags && post.tags.length > 0 && (
<>
{post.tags.map((tag) => (
<Link
key={tag.id}
to={`/forum?tag=${tag.tagName}`}
className="badge bg-light text-dark text-decoration-none"
>
#{tag.tagName}
</Link>
))}
</>
)}
</div>
<hr />
<div className="text-muted small mb-3">
By <strong>{post.author?.firstName || 'Unknown'} {post.author?.lastName || ''}</strong>
{' • '}
Posted {formatDate(post.createdAt)}
{post.updatedAt !== post.createdAt && ' (edited)'}
{' • '}
{post.commentCount || 0} comments
</div>
<div className="post-content mb-3" style={{ whiteSpace: 'pre-wrap' }}>
{post.content}
</div>
<div className="d-flex gap-3 text-muted small">
<span>
<i className="bi bi-chat me-1"></i>
{post.commentCount || 0} comments
</span>
<span>
<i className="bi bi-eye me-1"></i>
{post.viewCount || 0} views
</span>
</div>
{isAuthor && (
<>
<hr />
<div className="d-flex gap-2 flex-wrap">
{post.status === 'open' && (
<button
className="btn btn-sm btn-success"
onClick={() => handleStatusChange('solved')}
disabled={actionLoading}
>
<i className="bi bi-check-circle me-1"></i>
Mark as Solved
</button>
)}
<div className="d-flex gap-2 flex-wrap mb-3">
{post.status !== 'closed' && (
<button
className="btn btn-sm btn-secondary"
@@ -275,120 +247,70 @@ const ForumPostDetail: React.FC = () => {
<i className="bi bi-trash me-1"></i>
Delete
</button>
</div>
</>
)}
</div>
</div>
{/* Comments Section */}
<div className="card">
<div className="card-header">
<h5 className="mb-0">
<i className="bi bi-chat-dots me-2"></i>
Comments ({post.commentCount || 0})
</h5>
</div>
<div className="card-body">
{user ? (
<div className="mb-4">
<h6>Add a comment</h6>
<CommentForm
onSubmit={handleAddComment}
placeholder="Share your thoughts..."
buttonText="Post Comment"
/>
</div>
) : (
<div className="alert alert-info mb-4">
<i className="bi bi-info-circle me-2"></i>
<AuthButton mode="login" className="alert-link" asLink>Log in</AuthButton> to join the discussion.
</div>
)}
<hr />
{post.comments && post.comments.length > 0 ? (
<div className="comments-list">
{post.comments.map((comment: ForumComment) => (
<CommentThread
key={comment.id}
comment={comment}
onReply={handleReply}
onEdit={handleEditComment}
onDelete={handleDeleteComment}
currentUserId={user?.id}
{/* Comments Section */}
<div className="mt-4">
<h5 className="mb-3">
<i className="bi bi-chat-dots me-2"></i>
Comments ({post.commentCount || 0})
</h5>
{post.comments && post.comments.length > 0 ? (
<div className="comments-list mb-4">
{[...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;
})
.map((comment: ForumComment) => (
<CommentThread
key={comment.id}
comment={comment}
onReply={handleReply}
onEdit={handleEditComment}
onDelete={handleDeleteComment}
onMarkAsAnswer={handleMarkAsAnswer}
currentUserId={user?.id}
isPostAuthor={isAuthor}
acceptedAnswerId={post.acceptedAnswerId}
/>
))}
</div>
) : (
<div className="text-center py-4 text-muted mb-4">
<i className="bi bi-chat display-4 d-block mb-2"></i>
<p>No comments yet. Be the first to comment!</p>
</div>
)}
<hr />
{user ? (
<div>
<h6>Add a comment</h6>
<CommentForm
onSubmit={handleAddComment}
placeholder="Share your thoughts..."
buttonText="Post Comment"
/>
))}
</div>
) : (
<div className="text-center py-4 text-muted">
<i className="bi bi-chat display-4 d-block mb-2"></i>
<p>No comments yet. Be the first to comment!</p>
</div>
)}
</div>
</div>
</div>
{/* Sidebar */}
<div className="col-lg-4">
<div className="card mb-3">
<div className="card-header">
<h6 className="mb-0">About this post</h6>
</div>
<div className="card-body">
<div className="mb-2">
<small className="text-muted">Category:</small>
<div>
<CategoryBadge category={post.category} />
</div>
</div>
<div className="mb-2">
<small className="text-muted">Status:</small>
<div>
<PostStatusBadge status={post.status} />
</div>
</div>
<div className="mb-2">
<small className="text-muted">Created:</small>
<div>{formatDate(post.createdAt)}</div>
</div>
<div className="mb-2">
<small className="text-muted">Last updated:</small>
<div>{formatDate(post.updatedAt)}</div>
</div>
<div className="mb-2">
<small className="text-muted">Author:</small>
<div>
<Link to={`/users/${post.authorId}`}>
{post.author?.firstName || 'Unknown'} {post.author?.lastName || ''}
</Link>
</div>
</div>
</div>
</div>
<div className="card">
<div className="card-header">
<h6 className="mb-0">Actions</h6>
</div>
<div className="card-body">
<div className="d-grid gap-2">
<Link to="/forum" className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-left me-2"></i>
Back to Forum
</Link>
{user && (
<Link to="/forum/create" className="btn btn-outline-primary btn-sm">
<i className="bi bi-plus-circle me-2"></i>
Create New Post
</Link>
</div>
) : (
<div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i>
<AuthButton mode="login" className="alert-link" asLink>Log in</AuthButton> to join the discussion.
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { forumAPI } from '../services/api';
import { ForumPost } from '../types';
import ForumPostCard from '../components/ForumPostCard';
import ForumPostListItem from '../components/ForumPostListItem';
import AuthButton from '../components/AuthButton';
const ForumPosts: React.FC = () => {
@@ -138,7 +138,7 @@ const ForumPosts: React.FC = () => {
>
<option value="">All Status</option>
<option value="open">Open</option>
<option value="solved">Solved</option>
<option value="answered">Answered</option>
<option value="closed">Closed</option>
</select>
</div>
@@ -151,7 +151,6 @@ const ForumPosts: React.FC = () => {
>
<option value="recent">Most Recent</option>
<option value="comments">Most Commented</option>
<option value="views">Most Viewed</option>
</select>
</div>
</div>
@@ -194,11 +193,9 @@ const ForumPosts: React.FC = () => {
</div>
) : (
<>
<div className="row g-4">
<div className="list-group list-group-flush mb-4">
{posts.map((post) => (
<div key={post.id} className="col-md-6 col-lg-4">
<ForumPostCard post={post} />
</div>
<ForumPostListItem key={post.id} post={post} />
))}
</div>

View File

@@ -208,11 +208,11 @@ const MyPosts: React.FC = () => {
{post.status === 'open' && (
<button
className="btn btn-sm btn-outline-success"
onClick={() => handleStatusChange(post.id, 'solved')}
onClick={() => handleStatusChange(post.id, 'answered')}
disabled={actionLoading === post.id}
>
<i className="bi bi-check-circle me-1"></i>
Mark Solved
Mark Answered
</button>
)}