IssueDetail.tsx
Raw
1import type {
2 IssueCommentRow,
3 IssueRow,
4 RepositoryRow,
5} from "../../db/index.ts";
6import { displayName } from "../../lib/users.ts";
7import type { SessionUser } from "../../middleware/session.ts";
8import { Avatar } from "../Avatar.tsx";
9import { DateWithEdited } from "../DateWithEdited.tsx";
10import { Layout } from "../layout.tsx";
11import { ReactionBar, type ReactionCount } from "../ReactionBar.tsx";
12import { RepoHeader } from "../repos/RepoHeader.tsx";
13import { RepoNav } from "../repos/RepoNav.tsx";
14
15interface IssueDetailProps {
16 user: SessionUser | null;
17 repo: RepositoryRow;
18 issue: IssueRow & {
19 author_username: string;
20 author_avatar_version: number | null;
21 };
22 bodyHtml: string;
23 comments: (IssueCommentRow & {
24 author_username: string;
25 author_avatar_version: number | null;
26 bodyHtml: string;
27 })[];
28 reactions: ReactionCount[];
29 commentReactions: Map<number, ReactionCount[]>;
30}
31
32export function IssueDetail({
33 user,
34 repo,
35 issue,
36 bodyHtml,
37 comments,
38 reactions,
39 commentReactions,
40}: IssueDetailProps) {
41 const canEditIssue =
42 user != null && (user.isAdmin || user.id === issue.author_id);
43 const canEditComment = (c: IssueCommentRow) =>
44 user != null && (user.isAdmin || user.id === c.author_id);
45 return (
46 <Layout user={user} title={`${issue.title} — ${repo.name}`}>
47 <div class="container">
48 <RepoHeader repo={repo} />
49 <RepoNav repo={repo} active="issues" user={user} />
50 <div class="issue-detail">
51 <div class="issue-detail-header">
52 <span class="issue-number">#{issue.number}</span>
53 <h2 class="issue-detail-title">{issue.title}</h2>
54 <span class={`issue-badge ${issue.status}`}>
55 {issue.status}
56 </span>
57 {canEditIssue && (
58 <div class="issue-detail-meta-actions">
59 {user?.isAdmin && issue.status === "open" && (
60 <form
61 method="POST"
62 action={`/${repo.name}/issues/${issue.number}/complete`}
63 class="inline-form"
64 >
65 <button
66 type="submit"
67 class="btn btn-sm btn-primary"
68 >
69 Completed
70 </button>
71 </form>
72 )}
73 {user?.isAdmin && (
74 <form
75 method="POST"
76 action={`/${repo.name}/issues/${issue.number}/close`}
77 class="inline-form"
78 >
79 <button
80 type="submit"
81 class="btn btn-sm"
82 >
83 {issue.status === "open"
84 ? "Close issue"
85 : "Reopen issue"}
86 </button>
87 </form>
88 )}
89 <details class="confirm-details">
90 <summary class="btn btn-sm btn-danger">
91 Delete
92 </summary>
93 <div class="confirm-popup">
94 Permanently delete this issue?
95 <form
96 method="POST"
97 action={`/${repo.name}/issues/${issue.number}/delete`}
98 class="inline-form"
99 >
100 <button
101 type="submit"
102 class="btn btn-sm btn-danger"
103 >
104 Yes, delete
105 </button>
106 </form>
107 </div>
108 </details>
109 </div>
110 )}
111 </div>
112
113 <div class="timeline-item">
114 <div class="timeline-author">
115 <Avatar
116 userId={issue.author_id}
117 version={issue.author_avatar_version}
118 size={24}
119 />
120 <strong>
121 {displayName(issue.author_username)}
122 </strong>
123 <div class="timeline-author-right">
124 {canEditIssue && (
125 <details class="inline-edit-details">
126 <summary class="btn btn-xs btn-ghost">
127 <span class="when-closed">
128 Edit
129 </span>
130 <span class="when-open">
131 Stop editing
132 </span>
133 </summary>
134 </details>
135 )}
136 <DateWithEdited
137 date={issue.created_at}
138 editedAt={issue.edited_at}
139 />
140 </div>
141 </div>
142 {canEditIssue && (
143 <div class="inline-edit-form-area">
144 <form
145 method="POST"
146 action={`/${repo.name}/issues/${issue.number}/edit`}
147 class="inline-edit-form"
148 >
149 <div class="form-group">
150 <label
151 class="form-label"
152 for="edit-issue-title"
153 >
154 Title
155 </label>
156 <input
157 class="form-input"
158 id="edit-issue-title"
159 name="title"
160 value={issue.title}
161 required
162 />
163 </div>
164 <div class="form-group">
165 <label
166 class="form-label"
167 for="edit-issue-body"
168 >
169 Description
170 </label>
171 <textarea
172 class="form-input"
173 id="edit-issue-body"
174 name="edit_body"
175 rows="6"
176 >
177 {issue.body}
178 </textarea>
179 </div>
180 <div class="form-actions">
181 <button
182 type="submit"
183 class="btn btn-sm btn-primary"
184 >
185 Save
186 </button>
187 </div>
188 </form>
189 </div>
190 )}
191 <div class="timeline-body markdown-body">
192 {bodyHtml || (
193 <em class="text-muted">
194 No description provided.
195 </em>
196 )}
197 </div>
198 <ReactionBar
199 reactions={reactions}
200 postUrl={`/${repo.name}/issues/${issue.number}/react`}
201 user={user}
202 />
203 </div>
204
205 {comments.map((comment) => (
206 <div class="timeline-item">
207 <div class="timeline-author">
208 <Avatar
209 userId={comment.author_id}
210 version={comment.author_avatar_version}
211 size={24}
212 />
213 <strong>
214 {displayName(comment.author_username)}
215 </strong>
216 <div class="timeline-author-right">
217 {canEditComment(comment) && (
218 <details class="inline-edit-details">
219 <summary class="btn btn-xs btn-ghost">
220 <span class="when-closed">
221 Edit
222 </span>
223 <span class="when-open">
224 Stop editing
225 </span>
226 </summary>
227 </details>
228 )}
229 <DateWithEdited
230 date={comment.created_at}
231 editedAt={comment.edited_at}
232 />
233 </div>
234 </div>
235 {canEditComment(comment) && (
236 <div class="inline-edit-form-area">
237 <form
238 method="POST"
239 action={`/${repo.name}/issues/${issue.number}/comments/${comment.id}/edit`}
240 class="inline-edit-form"
241 >
242 <div class="form-group">
243 <textarea
244 class="form-input"
245 name="edit_body"
246 rows="6"
247 >
248 {comment.body}
249 </textarea>
250 </div>
251 <div class="form-actions">
252 <button
253 type="submit"
254 class="btn btn-sm btn-primary"
255 >
256 Save
257 </button>
258 </div>
259 </form>
260 </div>
261 )}
262 <div class="timeline-body markdown-body">
263 {comment.bodyHtml}
264 </div>
265 <ReactionBar
266 reactions={
267 commentReactions.get(comment.id) ?? []
268 }
269 postUrl={`/${repo.name}/issues/${issue.number}/react`}
270 commentId={comment.id}
271 user={user}
272 />
273 </div>
274 ))}
275
276 {user && (
277 <div class="timeline-item timeline-item-new">
278 <h3 class="section-title">Add a comment</h3>
279 <form
280 method="POST"
281 action={`/${repo.name}/issues/${issue.number}/comments`}
282 >
283 <div class="form-group">
284 <textarea
285 name="body"
286 rows="6"
287 placeholder="Leave a comment (Markdown supported)"
288 required
289 />
290 </div>
291 <div class="form-actions">
292 <button
293 type="submit"
294 class="btn btn-primary"
295 >
296 Comment
297 </button>
298 </div>
299 </form>
300 </div>
301 )}
302 {!user && (
303 <p class="text-muted">
304 <a href="/login">Sign in</a> to leave a comment.
305 </p>
306 )}
307 </div>
308 </div>
309 </Layout>
310 );
311}
312