feat: integrate API calls for forum, bug, and event pages; replace mock data with dynamic data fetching

This commit is contained in:
Thibault Pouch
2026-03-03 09:48:07 +01:00
parent 4768bd5184
commit f54f237dd9
7 changed files with 363 additions and 241 deletions

View File

@@ -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' }}>