FileBlob.tsx
Raw
1import type { RepositoryRow } from "../../db/index.ts";
2import type { SessionUser } from "../../middleware/session.ts";
3import type { FileView } from "../../services/highlight.ts";
4import { Layout } from "../layout.tsx";
5import { RepoHeader } from "../repos/RepoHeader.tsx";
6import { BranchSelector } from "./BranchSelector.tsx";
7import { RepoNav } from "./RepoNav.tsx";
8
9interface FileBlobProps {
10 user: SessionUser | null;
11 repo: RepositoryRow;
12 ref: string;
13 filePath: string;
14 view: FileView;
15 branches: string[];
16 tags?: string[];
17 markdownHtml?: string;
18}
19
20export function FileBlob({
21 user,
22 repo,
23 ref: blobRef,
24 filePath,
25 view,
26 branches,
27 tags,
28 markdownHtml,
29}: FileBlobProps) {
30 const parts = filePath.split("/");
31 const filename = parts[parts.length - 1] ?? filePath;
32 return (
33 <Layout user={user} title={`${repo.name}/${filePath}`}>
34 <div class="container">
35 <RepoHeader repo={repo} />
36 <RepoNav repo={repo} active="code" user={user} />
37 <div class="breadcrumb">
38 <a href={`/${repo.name}/tree/${blobRef}`}>{repo.name}</a>
39 {parts.map((part, i) => {
40 const partPath = parts.slice(0, i + 1).join("/");
41 const isLast = i === parts.length - 1;
42 return (
43 <>
44 <span class="breadcrumb-sep">/</span>
45 {isLast ? (
46 <span class="breadcrumb-current">
47 {part}
48 </span>
49 ) : (
50 <a
51 href={`/${repo.name}/tree/${blobRef}/${partPath}`}
52 >
53 {part}
54 </a>
55 )}
56 </>
57 );
58 })}
59 </div>
60 <div class="file-blob-header">
61 <span class="file-blob-name">{filename}</span>
62 <div class="file-blob-actions file-blob-actions-wrap">
63 <BranchSelector
64 repoName={repo.name}
65 branches={branches}
66 tags={tags}
67 currentRef={blobRef}
68 view="blob"
69 path={filePath}
70 />
71 <a
72 href={`/${repo.name}/raw/${blobRef}/${filePath}`}
73 class="btn btn-sm"
74 >
75 Raw
76 </a>
77 {view.type === "inline" &&
78 branches.includes(blobRef) &&
79 user?.isAdmin && (
80 <a
81 href={`/${repo.name}/edit/${blobRef}/${filePath}`}
82 class="btn btn-sm btn-primary"
83 >
84 Edit
85 </a>
86 )}
87 {branches.includes(blobRef) && user?.isAdmin && (
88 <details class="confirm-details">
89 <summary class="btn btn-sm btn-danger">
90 Delete
91 </summary>
92 <div class="confirm-popup">
93 Delete <strong>{filename}</strong>?
94 <form
95 method="POST"
96 action={`/${repo.name}/delete-file/${blobRef}/${filePath}`}
97 class="popup-form"
98 style="margin-top: var(--space-2);"
99 >
100 <div class="form-group">
101 <input
102 name="message"
103 type="text"
104 placeholder={`Delete ${filename}`}
105 maxlength="500"
106 />
107 </div>
108 <button
109 type="submit"
110 class="btn btn-sm btn-danger"
111 >
112 Yes, delete
113 </button>
114 </form>
115 </div>
116 </details>
117 )}
118 </div>
119 </div>
120 <div class="file-blob-body">
121 {markdownHtml ? (
122 <div class="markdown-body">{markdownHtml}</div>
123 ) : view.type === "inline" ? (
124 <div class="shiki-wrapper">{view.html}</div>
125 ) : view.type === "media" ? (
126 <div class="file-media">
127 {view.mimeType.startsWith("image/") ? (
128 <img
129 src={`/${repo.name}/raw/${blobRef}/${filePath}`}
130 alt={filename}
131 class="file-media-img"
132 />
133 ) : view.mimeType.startsWith("audio/") ? (
134 // biome-ignore lint/a11y/useMediaCaption: captions unavailable for arbitrary repo files
135 <audio
136 controls
137 src={`/${repo.name}/raw/${blobRef}/${filePath}`}
138 class="file-media-audio"
139 />
140 ) : (
141 // biome-ignore lint/a11y/useMediaCaption: captions unavailable for arbitrary repo files
142 <video
143 controls
144 src={`/${repo.name}/raw/${blobRef}/${filePath}`}
145 class="file-media-video"
146 />
147 )}
148 </div>
149 ) : view.type === "binary" ? (
150 <div class="file-download-notice">
151 <p>Binary file ({formatSize(view.size)})</p>
152 <a
153 href={`/${repo.name}/raw/${blobRef}/${filePath}`}
154 class="btn btn-primary"
155 >
156 Download
157 </a>
158 </div>
159 ) : (
160 <div class="file-download-notice">
161 <p>
162 File too large to display inline (
163 {formatSize(view.size)})
164 </p>
165 <a
166 href={`/${repo.name}/raw/${blobRef}/${filePath}`}
167 class="btn btn-primary"
168 >
169 Download
170 </a>
171 </div>
172 )}
173 </div>
174 </div>
175 </Layout>
176 );
177}
178
179function formatSize(bytes: number): string {
180 if (bytes < 1024) return `${bytes} B`;
181 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
182 return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
183}
184