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