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}
17
18export function FileBlob({
19 user,
20 repo,
21 ref: blobRef,
22 filePath,
23 view,
24 branches,
25}: FileBlobProps) {
26 const parts = filePath.split("/");
27 const filename = parts[parts.length - 1] ?? filePath;
28 const dir = parts.slice(0, -1).join("/");
29 const _backHref = dir
30 ? `/${repo.name}/tree/${blobRef}/${dir}`
31 : `/${repo.name}/tree/${blobRef}`;
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">
63 <BranchSelector
64 repoName={repo.name}
65 branches={branches}
66 currentRef={blobRef}
67 view="blob"
68 path={filePath}
69 />
70 <a
71 href={`/${repo.name}/raw/${blobRef}/${filePath}`}
72 class="btn btn-sm btn-ghost"
73 >
74 Raw
75 </a>
76 </div>
77 </div>
78 <div class="file-blob-body">
79 {view.type === "inline" ? (
80 <div class="shiki-wrapper">{view.html}</div>
81 ) : view.type === "binary" ? (
82 <div class="file-download-notice">
83 <p>Binary file ({formatSize(view.size)})</p>
84 <a
85 href={`/${repo.name}/raw/${blobRef}/${filePath}`}
86 class="btn btn-primary"
87 >
88 Download
89 </a>
90 </div>
91 ) : (
92 <div class="file-download-notice">
93 <p>
94 File too large to display inline (
95 {formatSize(view.size)})
96 </p>
97 <a
98 href={`/${repo.name}/raw/${blobRef}/${filePath}`}
99 class="btn btn-primary"
100 >
101 Download
102 </a>
103 </div>
104 )}
105 </div>
106 </div>
107 </Layout>
108 );
109}
110
111function formatSize(bytes: number): string {
112 if (bytes < 1024) return `${bytes} B`;
113 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
114 return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
115}
116