pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/components/repo-view.tsx

210 lines7,150 bytesCodeBlame
1import { BookOpen, File, Folder, FolderGit2 } from "lucide-react";
2import { Link } from "react-router";
3
4import type { BlobView as Blob, Commit, TreeView as Tree } from "@g1t/contracts";
5
6import { Markdown } from "./markdown";
7import { Avatar, CopyLine, TimeAgo } from "./ui";
8
9function encodePath(path: string): string {
10 return path.split("/").map(encodeURIComponent).join("/");
11}
12
13function Breadcrumbs({
14 base,
15 repo,
16 gitRef,
17 path,
18}: {
19 base: string;
20 repo: string;
21 gitRef: string;
22 path: string;
23}) {
24 const segments = path.split("/").filter(Boolean);
25 if (segments.length === 0) return null;
26 return (
27 <p className="mb-4 font-mono text-sm text-muted">
28 <Link to={`${base}/tree/${gitRef}`} className="text-accent hover:underline">
29 {repo}
30 </Link>
31 {segments.map((segment, i) => {
32 const to = encodePath(segments.slice(0, i + 1).join("/"));
33 const last = i === segments.length - 1;
34 return (
35 <span key={to}>
36 <span className="mx-1.5 text-faint">/</span>
37 {last ? (
38 <span className="font-medium text-fg">{segment}</span>
39 ) : (
40 <Link
41 to={`${base}/tree/${gitRef}/${to}`}
42 className="text-accent hover:underline"
43 >
44 {segment}
45 </Link>
46 )}
47 </span>
48 );
49 })}
50 </p>
51 );
52}
53
54/** The bar above a file listing describing the latest commit. */
55function CommitBar({ commit, gitRef }: { commit: Commit; gitRef: string }) {
56 return (
57 <div className="flex items-center gap-3 border-b border-line bg-surface px-4 py-2.5 text-sm">
58 <Avatar name={commit.author.name} />
59 <span className="shrink-0 font-medium">{commit.author.name}</span>
60 <span className="truncate text-muted">{commit.message.split("\n")[0]}</span>
61 <span className="ml-auto flex shrink-0 items-center gap-3 text-xs text-faint">
62 <span className="rounded-full border border-line px-2 py-0.5 font-mono text-accent">
63 {gitRef}
64 </span>
65 <span className="font-mono">{commit.hash.slice(0, 7)}</span>
66 <TimeAgo at={commit.authoredAt} />
67 </span>
68 </div>
69 );
70}
71
72export function TreeView({ tree }: { tree: Tree }) {
73 const { repo, ref, path, head, entries, readme } = tree;
74 const base = `/${repo.namespace}/${repo.name}`;
75 const prefix = path ? `${encodePath(path)}/` : "";
76 const cloneUrl = `https://g1t.sh${base}.git`;
77
78 if (!head) {
79 return (
80 <div className="mx-auto max-w-2xl rounded-xl border border-line bg-surface p-8">
81 <h2 className="text-lg font-semibold tracking-tight">
82 This repository is empty
83 </h2>
84 <p className="mt-1 text-sm text-muted">
85 Push an existing project to it. Use an access token as the password.
86 </p>
87 <div className="mt-5 space-y-2">
88 <CopyLine prompt text={`git remote add g1t ${cloneUrl}`} />
89 <CopyLine prompt text={`git push -u g1t ${ref}`} />
90 </div>
91 </div>
92 );
93 }
94
95 return (
96 <div className="grid gap-6 lg:grid-cols-[1fr_18rem]">
97 <div className="min-w-0">
98 <Breadcrumbs base={base} repo={repo.name} gitRef={ref} path={path} />
99 <div className="overflow-hidden rounded-xl border border-line">
100 <CommitBar commit={head} gitRef={ref} />
101 <ul className="divide-y divide-line text-sm">
102 {entries.map((entry) => {
103 const isTree = entry.kind === "tree";
104 const Icon = isTree ? Folder : entry.kind === "gitlink" ? FolderGit2 : File;
105 return (
106 <li key={entry.name}>
107 <Link
108 to={`${base}/${isTree ? "tree" : "blob"}/${ref}/${prefix}${encodeURIComponent(entry.name)}`}
109 className="flex items-center gap-3 px-4 py-2 transition-colors hover:bg-surface"
110 >
111 <Icon
112 size={15}
113 className={isTree ? "text-accent-dim" : "text-faint"}
114 />
115 <span className="font-mono text-[0.8125rem]">{entry.name}</span>
116 </Link>
117 </li>
118 );
119 })}
120 </ul>
121 </div>
122
123 {readme?.text != null && (
124 <section className="mt-6 overflow-hidden rounded-xl border border-line">
125 <h2 className="flex items-center gap-2 border-b border-line bg-surface px-4 py-2.5 text-sm font-medium">
126 <BookOpen size={15} className="text-faint" />
127 {readme.name}
128 </h2>
129 <div className="p-6">
130 {/\.(md|markdown)$/i.test(readme.name) ? (
131 <Markdown source={readme.text} />
132 ) : (
133 <pre className="whitespace-pre-wrap text-sm">{readme.text}</pre>
134 )}
135 </div>
136 </section>
137 )}
138 </div>
139
140 {!path && (
141 <aside className="space-y-6">
142 <section>
143 <h2 className="text-sm font-medium">Clone</h2>
144 <div className="mt-2">
145 <CopyLine text={`git clone ${cloneUrl}`} />
146 </div>
147 </section>
148 <section>
149 <h2 className="text-sm font-medium">About</h2>
150 <p className="mt-2 text-sm text-muted">
151 {repo.description ?? "No description."}
152 </p>
153 <p className="mt-3 text-xs text-faint">
154 Created <TimeAgo at={repo.createdAt} />
155 </p>
156 </section>
157 </aside>
158 )}
159 </div>
160 );
161}
162
163export function BlobView({
164 blob,
165 html,
166}: {
167 blob: Blob;
168 /** Syntax-highlighted HTML, when the language is known. */
169 html: string | null;
170}) {
171 const { repo, ref, path, size, text } = blob;
172 const lines = text?.replace(/\n$/, "").split("\n");
173 return (
174 <div>
175 <Breadcrumbs
176 base={`/${repo.namespace}/${repo.name}`}
177 repo={repo.name}
178 gitRef={ref}
179 path={path}
180 />
181 <div className="overflow-hidden rounded-xl border border-line">
182 <div className="flex items-center gap-3 border-b border-line bg-surface px-4 py-2.5 text-xs text-muted">
183 {lines && <span>{lines.length.toLocaleString("en-US")} lines</span>}
184 <span>{size.toLocaleString("en-US")} bytes</span>
185 </div>
186 {html ? (
187 <div
188 className="overflow-x-auto py-3 pr-4"
189 // Shiki escapes the source; this is its generated markup.
190 dangerouslySetInnerHTML={{ __html: html }}
191 />
192 ) : lines ? (
193 <div className="flex overflow-x-auto py-3 font-mono text-sm leading-6">
194 <pre
195 aria-hidden="true"
196 className="w-10 shrink-0 text-right text-faint select-none"
197 >
198 {lines.map((_, i) => i + 1).join("\n")}
199 </pre>
200 <pre className="pr-4 pl-5">{lines.join("\n")}</pre>
201 </div>
202 ) : (
203 <p className="p-6 text-sm text-muted">
204 This file is binary or too large to show.
205 </p>
206 )}
207 </div>
208 </div>
209 );
210}