import { useState, useCallback, useEffect, useMemo } from 'react'; import { Link, Navigate, useParams } from 'react-router-dom'; 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'; // ── Helpers ──────────────────────────────────────────────────────────────────── function StatusBadge({ status }: { status: BugStatus }) { const map: Record = { open: 'badge-open', in_progress: 'badge-progress', resolved: 'badge-resolved', closed: 'badge-closed', }; const labels: Record = { open: 'Open', in_progress: 'In Progress', resolved: 'Resolved', closed: 'Closed', }; return {labels[status]}; } function SeverityBadge({ severity }: { severity: BugSeverity }) { const map: Record = { low: 'badge-low', medium: 'badge-medium', high: 'badge-high', critical: 'badge-critical', }; return {severity}; } // ── Comment component ────────────────────────────────────────────────────────── function CommentItem({ comment }: { comment: BugComment }) { return (
{comment.authorName} {formatDateTime(comment.createdAt)}
{comment.content}
); } // ── Bug Detail Page ──────────────────────────────────────────────────────────── export default function BugDetailPage() { const { id } = useParams<{ id: string }>(); const { user, isAuthenticated } = useAuth(); const [bug, setBug] = useState(null); const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); const [notFound, setNotFound] = useState(false); const [newComment, setNewComment] = useState(''); const [commentError, setCommentError] = useState(''); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (!id) return; let cancelled = false; setLoading(true); 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]); const alreadyVoted = useMemo( () => !!user && !!bug && (bug.meTooBugs ?? []).includes(user.id), [user, bug] ); const isOwnReport = useMemo( () => !!user && !!bug && bug.submittedById === user.id, [user, bug] ); const handleMeToo = useCallback(async () => { if (!user || !bug || alreadyVoted || isOwnReport) return; 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 || !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); 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 (loading) { return (
Loading...
); } if (notFound || !bug) { return ; } const metooCount = (bug.meTooBugs ?? []).length; return (
{/* Breadcrumb */}
Bug Reports {' '}>{' '} {bug.uniqueCode}
{/* Header */}
{bug.uniqueCode}

{bug.title}

{/* Meta grid */}
{[ { label: 'Submitted by', value: bug.submittedByName }, { label: 'Date', value: formatDate(bug.createdAt) }, { label: 'Game Version', value: `v${bug.gameVersion}` }, { label: 'Assigned to', value: bug.assignedToName ?? 'Unassigned' }, ].map(({ label, value }) => (
{label}
{value}
))}
{/* Description */}
Description
{bug.description}
{/* Steps to reproduce */}
Steps to Reproduce
{bug.stepsToReproduce}
{/* "I have this too" section */}
{metooCount}{' '} {metooCount === 1 ? 'user has' : 'users have'} this issue
{!isAuthenticated ? (
Login to confirm you have this issue
) : isOwnReport ? ( (this is your report) ) : alreadyVoted ? (
✓ You reported this too
) : ( )}
{/* Comments section */}
Discussion {comments.length}
{comments.length === 0 ? (
No comments yet. Be the first to comment.
) : ( comments.map((comment) => ( )) )}
{isAuthenticated ? (
Add a Comment