1import { useParams, Link } from 'react-router-dom'
2import { ArrowLeft } from 'lucide-react'
3import { formatDistanceToNow } from 'date-fns'
4import { Skeleton } from '@/components/ui/skeleton'
5import { Separator } from '@/components/ui/separator'
6import { StatusBadge } from '@/components/bugs/StatusBadge'
7import { LabelEditor } from '@/components/bugs/LabelEditor'
8import { TitleEditor } from '@/components/bugs/TitleEditor'
9import { Timeline } from '@/components/bugs/Timeline'
10import { CommentBox } from '@/components/bugs/CommentBox'
11import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
12import { useBugDetailQuery } from '@/__generated__/graphql'
13import { useRepo } from '@/lib/repo'
14
15// Issue detail page (/:repo/issues/:id). Shows title, status, timeline of
16// comments and events, and a sidebar with labels and participants.
17export function BugDetailPage() {
18 const { id } = useParams<{ id: string }>()
19 const repo = useRepo()
20 const { data, loading, error } = useBugDetailQuery({
21 variables: { ref: repo, prefix: id! },
22 })
23
24 if (error) {
25 return (
26 <div className="py-16 text-center text-sm text-destructive">
27 Failed to load issue: {error.message}
28 </div>
29 )
30 }
31
32 if (loading && !data) {
33 return <BugDetailSkeleton />
34 }
35
36 const bug = data?.repository?.bug
37 if (!bug) {
38 return (
39 <div className="py-16 text-center text-sm text-muted-foreground">Issue not found.</div>
40 )
41 }
42
43 const issuesHref = repo ? `/${repo}/issues` : '/issues'
44 const authorHref = repo ? `/${repo}/user/${bug.author.humanId}` : `/user/${bug.author.humanId}`
45
46 return (
47 <div>
48 <Link
49 to={issuesHref}
50 className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
51 >
52 <ArrowLeft className="size-3.5" />
53 Back to issues
54 </Link>
55
56 {/* Title row — hover reveals edit button when logged in */}
57 <div className="mb-3">
58 <TitleEditor bugPrefix={bug.humanId} title={bug.title} humanId={bug.humanId} ref_={repo} />
59 </div>
60
61 <div className="mb-6 flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
62 <StatusBadge status={bug.status} />
63 <span>
64 <Link to={authorHref} className="font-medium text-foreground hover:underline">
65 {bug.author.displayName}
66 </Link>{' '}
67 opened this issue {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })}
68 </span>
69 </div>
70
71 <Separator className="mb-6" />
72
73 <div className="flex gap-8">
74 {/* Timeline + comment box */}
75 <div className="min-w-0 flex-1 space-y-4">
76 <Timeline bugPrefix={bug.humanId} items={bug.timeline.nodes} />
77 <CommentBox bugPrefix={bug.humanId} bugStatus={bug.status} ref_={repo} />
78 </div>
79
80 {/* Sidebar */}
81 <aside className="w-56 shrink-0 space-y-6">
82 <LabelEditor bugPrefix={bug.humanId} currentLabels={bug.labels} ref_={repo} />
83
84 <Separator />
85
86 <div>
87 <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
88 Participants
89 </h3>
90 <div className="flex flex-wrap gap-1.5">
91 {bug.participants.nodes.map((p) => {
92 const participantHref = repo ? `/${repo}/user/${p.humanId}` : `/user/${p.humanId}`
93 return (
94 <Link key={p.id} to={participantHref} title={p.displayName}>
95 <Avatar className="size-6">
96 <AvatarImage src={p.avatarUrl ?? undefined} alt={p.displayName} />
97 <AvatarFallback className="text-[10px]">
98 {p.displayName.slice(0, 2).toUpperCase()}
99 </AvatarFallback>
100 </Avatar>
101 </Link>
102 )
103 })}
104 </div>
105 </div>
106 </aside>
107 </div>
108 </div>
109 )
110}
111
112function BugDetailSkeleton() {
113 return (
114 <div className="space-y-4">
115 <Skeleton className="h-8 w-2/3" />
116 <Skeleton className="h-4 w-1/3" />
117 <Separator />
118 <div className="flex gap-8">
119 <div className="flex-1 space-y-4">
120 {Array.from({ length: 3 }).map((_, i) => (
121 <div key={i} className="rounded-md border border-border p-4">
122 <Skeleton className="mb-3 h-4 w-1/4" />
123 <Skeleton className="h-16 w-full" />
124 </div>
125 ))}
126 </div>
127 <div className="w-56 space-y-3">
128 <Skeleton className="h-4 w-full" />
129 <Skeleton className="h-4 w-3/4" />
130 </div>
131 </div>
132 </div>
133 )
134}