1import { useReadQuery } from "@apollo/client/react";
2import { createFileRoute, Link } from "@tanstack/react-router";
3import { formatDistanceToNow } from "date-fns";
4import { ArrowLeft } from "lucide-react";
5
6import { type BugDetailQuery, BugDetailDocument } from "@/__generated__/graphql";
7import { CommentBox } from "@/components/bugs/CommentBox";
8import { LabelEditor } from "@/components/bugs/LabelEditor";
9import { StatusBadge } from "@/components/bugs/StatusBadge";
10import { Timeline } from "@/components/bugs/Timeline";
11import { TitleEditor } from "@/components/bugs/TitleEditor";
12import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
13import { Separator } from "@/components/ui/separator";
14import { Skeleton } from "@/components/ui/skeleton";
15import { preloadQuery } from "@/lib/apollo";
16import { useRepo } from "@/lib/repo";
17
18export const Route = createFileRoute("/$repo/issues/$id")({
19 component: RouteComponent,
20 pendingComponent: BugDetailSkeleton,
21 loader: ({ params: { repo, id } }) => ({
22 bugDetailRef: preloadQuery<BugDetailQuery>(BugDetailDocument, {
23 variables: { ref: repo === "_" ? null : repo, prefix: id },
24 }),
25 }),
26});
27
28// Issue detail page (/:repo/issues/:id). Shows title, status, timeline of
29// comments and events, and a sidebar with labels and participants.
30function RouteComponent() {
31 const repo = useRepo();
32 const { bugDetailRef } = Route.useLoaderData();
33 const { data } = useReadQuery(bugDetailRef);
34
35 const bug = data?.repository?.bug;
36 if (!bug) {
37 return <div className="text-muted-foreground py-16 text-center text-sm">Issue not found.</div>;
38 }
39
40 return (
41 <div>
42 <Link
43 to="/$repo/issues"
44 params={{ repo: repo! }}
45 search={{ q: "status:open", after: "" }}
46 className="text-muted-foreground hover:text-foreground mb-4 flex items-center gap-1.5 text-sm"
47 >
48 <ArrowLeft className="size-3.5" />
49 Back to issues
50 </Link>
51
52 {/* Title row — hover reveals edit button when logged in */}
53 <div className="mb-3">
54 <TitleEditor bugPrefix={bug.humanId} title={bug.title} humanId={bug.humanId} ref_={repo} />
55 </div>
56
57 <div className="text-muted-foreground mb-6 flex flex-wrap items-center gap-3 text-sm">
58 <StatusBadge status={bug.status} />
59 <span>
60 <Link
61 to="/$repo/user/$id"
62 params={{ repo: repo!, id: bug.author.humanId }}
63 className="text-foreground font-medium hover:underline"
64 >
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="text-muted-foreground mb-2 text-xs font-semibold tracking-wider uppercase">
88 Participants
89 </h3>
90 <div className="flex flex-wrap gap-1.5">
91 {bug.participants.nodes.map((p) => {
92 return (
93 <Link
94 key={p.id}
95 to="/$repo/user/$id"
96 params={{ repo: repo!, id: p.humanId }}
97 title={p.displayName}
98 >
99 <Avatar className="size-6">
100 <AvatarImage src={p.avatarUrl ?? undefined} alt={p.displayName} />
101 <AvatarFallback className="text-[10px]">
102 {p.displayName.slice(0, 2).toUpperCase()}
103 </AvatarFallback>
104 </Avatar>
105 </Link>
106 );
107 })}
108 </div>
109 </div>
110 </aside>
111 </div>
112 </div>
113 );
114}
115
116function BugDetailSkeleton() {
117 return (
118 <div className="space-y-4">
119 <Skeleton className="h-8 w-2/3" />
120 <Skeleton className="h-4 w-1/3" />
121 <Separator />
122 <div className="flex gap-8">
123 <div className="flex-1 space-y-4">
124 {Array.from({ length: 3 }).map((_, i) => (
125 <div key={i} className="border-border rounded-md border p-4">
126 <Skeleton className="mb-3 h-4 w-1/4" />
127 <Skeleton className="h-16 w-full" />
128 </div>
129 ))}
130 </div>
131 <div className="w-56 space-y-3">
132 <Skeleton className="h-4 w-full" />
133 <Skeleton className="h-4 w-3/4" />
134 </div>
135 </div>
136 </div>
137 );
138}