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.tsxBlame418 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;
c81ab7aClaude11 forkCount?: number;
06d5ffeClaude12 currentUser?: string | null;
c81ab7aClaude13 forkedFrom?: string | null;
14}> = ({ owner, repo, starCount, starred, forkCount, currentUser, forkedFrom }) => (
fc1817aClaude15 <div class="repo-header">
c81ab7aClaude16 <div>
17 <div style="display: flex; align-items: center; gap: 8px; font-size: 20px">
18 <a href={`/${owner}`} class="owner">
19 {owner}
20 </a>
21 <span class="separator">/</span>
22 <a href={`/${owner}/${repo}`} class="name">
23 {repo}
24 </a>
25 </div>
26 {forkedFrom && (
27 <div style="font-size: 12px; color: var(--text-muted); margin-top: 2px">
28 forked from <a href={`/${forkedFrom}`}>{forkedFrom}</a>
29 </div>
30 )}
31 </div>
06d5ffeClaude32 <div class="repo-header-actions">
c81ab7aClaude33 {currentUser && currentUser !== owner && (
34 <form method="POST" action={`/${owner}/${repo}/fork`} style="display:inline">
35 <button type="submit" class="star-btn">
36 {"\u2442"} Fork {forkCount !== undefined && forkCount > 0 ? forkCount : ""}
37 </button>
38 </form>
39 )}
06d5ffeClaude40 {starCount !== undefined && (
41 currentUser ? (
42 <form method="POST" action={`/${owner}/${repo}/star`} style="display:inline">
43 <button
44 type="submit"
45 class={`star-btn${starred ? " starred" : ""}`}
46 >
47 {starred ? "\u2605" : "\u2606"} {starCount}
48 </button>
49 </form>
50 ) : (
51 <span class="star-btn">
52 {"\u2606"} {starCount}
53 </span>
54 )
55 )}
56 </div>
fc1817aClaude57 </div>
58);
59
60export const RepoNav: FC<{
61 owner: string;
62 repo: string;
3ef4c9dClaude63 active:
64 | "code"
65 | "commits"
66 | "issues"
67 | "pulls"
68 | "releases"
eafe8c6Claude69 | "actions"
3ef4c9dClaude70 | "gates"
71 | "insights";
fc1817aClaude72}> = ({ owner, repo, active }) => (
73 <div class="repo-nav">
06d5ffeClaude74 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude75 Code
76 </a>
79136bbClaude77 <a
78 href={`/${owner}/${repo}/issues`}
79 class={active === "issues" ? "active" : ""}
80 >
81 Issues
82 </a>
0074234Claude83 <a
84 href={`/${owner}/${repo}/pulls`}
85 class={active === "pulls" ? "active" : ""}
86 >
87 Pull Requests
88 </a>
fc1817aClaude89 <a
90 href={`/${owner}/${repo}/commits`}
91 class={active === "commits" ? "active" : ""}
92 >
93 Commits
94 </a>
eafe8c6Claude95 <a
96 href={`/${owner}/${repo}/actions`}
97 class={active === "actions" ? "active" : ""}
98 >
99 Actions
100 </a>
3ef4c9dClaude101 <a
102 href={`/${owner}/${repo}/releases`}
103 class={active === "releases" ? "active" : ""}
104 >
105 Releases
106 </a>
107 <a
108 href={`/${owner}/${repo}/gates`}
109 class={active === "gates" ? "active" : ""}
110 >
111 {"\u25CF"} Gates
112 </a>
113 <a
114 href={`/${owner}/${repo}/insights`}
115 class={active === "insights" ? "active" : ""}
116 >
117 Insights
118 </a>
119 <a href={`/${owner}/${repo}/ask`} style="margin-left: auto; color: #bc8cff">
120 {"\u2728"} Ask AI
121 </a>
fc1817aClaude122 </div>
123);
124
06d5ffeClaude125export const BranchSwitcher: FC<{
126 owner: string;
127 repo: string;
128 currentRef: string;
129 branches: string[];
130 pathType: "tree" | "blob" | "commits";
131 subPath?: string;
132}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
133 if (branches.length <= 1) {
134 return <div class="branch-selector">{currentRef}</div>;
135 }
136
137 return (
138 <div class="branch-dropdown">
139 <button class="branch-selector" type="button">
140 {currentRef} &#9662;
141 </button>
142 <div class="branch-dropdown-content">
143 {branches.map((branch) => {
144 let href: string;
145 if (pathType === "commits") {
146 href = `/${owner}/${repo}/commits/${branch}`;
147 } else if (subPath) {
148 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
149 } else {
150 href = `/${owner}/${repo}/tree/${branch}`;
151 }
152 return (
153 <a
154 href={href}
155 class={branch === currentRef ? "active-branch" : ""}
156 >
157 {branch}
158 </a>
159 );
160 })}
161 </div>
162 </div>
163 );
164};
165
fc1817aClaude166export const Breadcrumb: FC<{
167 owner: string;
168 repo: string;
169 ref: string;
170 path: string;
171}> = ({ owner, repo, ref, path }) => {
172 const parts = path.split("/").filter(Boolean);
173 const crumbs: { name: string; href: string }[] = [
174 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
175 ];
176 let accumulated = "";
177 for (const part of parts) {
178 accumulated += (accumulated ? "/" : "") + part;
179 crumbs.push({
180 name: part,
181 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
182 });
183 }
184 return (
185 <div class="breadcrumb">
186 {crumbs.map((crumb, i) => (
187 <>
188 {i > 0 && <span>/</span>}
189 {i === crumbs.length - 1 ? (
190 <strong>{crumb.name}</strong>
191 ) : (
192 <a href={crumb.href}>{crumb.name}</a>
193 )}
194 </>
195 ))}
196 </div>
197 );
198};
199
200export const FileTable: FC<{
201 entries: GitTreeEntry[];
202 owner: string;
203 repo: string;
204 ref: string;
205 path: string;
206}> = ({ entries, owner, repo, ref, path }) => (
207 <table class="file-table">
208 <tbody>
209 {entries.map((entry) => {
210 const fullPath = path ? `${path}/${entry.name}` : entry.name;
211 const href =
212 entry.type === "tree"
213 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
214 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
215 return (
216 <tr>
217 <td class="file-icon">
218 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
219 </td>
220 <td class="file-name">
221 <a href={href}>{entry.name}</a>
222 </td>
223 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
224 {entry.size !== undefined ? formatSize(entry.size) : ""}
225 </td>
226 </tr>
227 );
228 })}
229 </tbody>
230 </table>
231);
232
06d5ffeClaude233export const HighlightedCode: FC<{
234 highlightedHtml: string;
235 lineCount: number;
236}> = ({ highlightedHtml, lineCount }) => {
237 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
238 return (
239 <div class="blob-code">
240 <table>
241 <tbody>
242 <tr>
243 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
244 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
245 {lineNums.map((n) => (
246 <>
247 <span>{n}</span>
248 {"\n"}
249 </>
250 ))}
251 </pre>
252 </td>
253 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
254 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
255 </td>
256 </tr>
257 </tbody>
258 </table>
259 </div>
260 );
261};
262
263export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
264 <div class="blob-code">
265 <table>
266 <tbody>
267 {lines.map((line, i) => (
268 <tr>
269 <td class="line-num">{i + 1}</td>
270 <td class="line-content">{line}</td>
271 </tr>
272 ))}
273 </tbody>
274 </table>
275 </div>
276);
277
fc1817aClaude278export const CommitList: FC<{
279 commits: GitCommit[];
280 owner: string;
281 repo: string;
282}> = ({ commits, owner, repo }) => (
283 <div class="commit-list">
284 {commits.map((commit) => (
285 <div class="commit-item">
286 <div>
287 <div class="commit-message">
288 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
289 {commit.message}
290 </a>
291 </div>
292 <div class="commit-meta">
06d5ffeClaude293 {commit.author} committed {formatRelativeDate(commit.date)}
fc1817aClaude294 </div>
295 </div>
296 <a
297 href={`/${owner}/${repo}/commit/${commit.sha}`}
298 class="commit-sha"
299 >
300 {commit.sha.slice(0, 7)}
301 </a>
302 </div>
303 ))}
304 </div>
305);
306
307export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
308 raw,
309 files,
310}) => {
311 const sections = parseDiff(raw);
312
313 return (
314 <div class="diff-view">
315 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
316 Showing{" "}
317 <strong style="color: var(--text)">{files.length}</strong> changed
318 file{files.length !== 1 ? "s" : ""} with{" "}
319 <span class="stat-add">
320 +{files.reduce((s, f) => s + f.additions, 0)}
321 </span>{" "}
322 and{" "}
323 <span class="stat-del">
324 -{files.reduce((s, f) => s + f.deletions, 0)}
325 </span>
326 </div>
327 {sections.map((section) => (
328 <div class="diff-file">
329 <div class="diff-file-header">{section.path}</div>
330 <div class="diff-content">
331 {section.lines.map((line) => {
332 let cls = "line";
333 if (line.startsWith("+")) cls += " line-add";
334 else if (line.startsWith("-")) cls += " line-del";
335 else if (line.startsWith("@@")) cls += " line-hunk";
336 return <span class={cls}>{line + "\n"}</span>;
337 })}
338 </div>
339 </div>
340 ))}
341 </div>
342 );
343};
344
06d5ffeClaude345export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
346 repo,
347 ownerName,
348}) => (
349 <div class="card">
350 <h3>
351 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
352 </h3>
353 {repo.description && <p>{repo.description}</p>}
354 <div class="card-meta">
355 {repo.isPrivate && <span class="badge">Private</span>}
356 <span>{"\u2606"} {repo.starCount}</span>
357 {repo.pushedAt && (
358 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
359 )}
360 </div>
361 </div>
362);
363
fc1817aClaude364function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
365 const sections: Array<{ path: string; lines: string[] }> = [];
366 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
367 let current: { path: string; lines: string[] } | null = null;
368
369 for (const line of raw.split("\n")) {
370 const match = line.match(diffRegex);
371 if (match) {
372 if (current) sections.push(current);
373 current = { path: match[1], lines: [] };
374 continue;
375 }
376 if (current && !line.startsWith("diff --git")) {
377 if (
378 line.startsWith("index ") ||
379 line.startsWith("--- ") ||
380 line.startsWith("+++ ") ||
381 line.startsWith("new file") ||
382 line.startsWith("deleted file") ||
383 line.startsWith("old mode") ||
384 line.startsWith("new mode")
385 ) {
386 continue;
387 }
388 current.lines.push(line);
389 }
390 }
391 if (current) sections.push(current);
392 return sections;
393}
394
395function formatSize(bytes: number): string {
396 if (bytes < 1024) return `${bytes} B`;
397 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
398 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
399}
400
401function formatRelativeDate(dateStr: string): string {
402 const date = new Date(dateStr);
403 const now = new Date();
404 const diffMs = now.getTime() - date.getTime();
405 const diffMins = Math.floor(diffMs / 60000);
406 if (diffMins < 1) return "just now";
407 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
408 const diffHours = Math.floor(diffMins / 60);
409 if (diffHours < 24)
410 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
411 const diffDays = Math.floor(diffHours / 24);
412 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
413 return date.toLocaleDateString("en-US", {
414 month: "short",
415 day: "numeric",
416 year: "numeric",
417 });
418}