Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

components.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

components.tsxBlame365 lines · 1 contributor
fc1817aClaude1import type { FC } from "hono/jsx";
06d5ffeClaude2import { html } from "hono/html";
fc1817aClaude3import type { GitCommit, GitTreeEntry, GitDiffFile } from "../git/repository";
06d5ffeClaude4import type { Repository } from "../db/schema";
fc1817aClaude5
06d5ffeClaude6export const RepoHeader: FC<{
7 owner: string;
8 repo: string;
9 starCount?: number;
10 starred?: boolean;
11 currentUser?: string | null;
12}> = ({ owner, repo, starCount, starred, currentUser }) => (
fc1817aClaude13 <div class="repo-header">
14 <a href={`/${owner}`} class="owner">
15 {owner}
16 </a>
17 <span class="separator">/</span>
18 <a href={`/${owner}/${repo}`} class="name">
19 {repo}
20 </a>
06d5ffeClaude21 <div class="repo-header-actions">
22 {starCount !== undefined && (
23 currentUser ? (
24 <form method="POST" action={`/${owner}/${repo}/star`} style="display:inline">
25 <button
26 type="submit"
27 class={`star-btn${starred ? " starred" : ""}`}
28 >
29 {starred ? "\u2605" : "\u2606"} {starCount}
30 </button>
31 </form>
32 ) : (
33 <span class="star-btn">
34 {"\u2606"} {starCount}
35 </span>
36 )
37 )}
38 </div>
fc1817aClaude39 </div>
40);
41
42export const RepoNav: FC<{
43 owner: string;
44 repo: string;
0074234Claude45 active: "code" | "commits" | "issues" | "pulls";
fc1817aClaude46}> = ({ owner, repo, active }) => (
47 <div class="repo-nav">
06d5ffeClaude48 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude49 Code
50 </a>
79136bbClaude51 <a
52 href={`/${owner}/${repo}/issues`}
53 class={active === "issues" ? "active" : ""}
54 >
55 Issues
56 </a>
0074234Claude57 <a
58 href={`/${owner}/${repo}/pulls`}
59 class={active === "pulls" ? "active" : ""}
60 >
61 Pull Requests
62 </a>
fc1817aClaude63 <a
64 href={`/${owner}/${repo}/commits`}
65 class={active === "commits" ? "active" : ""}
66 >
67 Commits
68 </a>
69 </div>
70);
71
06d5ffeClaude72export const BranchSwitcher: FC<{
73 owner: string;
74 repo: string;
75 currentRef: string;
76 branches: string[];
77 pathType: "tree" | "blob" | "commits";
78 subPath?: string;
79}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
80 if (branches.length <= 1) {
81 return <div class="branch-selector">{currentRef}</div>;
82 }
83
84 return (
85 <div class="branch-dropdown">
86 <button class="branch-selector" type="button">
87 {currentRef} &#9662;
88 </button>
89 <div class="branch-dropdown-content">
90 {branches.map((branch) => {
91 let href: string;
92 if (pathType === "commits") {
93 href = `/${owner}/${repo}/commits/${branch}`;
94 } else if (subPath) {
95 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
96 } else {
97 href = `/${owner}/${repo}/tree/${branch}`;
98 }
99 return (
100 <a
101 href={href}
102 class={branch === currentRef ? "active-branch" : ""}
103 >
104 {branch}
105 </a>
106 );
107 })}
108 </div>
109 </div>
110 );
111};
112
fc1817aClaude113export const Breadcrumb: FC<{
114 owner: string;
115 repo: string;
116 ref: string;
117 path: string;
118}> = ({ owner, repo, ref, path }) => {
119 const parts = path.split("/").filter(Boolean);
120 const crumbs: { name: string; href: string }[] = [
121 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
122 ];
123 let accumulated = "";
124 for (const part of parts) {
125 accumulated += (accumulated ? "/" : "") + part;
126 crumbs.push({
127 name: part,
128 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
129 });
130 }
131 return (
132 <div class="breadcrumb">
133 {crumbs.map((crumb, i) => (
134 <>
135 {i > 0 && <span>/</span>}
136 {i === crumbs.length - 1 ? (
137 <strong>{crumb.name}</strong>
138 ) : (
139 <a href={crumb.href}>{crumb.name}</a>
140 )}
141 </>
142 ))}
143 </div>
144 );
145};
146
147export const FileTable: FC<{
148 entries: GitTreeEntry[];
149 owner: string;
150 repo: string;
151 ref: string;
152 path: string;
153}> = ({ entries, owner, repo, ref, path }) => (
154 <table class="file-table">
155 <tbody>
156 {entries.map((entry) => {
157 const fullPath = path ? `${path}/${entry.name}` : entry.name;
158 const href =
159 entry.type === "tree"
160 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
161 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
162 return (
163 <tr>
164 <td class="file-icon">
165 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
166 </td>
167 <td class="file-name">
168 <a href={href}>{entry.name}</a>
169 </td>
170 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
171 {entry.size !== undefined ? formatSize(entry.size) : ""}
172 </td>
173 </tr>
174 );
175 })}
176 </tbody>
177 </table>
178);
179
06d5ffeClaude180export const HighlightedCode: FC<{
181 highlightedHtml: string;
182 lineCount: number;
183}> = ({ highlightedHtml, lineCount }) => {
184 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
185 return (
186 <div class="blob-code">
187 <table>
188 <tbody>
189 <tr>
190 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
191 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
192 {lineNums.map((n) => (
193 <>
194 <span>{n}</span>
195 {"\n"}
196 </>
197 ))}
198 </pre>
199 </td>
200 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
201 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
202 </td>
203 </tr>
204 </tbody>
205 </table>
206 </div>
207 );
208};
209
210export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
211 <div class="blob-code">
212 <table>
213 <tbody>
214 {lines.map((line, i) => (
215 <tr>
216 <td class="line-num">{i + 1}</td>
217 <td class="line-content">{line}</td>
218 </tr>
219 ))}
220 </tbody>
221 </table>
222 </div>
223);
224
fc1817aClaude225export const CommitList: FC<{
226 commits: GitCommit[];
227 owner: string;
228 repo: string;
229}> = ({ commits, owner, repo }) => (
230 <div class="commit-list">
231 {commits.map((commit) => (
232 <div class="commit-item">
233 <div>
234 <div class="commit-message">
235 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
236 {commit.message}
237 </a>
238 </div>
239 <div class="commit-meta">
06d5ffeClaude240 {commit.author} committed {formatRelativeDate(commit.date)}
fc1817aClaude241 </div>
242 </div>
243 <a
244 href={`/${owner}/${repo}/commit/${commit.sha}`}
245 class="commit-sha"
246 >
247 {commit.sha.slice(0, 7)}
248 </a>
249 </div>
250 ))}
251 </div>
252);
253
254export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
255 raw,
256 files,
257}) => {
258 const sections = parseDiff(raw);
259
260 return (
261 <div class="diff-view">
262 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
263 Showing{" "}
264 <strong style="color: var(--text)">{files.length}</strong> changed
265 file{files.length !== 1 ? "s" : ""} with{" "}
266 <span class="stat-add">
267 +{files.reduce((s, f) => s + f.additions, 0)}
268 </span>{" "}
269 and{" "}
270 <span class="stat-del">
271 -{files.reduce((s, f) => s + f.deletions, 0)}
272 </span>
273 </div>
274 {sections.map((section) => (
275 <div class="diff-file">
276 <div class="diff-file-header">{section.path}</div>
277 <div class="diff-content">
278 {section.lines.map((line) => {
279 let cls = "line";
280 if (line.startsWith("+")) cls += " line-add";
281 else if (line.startsWith("-")) cls += " line-del";
282 else if (line.startsWith("@@")) cls += " line-hunk";
283 return <span class={cls}>{line + "\n"}</span>;
284 })}
285 </div>
286 </div>
287 ))}
288 </div>
289 );
290};
291
06d5ffeClaude292export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
293 repo,
294 ownerName,
295}) => (
296 <div class="card">
297 <h3>
298 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
299 </h3>
300 {repo.description && <p>{repo.description}</p>}
301 <div class="card-meta">
302 {repo.isPrivate && <span class="badge">Private</span>}
303 <span>{"\u2606"} {repo.starCount}</span>
304 {repo.pushedAt && (
305 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
306 )}
307 </div>
308 </div>
309);
310
fc1817aClaude311function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
312 const sections: Array<{ path: string; lines: string[] }> = [];
313 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
314 let current: { path: string; lines: string[] } | null = null;
315
316 for (const line of raw.split("\n")) {
317 const match = line.match(diffRegex);
318 if (match) {
319 if (current) sections.push(current);
320 current = { path: match[1], lines: [] };
321 continue;
322 }
323 if (current && !line.startsWith("diff --git")) {
324 if (
325 line.startsWith("index ") ||
326 line.startsWith("--- ") ||
327 line.startsWith("+++ ") ||
328 line.startsWith("new file") ||
329 line.startsWith("deleted file") ||
330 line.startsWith("old mode") ||
331 line.startsWith("new mode")
332 ) {
333 continue;
334 }
335 current.lines.push(line);
336 }
337 }
338 if (current) sections.push(current);
339 return sections;
340}
341
342function formatSize(bytes: number): string {
343 if (bytes < 1024) return `${bytes} B`;
344 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
345 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
346}
347
348function formatRelativeDate(dateStr: string): string {
349 const date = new Date(dateStr);
350 const now = new Date();
351 const diffMs = now.getTime() - date.getTime();
352 const diffMins = Math.floor(diffMs / 60000);
353 if (diffMins < 1) return "just now";
354 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
355 const diffHours = Math.floor(diffMins / 60);
356 if (diffHours < 24)
357 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
358 const diffDays = Math.floor(diffHours / 24);
359 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
360 return date.toLocaleDateString("en-US", {
361 month: "short",
362 day: "numeric",
363 year: "numeric",
364 });
365}