275 lines
9.6 KiB
TypeScript
275 lines
9.6 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { forumAPI } from '../services/api';
|
|
import { ForumPost } from '../types';
|
|
import CategoryBadge from '../components/CategoryBadge';
|
|
import PostStatusBadge from '../components/PostStatusBadge';
|
|
|
|
const MyPosts: React.FC = () => {
|
|
const { user } = useAuth();
|
|
const [posts, setPosts] = useState<ForumPost[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchMyPosts();
|
|
}
|
|
}, [user]);
|
|
|
|
const fetchMyPosts = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await forumAPI.getMyPosts();
|
|
setPosts(response.data);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || 'Failed to fetch your posts');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleStatusChange = async (postId: string, newStatus: string) => {
|
|
try {
|
|
setActionLoading(postId);
|
|
await forumAPI.updatePostStatus(postId, newStatus);
|
|
await fetchMyPosts(); // Refresh list
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.error || 'Failed to update status');
|
|
} finally {
|
|
setActionLoading(null);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (postId: string, postTitle: string) => {
|
|
if (!window.confirm(`Are you sure you want to delete "${postTitle}"? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setActionLoading(postId);
|
|
await forumAPI.deletePost(postId);
|
|
await fetchMyPosts(); // Refresh list
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.error || 'Failed to delete post');
|
|
setActionLoading(null);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffHours < 1) {
|
|
return 'Just now';
|
|
} else if (diffHours < 24) {
|
|
return `${diffHours}h ago`;
|
|
} else if (diffDays < 7) {
|
|
return `${diffDays}d ago`;
|
|
} else {
|
|
return date.toLocaleDateString();
|
|
}
|
|
};
|
|
|
|
if (!user) {
|
|
return (
|
|
<div className="container mt-4">
|
|
<div className="alert alert-warning" role="alert">
|
|
<i className="bi bi-exclamation-triangle me-2"></i>
|
|
You must be logged in to view your posts.
|
|
</div>
|
|
<Link to="/forum" className="btn btn-secondary">
|
|
<i className="bi bi-arrow-left me-2"></i>
|
|
Back to Forum
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="container mt-4">
|
|
<div className="text-center py-5">
|
|
<div className="spinner-border" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mt-4">
|
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1>My Posts</h1>
|
|
<p className="text-muted">Manage your forum posts and discussions</p>
|
|
</div>
|
|
<Link to="/forum/create" className="btn btn-primary">
|
|
<i className="bi bi-plus-circle me-2"></i>
|
|
Create Post
|
|
</Link>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="alert alert-danger" role="alert">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{posts.length === 0 ? (
|
|
<div className="text-center py-5">
|
|
<i className="bi bi-inbox display-1 text-muted"></i>
|
|
<h3 className="mt-3">No posts yet</h3>
|
|
<p className="text-muted mb-4">
|
|
Start a conversation by creating your first post!
|
|
</p>
|
|
<Link to="/forum/create" className="btn btn-primary">
|
|
<i className="bi bi-plus-circle me-2"></i>
|
|
Create Your First Post
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="mb-3">
|
|
<p className="text-muted">
|
|
You have {posts.length} post{posts.length !== 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="list-group">
|
|
{posts.map((post) => (
|
|
<div key={post.id} className="list-group-item">
|
|
<div className="row align-items-center">
|
|
<div className="col-lg-7">
|
|
<div className="d-flex align-items-start mb-2">
|
|
{post.isPinned && (
|
|
<span className="badge bg-danger me-2">
|
|
<i className="bi bi-pin-angle-fill"></i>
|
|
</span>
|
|
)}
|
|
<div className="flex-grow-1">
|
|
<h5 className="mb-1">
|
|
<Link to={`/forum/${post.id}`} className="text-decoration-none">
|
|
{post.title}
|
|
</Link>
|
|
</h5>
|
|
<div className="d-flex gap-2 mb-2">
|
|
<CategoryBadge category={post.category} />
|
|
<PostStatusBadge status={post.status} />
|
|
</div>
|
|
{post.tags && post.tags.length > 0 && (
|
|
<div className="mb-2">
|
|
{post.tags.slice(0, 3).map((tag) => (
|
|
<span key={tag.id} className="badge bg-light text-dark me-1">
|
|
#{tag.tagName}
|
|
</span>
|
|
))}
|
|
{post.tags.length > 3 && (
|
|
<span className="badge bg-light text-dark">
|
|
+{post.tags.length - 3}
|
|
</span>
|
|
)}
|
|
</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>
|
|
<span>
|
|
<i className="bi bi-clock me-1"></i>
|
|
{formatDate(post.updatedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-lg-5">
|
|
<div className="d-flex gap-2 flex-wrap justify-content-lg-end">
|
|
<Link
|
|
to={`/forum/${post.id}`}
|
|
className="btn btn-sm btn-outline-primary"
|
|
>
|
|
<i className="bi bi-eye me-1"></i>
|
|
View
|
|
</Link>
|
|
|
|
{post.status === 'open' && (
|
|
<button
|
|
className="btn btn-sm btn-outline-success"
|
|
onClick={() => handleStatusChange(post.id, 'answered')}
|
|
disabled={actionLoading === post.id}
|
|
>
|
|
<i className="bi bi-check-circle me-1"></i>
|
|
Mark Answered
|
|
</button>
|
|
)}
|
|
|
|
{post.status !== 'closed' && (
|
|
<button
|
|
className="btn btn-sm btn-outline-secondary"
|
|
onClick={() => handleStatusChange(post.id, 'closed')}
|
|
disabled={actionLoading === post.id}
|
|
>
|
|
<i className="bi bi-x-circle me-1"></i>
|
|
Close
|
|
</button>
|
|
)}
|
|
|
|
{post.status === 'closed' && (
|
|
<button
|
|
className="btn btn-sm btn-outline-success"
|
|
onClick={() => handleStatusChange(post.id, 'open')}
|
|
disabled={actionLoading === post.id}
|
|
>
|
|
<i className="bi bi-arrow-counterclockwise me-1"></i>
|
|
Reopen
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
className="btn btn-sm btn-outline-danger"
|
|
onClick={() => handleDelete(post.id, post.title)}
|
|
disabled={actionLoading === post.id}
|
|
>
|
|
{actionLoading === post.id ? (
|
|
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-trash me-1"></i>
|
|
Delete
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="mt-4">
|
|
<Link to="/forum" className="btn btn-outline-secondary">
|
|
<i className="bi bi-arrow-left me-2"></i>
|
|
Back to Forum
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MyPosts;
|