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 >
59 {release.name}
60 </h2>
61 </div>
62 <div class="release-item-date">
63 {release.tag_name && (
64 <a
65 href={`/${repo.name}/tree/${release.tag_name}`}
66 class="badge"
67 >
68 {release.tag_name}
69 </a>
70 )}
71 <time datetime={release.created_at}>
72 {formatDate(release.created_at)}
73 </time>
74 </div>
75 </div>
76 </div>
77
78 {notesHtml && (
79 <div class="markdown-body release-notes">
80 {notesHtml}
81 </div>
82 )}
83
84 {hasDownloads && (
85 <div class="release-assets">
86 <h3 class="release-assets-heading">Downloads</h3>
87 <ul class="asset-list">
88 {assets.map((asset) => (
89 <li class="asset-item">
90 <a
91 href={`/${repo.name}/releases/${release.id}/assets/${asset.filename}`}
92 class="asset-name"
93 >
94 {asset.filename}
95 </a>
96 <span class="asset-size">
97 {formatBytes(asset.size)}
98 </span>
99 </li>
100 ))}
101 {sourceArchives.map((archive) => (
102 <li class="asset-item">
103 <a
104 href={`/${repo.name}/releases/${release.id}/source/${archive.filename}`}
105 class="asset-name"
106 >
107 {archive.filename}
108 </a>
109 <span class="asset-meta">
110 Source code ({archive.format})
111 </span>
112 <span class="asset-size">
113 {formatBytes(archive.size)}
114 </span>
115 </li>
116 ))}
117 {sourceArchivesPending &&
118 (
119 [
120 ["zip", ".zip"],
121 ["tar.gz", ".tar.gz"],
122 ["tar.zst", ".tar.zst"],
123 ] as const
124 )
125 .filter(
126 ([format]) =>
127 !sourceArchives.some(
128 (a) => a.format === format,
129 ),
130 )
131 .map(([format, ext]) => (
132 <li class="asset-item asset-item-pending">
133 <span class="asset-name asset-name-pending">
134 {`${repo.name}-${release.tag_name}${ext}`}
135 </span>
136 <span class="asset-meta">
137 Source code ({format}) —
138 generating…
139 </span>
140 </li>
141 ))}
142 </ul>
143 </div>
144 )}
145
146 <div class="release-back">
147 <a
148 href={`/${repo.name}/releases`}
149 class="btn btn-secondary btn-sm"
150 >
151 ← All releases
152 </a>
153 {user?.isAdmin && (
154 <details class="confirm-details">
155 <summary class="btn btn-sm btn-danger">
156 Delete release
157 </summary>
158 <div class="confirm-popup">
159 Permanently delete this release and all its
160 files?
161 <form
162 method="POST"
163 action={`/${repo.name}/releases/${release.id}/delete`}
164 class="inline-form"
165 >
166 <button
167 type="submit"
168 class="btn btn-sm btn-danger"
169 >
170 Yes, delete
171 </button>
172 </form>
173 </div>
174 </details>
175 )}
176 </div>
177 </div>
178 </div>
179 </Layout>
180 );
181}
182