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