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
48 user={user}
49 title={`${release.name || release.tag_name} — ${repo.name}`}
50 >
51 <div class="container">
52 <RepoHeader repo={repo} />
53 <RepoNav repo={repo} active="releases" user={user} />
54 <div class="release-detail">
55 <div class="release-header">
56 <div class="release-item-header">
57 <div class="release-item-main">
58 <h2
59 class="page-title"
60 style={`view-transition-name: release-title-${release.id}`}
61 >
62 {release.name || release.tag_name}
63 </h2>
64 <div class="release-item-meta">
65 <a
66 href={`/${repo.name}/tree/${release.commit_hash}`}
67 class="monospace"
68 >
69 {release.commit_hash.slice(0, 8)}
70 </a>
71 </div>
72 </div>
73 <div class="release-item-date">
74 <span class="badge">{release.tag_name}</span>
75 <time datetime={release.created_at}>
76 {formatDate(release.created_at)}
77 </time>
78 </div>
79 </div>
80 </div>
81
82 {notesHtml && (
83 <div class="markdown-body release-notes">
84 {notesHtml}
85 </div>
86 )}
87
88 {hasDownloads && (
89 <div class="release-assets">
90 <h3 class="release-assets-heading">Downloads</h3>
91 <ul class="asset-list">
92 {assets.map((asset) => (
93 <li class="asset-item">
94 <a
95 href={`/${repo.name}/releases/${release.id}/assets/${asset.filename}`}
96 class="asset-name"
97 >
98 {asset.filename}
99 </a>
100 <span class="asset-size">
101 {formatBytes(asset.size)}
102 </span>
103 </li>
104 ))}
105 {sourceArchives.map((archive) => (
106 <li class="asset-item">
107 <a
108 href={`/${repo.name}/releases/${release.id}/source/${archive.filename}`}
109 class="asset-name"
110 >
111 {archive.filename}
112 </a>
113 <span class="asset-meta">
114 Source code ({archive.format})
115 </span>
116 <span class="asset-size">
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 class="asset-name asset-name-pending">
138 {`${repo.name}-${release.commit_hash.slice(0, 8)}${ext}`}
139 </span>
140 <span class="asset-meta">
141 Source code ({format}) —
142 generating…
143 </span>
144 </li>
145 ))}
146 </ul>
147 </div>
148 )}
149
150 <div class="release-back">
151 <a
152 href={`/${repo.name}/releases`}
153 class="btn btn-secondary btn-sm"
154 >
155 ← All releases
156 </a>
157 {user?.isAdmin && (
158 <details class="confirm-details">
159 <summary class="btn btn-sm btn-danger">
160 Delete release
161 </summary>
162 <div class="confirm-popup">
163 Permanently delete this release and all its
164 files?
165 <form
166 method="POST"
167 action={`/${repo.name}/releases/${release.id}/delete`}
168 class="inline-form"
169 >
170 <button
171 type="submit"
172 class="btn btn-sm btn-danger"
173 >
174 Yes, delete
175 </button>
176 </form>
177 </div>
178 </details>
179 )}
180 </div>
181 </div>
182 </div>
183 </Layout>
184 );
185}
186