This repository has been archived on 2026-05-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Nest/nest-front/src/pages/public/BugDetailPage.tsx
2026-02-26 16:16:44 +01:00

348 lines
13 KiB
TypeScript

import { useState, useCallback, useMemo } from 'react';
import { Link, Navigate, useParams } from 'react-router-dom';
import { MOCK_BUGS, MOCK_BUG_COMMENTS } from '../../data/mockData';
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<BugStatus, string> = {
open: 'badge-open', in_progress: 'badge-progress', resolved: 'badge-resolved', closed: 'badge-closed',
};
const labels: Record<BugStatus, string> = {
open: 'Open', in_progress: 'In Progress', resolved: 'Resolved', closed: 'Closed',
};
return <span className={`badge ${map[status]}`}>{labels[status]}</span>;
}
function SeverityBadge({ severity }: { severity: BugSeverity }) {
const map: Record<BugSeverity, string> = {
low: 'badge-low', medium: 'badge-medium', high: 'badge-high', critical: 'badge-critical',
};
return <span className={`badge ${map[severity]}`}>{severity}</span>;
}
// ── Comment component ──────────────────────────────────────────────────────────
function CommentItem({ comment }: { comment: BugComment }) {
return (
<div
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: '0.9rem 1.1rem',
marginBottom: '0.5rem',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', gap: '1rem', flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-cyan)', fontSize: '0.78rem' }}>
{comment.authorName}
</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem', flexShrink: 0 }}>
{formatDateTime(comment.createdAt)}
</span>
</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-dim)', fontSize: '0.83rem', lineHeight: 1.75 }}>
{comment.content}
</div>
</div>
);
}
// ── Bug Detail Page ────────────────────────────────────────────────────────────
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 [newComment, setNewComment] = useState('');
const [commentError, setCommentError] = useState('');
const [submitting, setSubmitting] = useState(false);
const bug = useMemo(() => bugs.find((b) => b.id === id), [bugs, id]);
const bugComments = useMemo(
() => comments.filter((c) => c.bugReportId === id).sort((a, b) => a.createdAt.localeCompare(b.createdAt)),
[comments, id]
);
// "I have this too" logic
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(() => {
if (!user || !bug || alreadyVoted || isOwnReport) return;
setBugs((prev) =>
prev.map((b) =>
b.id === bug.id ? { ...b, meTooBugs: [...b.meTooBugs, user.id] } : b
)
);
}, [user, bug, alreadyVoted, isOwnReport]);
const handleComment = useCallback(async () => {
if (!user) 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]);
if (!bug) {
return <Navigate to="/bugs" replace />;
}
const metooCount = bug.meTooBugs.length;
return (
<div style={{ maxWidth: '860px', margin: '0 auto', padding: '3rem 1.5rem' }}>
{/* Breadcrumb */}
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--color-text-muted)', marginBottom: '1.5rem' }}>
<Link to="/bugs" style={{ color: 'var(--color-cyan)' }}>Bug Reports</Link>
{' '}&gt;{' '}
<span style={{ color: 'var(--color-text-dim)' }}>{bug.uniqueCode}</span>
</div>
{/* 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}
</span>
<StatusBadge status={bug.status} />
<SeverityBadge severity={bug.severity} />
</div>
{/* Title */}
<h1
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-text)',
fontSize: 'clamp(1.4rem, 4vw, 2rem)',
marginBottom: '1.25rem',
}}
>
{bug.title}
</h1>
{/* Meta grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: '0.6rem', marginBottom: '1.5rem' }}>
{[
{ 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 }) => (
<div
key={label}
style={{
background: 'var(--color-surface-alt)',
border: '1px solid var(--color-border)',
padding: '0.55rem 0.7rem',
}}
>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.62rem', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: '0.2rem' }}>
{label}
</div>
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-dim)', fontSize: '0.8rem' }}>{value}</div>
</div>
))}
</div>
{/* Description */}
<div style={{ marginBottom: '1.25rem' }}>
<div className="section-label" style={{ marginBottom: '0.4rem' }}>Description</div>
<div
style={{
background: 'var(--color-surface-alt)',
border: '1px solid var(--color-border)',
padding: '0.9rem 1rem',
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-dim)',
fontSize: '0.83rem',
lineHeight: 1.8,
whiteSpace: 'pre-wrap',
borderRadius: '6px',
}}
>
{bug.description}
</div>
</div>
{/* Steps to reproduce */}
<div style={{ marginBottom: '1.5rem' }}>
<div className="section-label" style={{ marginBottom: '0.4rem' }}>Steps to Reproduce</div>
<div
style={{
background: 'var(--color-surface-alt)',
border: '1px solid var(--color-border)',
padding: '0.9rem 1rem',
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-dim)',
fontSize: '0.83rem',
lineHeight: 1.8,
whiteSpace: 'pre-wrap',
borderRadius: '6px',
}}
>
{bug.stepsToReproduce}
</div>
</div>
{/* "I have this too" section */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
flexWrap: 'wrap',
padding: '0.9rem 1rem',
background: 'rgba(5,150,105,0.05)',
border: '1px solid rgba(5,150,105,0.2)',
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
</div>
) : isOwnReport ? (
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.78rem' }}>
(this is your report)
</span>
) : alreadyVoted ? (
<div
style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.78rem',
color: 'var(--color-green)',
border: '1px solid var(--color-green)',
padding: '0.25rem 0.75rem',
background: 'rgba(5,150,105,0.08)',
cursor: 'default',
borderRadius: '4px',
}}
>
&#10003; You reported this too
</div>
) : (
<button
className="btn-terminal"
onClick={handleMeToo}
style={{ fontSize: '0.78rem', padding: '0.3rem 0.9rem' }}
>
&#9654; I have this too
</button>
)}
</div>
</div>
{/* Comments section */}
<div style={{ marginTop: '2rem' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
borderBottom: '2px solid var(--color-border)',
paddingBottom: '0.5rem',
marginBottom: '1rem',
}}
>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-cyan)', fontSize: '0.72rem', letterSpacing: '0.15em', textTransform: 'uppercase' }}>
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}
</span>
</div>
{/* Comment list */}
{bugComments.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) => (
<CommentItem key={comment.id} comment={comment} />
))
)}
{/* Add comment */}
<div style={{ marginTop: '1.25rem' }}>
{isAuthenticated ? (
<div className="crt-box" style={{ padding: '1.25rem' }}>
<div className="section-label" style={{ marginBottom: '0.6rem' }}>Add a Comment</div>
<textarea
className={`input-terminal${commentError ? ' error' : ''}`}
rows={4}
placeholder="Write your comment..."
value={newComment}
onChange={(e) => { setNewComment(e.target.value); setCommentError(''); }}
style={{ resize: 'vertical', marginBottom: '0.6rem' }}
disabled={submitting}
aria-label="Comment text"
/>
{commentError && (
<div style={{ color: 'var(--color-red)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', marginBottom: '0.6rem' }}>
{commentError}
</div>
)}
<button
className="btn-terminal"
onClick={handleComment}
disabled={submitting}
style={{ opacity: submitting ? 0.6 : 1 }}
>
{submitting ? 'Posting...' : '&#9654; Post Comment'}
</button>
</div>
) : (
<div className="crt-box" style={{ padding: '1.25rem', textAlign: 'center' }}>
<p style={{ color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.82rem', marginBottom: '0.75rem' }}>
You must be logged in to comment.
</p>
<div style={{ display: 'flex', gap: '0.6rem', justifyContent: 'center' }}>
<Link to="/login" className="btn-terminal">Login</Link>
<Link to="/register" className="btn-terminal btn-amber">Register</Link>
</div>
</div>
)}
</div>
</div>
</div>
);
}