ReleaseDetail.tsx
Raw
1import type {
2 ReleaseAssetRow,
3 ReleaseRow,
4 RepositoryRow,
5} from "../../db/index.ts";
6import { formatDate } from "../../lib/formatDate.ts";
7import type { SessionUser } from "../../middleware/session.ts";
8import { Layout } from "../layout.tsx";
9import { RepoHeader } from "../repos/RepoHeader.tsx";
10import { RepoNav } from "../repos/RepoNav.tsx";
11
12function formatBytes(bytes: number): string {
13 if (bytes < 1024) return `${bytes} B`;
14 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
15 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
16}
17
18interface SourceArchive {
19 format: string;
20 filename: string;
21 size: number;
22}
23
24interface ReleaseDetailProps {
25 user: SessionUser | null;
26 repo: RepositoryRow;
27 release: ReleaseRow;
28 notesHtml: string;
29 assets: ReleaseAssetRow[];
30 sourceArchives: SourceArchive[];
31 sourceArchivesPending?: boolean;
32}
33
34export function ReleaseDetail({
35 user,
36 repo,
37 release,
38 notesHtml,
39 assets,
40 sourceArchives,
41 sourceArchivesPending,
42}: ReleaseDetailProps) {
43 const hasDownloads =
44 sourceArchives.length > 0 || assets.length > 0 || sourceArchivesPending;
45
46 return (
47 <Layout user={user} title={`${release.name} — ${repo.name}`}>
48 <div class="container">
49 <RepoHeader repo={repo} />
50 <RepoNav repo={repo} active="releases" user={user} />
51 <div class="release-detail">
52 <div class="release-header">
53 <div class="release-item-header">
54 <div class="release-item-main">
55 <h2
56 class="page-title"
57 style={`view-transition-name: release-title-${release.id}`}
58 safe
59 >
60 {release.name}
61 </h2>
62 </div>
63 <div class="release-item-date">
64 {!!release.tag_name && (
65 <a
66 href={`/${repo.name}/tree/${release.tag_name}`}
67 class="badge"
68 safe
69 >
70 {release.tag_name}
71 </a>
72 )}
73 <time datetime={release.created_at} safe>
74 {formatDate(release.created_at)}
75 </time>
76 </div>
77 </div>
78 </div>
79
80 {!!notesHtml && (
81 <div class="markdown-body release-notes">
82 {notesHtml as "safe"}
83 </div>
84 )}
85
86 {hasDownloads && (
87 <div class="release-assets">
88 <h3 class="release-assets-heading">Downloads</h3>
89 <ul class="asset-list">
90 {assets.map((asset) => (
91 <li class="asset-item">
92 <a
93 href={`/${repo.name}/releases/${release.id}/assets/${asset.filename}`}
94 class="asset-name"
95 safe
96 >
97 {asset.filename}
98 </a>
99 <span class="asset-size" safe>
100 {formatBytes(asset.size)}
101 </span>
102 </li>
103 ))}
104 {sourceArchives.map((archive) => (
105 <li class="asset-item">
106 <a
107 href={`/${repo.name}/releases/${release.id}/source/${archive.filename}`}
108 class="asset-name"
109 safe
110 >
111 {archive.filename}
112 </a>
113 <span class="asset-meta" safe>
114 Source code ({archive.format})
115 </span>
116 <span class="asset-size" safe>
117 {formatBytes(archive.size)}
118 </span>
119 </li>
120 ))}
121 {sourceArchivesPending &&
122 (
123 [
124 ["zip", ".zip"],
125 ["tar.gz", ".tar.gz"],
126 ["tar.zst", ".tar.zst"],
127 ] as const
128 )
129 .filter(
130 ([format]) =>
131 !sourceArchives.some(
132 (a) => a.format === format,
133 ),
134 )
135 .map(([format, ext]) => (
136 <li class="asset-item asset-item-pending">
137 <span
138 class="asset-name asset-name-pending"
139 safe
140 >
141 {`${repo.name}-${release.tag_name}${ext}`}
142 </span>
143 <span class="asset-meta">
144 Source code ({format}) —
145 generating…
146 </span>
147 </li>
148 ))}
149 </ul>
150 </div>
151 )}
152
153 <div class="release-back">
154 <a
155 href={`/${repo.name}/releases`}
156 class="btn btn-secondary btn-sm"
157 >
158 ← All releases
159 </a>
160 {user?.isAdmin && (
161 <details class="confirm-details">
162 <summary class="btn btn-sm btn-danger">
163 Delete release
164 </summary>
165 <div class="confirm-popup">
166 Permanently delete this release and all its
167 files?
168 <form
169 method="POST"
170 action={`/${repo.name}/releases/${release.id}/delete`}
171 class="inline-form"
172 >
173 <button
174 type="submit"
175 class="btn btn-sm btn-danger"
176 >
177 Yes, delete
178 </button>
179 </form>
180 </div>
181 </details>
182 )}
183 </div>
184 </div>
185 </div>
186 </Layout>
187 );
188}
189