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 && (
60 <form
61 method="POST"
62 action={`/${repo.name}/issues/${issue.number}/close`}
63 class="inline-form"
64 >
65 <button
66 type="submit"
67 class="btn btn-sm"
68 >
69 {issue.status === "open"
70 ? "Close issue"
71 : "Reopen issue"}
72 </button>
73 </form>
74 )}
75 <details class="confirm-details">
76 <summary class="btn btn-sm btn-danger">
77 Delete
78 </summary>
79 <div class="confirm-popup">
80 Permanently delete this issue?
81 <form
82 method="POST"
83 action={`/${repo.name}/issues/${issue.number}/delete`}
84 class="inline-form"
85 >
86 <button
87 type="submit"
88 class="btn btn-sm btn-danger"
89 >
90 Yes, delete
91 </button>
92 </form>
93 </div>
94 </details>
95 </div>
96 )}
97 </div>
98
99 <div class="timeline-item">
100 <div class="timeline-author">
101 <Avatar
102 userId={issue.author_id}
103 version={issue.author_avatar_version}
104 size={24}
105 />
106 <strong>
107 {displayName(issue.author_username)}
108 </strong>
109 <div class="timeline-author-right">
110 {canEditIssue && (
111 <details class="inline-edit-details">
112 <summary class="btn btn-xs btn-ghost">
113 <span class="when-closed">
114 Edit
115 </span>
116 <span class="when-open">
117 Stop editing
118 </span>
119 </summary>
120 </details>
121 )}
122 <DateWithEdited
123 date={issue.created_at}
124 editedAt={issue.edited_at}
125 />
126 </div>
127 </div>
128 {canEditIssue && (
129 <div class="inline-edit-form-area">
130 <form
131 method="POST"
132 action={`/${repo.name}/issues/${issue.number}/edit`}
133 class="inline-edit-form"
134 >
135 <div class="form-group">
136 <label
137 class="form-label"
138 for="edit-issue-title"
139 >
140 Title
141 </label>
142 <input
143 class="form-input"
144 id="edit-issue-title"
145 name="title"
146 value={issue.title}
147 required
148 />
149 </div>
150 <div class="form-group">
151 <label
152 class="form-label"
153 for="edit-issue-body"
154 >
155 Description
156 </label>
157 <textarea
158 class="form-input"
159 id="edit-issue-body"
160 name="edit_body"
161 rows="6"
162 >
163 {issue.body}
164 </textarea>
165 </div>
166 <div class="form-actions">
167 <button
168 type="submit"
169 class="btn btn-sm btn-primary"
170 >
171 Save
172 </button>
173 </div>
174 </form>
175 </div>
176 )}
177 <div class="timeline-body markdown-body">
178 {bodyHtml || (
179 <em class="text-muted">
180 No description provided.
181 </em>
182 )}
183 </div>
184 <ReactionBar
185 reactions={reactions}
186 postUrl={`/${repo.name}/issues/${issue.number}/react`}
187 user={user}
188 />
189 </div>
190
191 {comments.map((comment) => (
192 <div class="timeline-item">
193 <div class="timeline-author">
194 <Avatar
195 userId={comment.author_id}
196 version={comment.author_avatar_version}
197 size={24}
198 />
199 <strong>
200 {displayName(comment.author_username)}
201 </strong>
202 <div class="timeline-author-right">
203 {canEditComment(comment) && (
204 <details class="inline-edit-details">
205 <summary class="btn btn-xs btn-ghost">
206 <span class="when-closed">
207 Edit
208 </span>
209 <span class="when-open">
210 Stop editing
211 </span>
212 </summary>
213 </details>
214 )}
215 <DateWithEdited
216 date={comment.created_at}
217 editedAt={comment.edited_at}
218 />
219 </div>
220 </div>
221 {canEditComment(comment) && (
222 <div class="inline-edit-form-area">
223 <form
224 method="POST"
225 action={`/${repo.name}/issues/${issue.number}/comments/${comment.id}/edit`}
226 class="inline-edit-form"
227 >
228 <div class="form-group">
229 <textarea
230 class="form-input"
231 name="edit_body"
232 rows="6"
233 >
234 {comment.body}
235 </textarea>
236 </div>
237 <div class="form-actions">
238 <button
239 type="submit"
240 class="btn btn-sm btn-primary"
241 >
242 Save
243 </button>
244 </div>
245 </form>
246 </div>
247 )}
248 <div class="timeline-body markdown-body">
249 {comment.bodyHtml}
250 </div>
251 <ReactionBar
252 reactions={
253 commentReactions.get(comment.id) ?? []
254 }
255 postUrl={`/${repo.name}/issues/${issue.number}/react`}
256 commentId={comment.id}
257 user={user}
258 />
259 </div>
260 ))}
261
262 {user && (
263 <div class="timeline-item timeline-item-new">
264 <h3 class="section-title">Add a comment</h3>
265 <form
266 method="POST"
267 action={`/${repo.name}/issues/${issue.number}/comments`}
268 >
269 <div class="form-group">
270 <textarea
271 name="body"
272 rows="6"
273 placeholder="Leave a comment (Markdown supported)"
274 required
275 />
276 </div>
277 <div class="form-actions">
278 <button
279 type="submit"
280 class="btn btn-primary"
281 >
282 Comment
283 </button>
284 </div>
285 </form>
286 </div>
287 )}
288 {!user && (
289 <p class="text-muted">
290 <a href="/login">Sign in</a> to leave a comment.
291 </p>
292 )}
293 </div>
294 </div>
295 </Layout>
296 );
297}
298