g1t/apps/web/app/routes/repo/issues.tsx

130 lines5,111 bytesCodeBlame
1import { GitPullRequest, MessageSquare, Plus, X } from "lucide-react";
2import { Link } from "react-router";
3
4import type { Route } from "./+types/issues";
5import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui";
6import { IssueIcon, Label, StateTabs } from "../../components/work";
7import { work } from "../../lib/services.server";
8import { getViewer, unwrap } from "../../lib/session.server";
9
10export function meta({ params }: Route.MetaArgs) {
11 return [{ title: `Issues · ${params.owner}/${params.repo} · g1t` }];
12}
13
14export async function loader({ params, context, request }: Route.LoaderArgs) {
15 const viewer = getViewer(context);
16 const path = { namespace: params.owner, name: params.repo };
17 const query = new URL(request.url).searchParams;
18 const state = query.get("state") === "closed" ? "closed" : "open";
19 const label = query.get("label") ?? "";
20 const [issues, labels] = await Promise.all([
21 work.listIssues(path, viewer, { state, label: label || undefined }),
22 work.listLabels(path, viewer),
23 ]);
24 return { issues: unwrap(issues), labels: unwrap(labels), state, label } as const;
25}
26
27export default function Issues({ loaderData, params }: Route.ComponentProps) {
28 const { issues, labels, state, label } = loaderData;
29 const repo = `/${params.owner}/${params.repo}`;
30 const base = `${repo}/issues`;
31 const stateQuery = state === "closed" ? "state=closed" : "";
32 return (
33 <div>
34 <StateTabs
35 to={base}
36 state={state}
37 query={label ? `label=${encodeURIComponent(label)}` : ""}
38 action={
39 <ButtonLink to={`${base}/new`}>
40 <Plus size={15} />
41 New issue
42 </ButtonLink>
43 }
44 />
45 <div className="mt-3 flex flex-wrap items-center gap-1.5">
46 {labels.map((name) => {
47 const active = name === label;
48 const query = [stateQuery, active ? "" : `label=${encodeURIComponent(name)}`]
49 .filter(Boolean)
50 .join("&");
51 return (
52 <Link
53 key={name}
54 to={`${base}?${query}`}
55 className={`flex items-center gap-1 rounded-full transition-opacity ${
56 label && !active ? "opacity-45 hover:opacity-100" : ""
57 }`}
58 >
59 <Label name={name} />
60 {active && <X size={12} className="text-muted" />}
61 </Link>
62 );
63 })}
64 </div>
65 <div className="mt-4">
66 {issues.length === 0 ? (
67 <EmptyState
68 title={
69 label
70 ? `No ${state} issues labelled ${label}`
71 : state === "open"
72 ? "No open issues"
73 : "No closed issues"
74 }
75 >
76 An issue says what should change: a bug, a feature, a question.
77 Agents and people open pull requests against it.
78 </EmptyState>
79 ) : (
80 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line">
81 {issues.map((issue) => (
82 <li key={issue.id}>
83 <Link
84 to={`${base}/${issue.number}`}
85 className="flex items-start gap-3 px-4 py-3 transition-colors hover:bg-surface"
86 >
87 <span className="mt-0.5">
88 <IssueIcon issue={issue} />
89 </span>
90 <span className="min-w-0 grow">
91 <span className="flex flex-wrap items-center gap-x-2 gap-y-1">
92 <span className="font-medium">{issue.title}</span>
93 {issue.labels.map((name) => (
94 <Label key={name} name={name} />
95 ))}
96 </span>
97 <span className="mt-0.5 block text-xs text-faint">
98 #{issue.number} opened <TimeAgo at={issue.createdAt} /> by{" "}
99 {issue.author.username}
100 {issue.resolvedBy != null && (
101 <span className="text-merged"> · resolved by #{issue.resolvedBy}</span>
102 )}
103 </span>
104 </span>
105 <span className="mt-0.5 flex shrink-0 items-center gap-3 text-xs text-muted">
106 {issue.pullCount > 0 && (
107 <span
108 className="flex items-center gap-1"
109 title={`${issue.pullCount} pull ${issue.pullCount === 1 ? "request" : "requests"}`}
110 >
111 <GitPullRequest size={13} />
112 {issue.pullCount}
113 </span>
114 )}
115 {issue.commentCount > 0 && (
116 <span className="flex items-center gap-1">
117 <MessageSquare size={13} />
118 {issue.commentCount}
119 </span>
120 )}
121 </span>
122 </Link>
123 </li>
124 ))}
125 </ul>
126 )}
127 </div>
128 </div>
129 );
130}