feat: integrate API calls for forum, bug, and event pages; replace mock data with dynamic data fetching
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Link, Navigate, useParams } from 'react-router-dom';
|
||||
import { MOCK_BUGS, MOCK_BUG_COMMENTS } from '../../data/mockData';
|
||||
import { bugsApi, ApiError } from '../../utils/api';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { formatDate, formatDateTime } from '../../utils/format';
|
||||
import type { BugComment, BugReport, BugSeverity, BugStatus } from '../../types';
|
||||
@@ -57,21 +57,36 @@ export default function BugDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
|
||||
// Local state — mirrors the global bug list in memory
|
||||
const [bugs, setBugs] = useState<BugReport[]>(MOCK_BUGS);
|
||||
const [comments, setComments] = useState<BugComment[]>(MOCK_BUG_COMMENTS);
|
||||
const [bug, setBug] = useState<BugReport | null>(null);
|
||||
const [comments, setComments] = useState<BugComment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [commentError, setCommentError] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const bug = useMemo(() => bugs.find((b) => b.id === id), [bugs, id]);
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
const bugComments = useMemo(
|
||||
() => comments.filter((c) => c.bugReportId === id).sort((a, b) => a.createdAt.localeCompare(b.createdAt)),
|
||||
[comments, id]
|
||||
);
|
||||
bugsApi.getBug(id)
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
const { comments: bugComments, ...bugData } = data;
|
||||
setBug(bugData);
|
||||
setComments(bugComments.sort((a, b) => a.createdAt.localeCompare(b.createdAt)));
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
if (err instanceof ApiError && err.status === 404) setNotFound(true);
|
||||
})
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [id]);
|
||||
|
||||
// "I have this too" logic
|
||||
const alreadyVoted = useMemo(
|
||||
() => !!user && !!bug && bug.meTooBugs.includes(user.id),
|
||||
[user, bug]
|
||||
@@ -81,38 +96,44 @@ export default function BugDetailPage() {
|
||||
[user, bug]
|
||||
);
|
||||
|
||||
const handleMeToo = useCallback(() => {
|
||||
const handleMeToo = useCallback(async () => {
|
||||
if (!user || !bug || alreadyVoted || isOwnReport) return;
|
||||
setBugs((prev) =>
|
||||
prev.map((b) =>
|
||||
b.id === bug.id ? { ...b, meTooBugs: [...b.meTooBugs, user.id] } : b
|
||||
)
|
||||
);
|
||||
try {
|
||||
await bugsApi.toggleMeToo(bug.id);
|
||||
setBug((prev) => prev ? { ...prev, meTooBugs: [...prev.meTooBugs, user.id] } : prev);
|
||||
} catch {
|
||||
// silently ignore
|
||||
}
|
||||
}, [user, bug, alreadyVoted, isOwnReport]);
|
||||
|
||||
const handleComment = useCallback(async () => {
|
||||
if (!user) return;
|
||||
if (!user || !bug) return;
|
||||
if (!newComment.trim()) { setCommentError('Comment cannot be empty.'); return; }
|
||||
if (newComment.trim().length < 5) { setCommentError('Comment must be at least 5 characters.'); return; }
|
||||
|
||||
setCommentError('');
|
||||
setSubmitting(true);
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
|
||||
const comment: BugComment = {
|
||||
id: `bc${Date.now()}`,
|
||||
bugReportId: id!,
|
||||
authorId: user.id,
|
||||
authorName: user.username,
|
||||
content: newComment.trim(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setComments((prev) => [...prev, comment]);
|
||||
setNewComment('');
|
||||
setSubmitting(false);
|
||||
}, [user, newComment, id]);
|
||||
try {
|
||||
const comment = await bugsApi.addComment(bug.id, newComment.trim());
|
||||
setComments((prev) => [...prev, comment]);
|
||||
setNewComment('');
|
||||
} catch (err) {
|
||||
setCommentError(err instanceof Error ? err.message : 'Failed to post comment.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [user, bug, newComment]);
|
||||
|
||||
if (!bug) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ maxWidth: '860px', margin: '0 auto', padding: '3rem 1.5rem', textAlign: 'center', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)' }}>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (notFound || !bug) {
|
||||
return <Navigate to="/bugs" replace />;
|
||||
}
|
||||
|
||||
@@ -129,7 +150,6 @@ export default function BugDetailPage() {
|
||||
|
||||
{/* Header */}
|
||||
<div className="crt-box" style={{ padding: '1.75rem', marginBottom: '1rem' }}>
|
||||
{/* Badges */}
|
||||
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.72rem' }}>
|
||||
{bug.uniqueCode}
|
||||
@@ -138,7 +158,6 @@ export default function BugDetailPage() {
|
||||
<SeverityBadge severity={bug.severity} />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1
|
||||
style={{
|
||||
fontFamily: 'var(--font-heading)',
|
||||
@@ -227,13 +246,11 @@ export default function BugDetailPage() {
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
{/* Count */}
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-green)', fontSize: '0.82rem' }}>
|
||||
<span style={{ fontSize: '1.1rem' }}>{metooCount}</span>{' '}
|
||||
{metooCount === 1 ? 'user has' : 'users have'} this issue
|
||||
</div>
|
||||
|
||||
{/* Button logic */}
|
||||
{!isAuthenticated ? (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.78rem' }}>
|
||||
<Link to="/login">Login</Link> to confirm you have this issue
|
||||
@@ -285,22 +302,20 @@ export default function BugDetailPage() {
|
||||
Discussion
|
||||
</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.65rem', border: '1px solid var(--color-border)', padding: '0.05rem 0.4rem' }}>
|
||||
{bugComments.length}
|
||||
{comments.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Comment list */}
|
||||
{bugComments.length === 0 ? (
|
||||
{comments.length === 0 ? (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.8rem', padding: '1rem 0' }}>
|
||||
No comments yet. Be the first to comment.
|
||||
</div>
|
||||
) : (
|
||||
bugComments.map((comment) => (
|
||||
comments.map((comment) => (
|
||||
<CommentItem key={comment.id} comment={comment} />
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Add comment */}
|
||||
<div style={{ marginTop: '1.25rem' }}>
|
||||
{isAuthenticated ? (
|
||||
<div className="crt-box" style={{ padding: '1.25rem' }}>
|
||||
|
||||
Reference in New Issue
Block a user