Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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.tsxBlame485 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;
71cd5ecClaude14 archived?: boolean;
15 isTemplate?: boolean;
16}> = ({
17 owner,
18 repo,
19 starCount,
20 starred,
21 forkCount,
22 currentUser,
23 forkedFrom,
24 archived,
25 isTemplate,
26}) => (
fc1817aClaude27 <div class="repo-header">
c81ab7aClaude28 <div>
29 <div style="display: flex; align-items: center; gap: 8px; font-size: 20px">
30 <a href={`/${owner}`} class="owner">
31 {owner}
32 </a>
33 <span class="separator">/</span>
34 <a href={`/${owner}/${repo}`} class="name">
35 {repo}
36 </a>
71cd5ecClaude37 {archived && (
38 <span
39 class="badge"
40 style="background:var(--bg-secondary);color:var(--text-muted);font-size:11px;padding:2px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:0.5px"
41 title="Read-only: pushes and new issues/PRs disabled"
42 >
43 Archived
44 </span>
45 )}
46 {isTemplate && (
47 <span
48 class="badge"
49 style="background:var(--bg-secondary);color:var(--accent);font-size:11px;padding:2px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:0.5px"
50 title="This repository can be used as a template"
51 >
52 Template
53 </span>
54 )}
c81ab7aClaude55 </div>
56 {forkedFrom && (
57 <div style="font-size: 12px; color: var(--text-muted); margin-top: 2px">
58 forked from <a href={`/${forkedFrom}`}>{forkedFrom}</a>
59 </div>
60 )}
61 </div>
06d5ffeClaude62 <div class="repo-header-actions">
c81ab7aClaude63 {currentUser && currentUser !== owner && (
45e31d0Claude64 <form method="post" action={`/${owner}/${repo}/fork`} style="display:inline">
c81ab7aClaude65 <button type="submit" class="star-btn">
66 {"\u2442"} Fork {forkCount !== undefined && forkCount > 0 ? forkCount : ""}
67 </button>
68 </form>
69 )}
06d5ffeClaude70 {starCount !== undefined && (
71 currentUser ? (
45e31d0Claude72 <form method="post" action={`/${owner}/${repo}/star`} style="display:inline">
06d5ffeClaude73 <button
74 type="submit"
75 class={`star-btn${starred ? " starred" : ""}`}
76 >
77 {starred ? "\u2605" : "\u2606"} {starCount}
78 </button>
79 </form>
80 ) : (
81 <span class="star-btn">
82 {"\u2606"} {starCount}
83 </span>
84 )
85 )}
86 </div>
fc1817aClaude87 </div>
88);
89
90export const RepoNav: FC<{
91 owner: string;
92 repo: string;
3ef4c9dClaude93 active:
94 | "code"
95 | "commits"
96 | "issues"
97 | "pulls"
98 | "releases"
eafe8c6Claude99 | "actions"
3ef4c9dClaude100 | "gates"
3cbe3d6Claude101 | "insights"
102 | "explain"
103 | "changelog"
1e162a8Claude104 | "semantic"
105 | "wiki"
0316dbbClaude106 | "projects"
107 | "settings";
fc1817aClaude108}> = ({ owner, repo, active }) => (
109 <div class="repo-nav">
06d5ffeClaude110 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude111 Code
112 </a>
79136bbClaude113 <a
114 href={`/${owner}/${repo}/issues`}
115 class={active === "issues" ? "active" : ""}
116 >
117 Issues
118 </a>
1e162a8Claude119 <a
120 href={`/${owner}/${repo}/wiki`}
121 class={active === "wiki" ? "active" : ""}
122 >
123 Wiki
124 </a>
0074234Claude125 <a
126 href={`/${owner}/${repo}/pulls`}
127 class={active === "pulls" ? "active" : ""}
128 >
129 Pull Requests
130 </a>
1e162a8Claude131 <a
132 href={`/${owner}/${repo}/projects`}
133 class={active === "projects" ? "active" : ""}
134 >
135 Projects
136 </a>
fc1817aClaude137 <a
138 href={`/${owner}/${repo}/commits`}
139 class={active === "commits" ? "active" : ""}
140 >
141 Commits
142 </a>
eafe8c6Claude143 <a
144 href={`/${owner}/${repo}/actions`}
145 class={active === "actions" ? "active" : ""}
146 >
147 Actions
148 </a>
3ef4c9dClaude149 <a
150 href={`/${owner}/${repo}/releases`}
151 class={active === "releases" ? "active" : ""}
152 >
153 Releases
154 </a>
155 <a
156 href={`/${owner}/${repo}/gates`}
157 class={active === "gates" ? "active" : ""}
158 >
159 {"\u25CF"} Gates
160 </a>
161 <a
162 href={`/${owner}/${repo}/insights`}
163 class={active === "insights" ? "active" : ""}
164 >
165 Insights
166 </a>
3cbe3d6Claude167 <a
168 href={`/${owner}/${repo}/explain`}
169 class={active === "explain" ? "active" : ""}
170 style="margin-left: auto; color: #bc8cff"
171 >
172 {"\u2728"} Explain
173 </a>
174 <a href={`/${owner}/${repo}/ask`} style="color: #bc8cff">
3ef4c9dClaude175 {"\u2728"} Ask AI
176 </a>
fc1817aClaude177 </div>
178);
179
06d5ffeClaude180export const BranchSwitcher: FC<{
181 owner: string;
182 repo: string;
183 currentRef: string;
184 branches: string[];
185 pathType: "tree" | "blob" | "commits";
186 subPath?: string;
187}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
188 if (branches.length <= 1) {
189 return <div class="branch-selector">{currentRef}</div>;
190 }
191
192 return (
193 <div class="branch-dropdown">
194 <button class="branch-selector" type="button">
195 {currentRef} &#9662;
196 </button>
197 <div class="branch-dropdown-content">
198 {branches.map((branch) => {
199 let href: string;
200 if (pathType === "commits") {
201 href = `/${owner}/${repo}/commits/${branch}`;
202 } else if (subPath) {
203 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
204 } else {
205 href = `/${owner}/${repo}/tree/${branch}`;
206 }
207 return (
208 <a
209 href={href}
210 class={branch === currentRef ? "active-branch" : ""}
211 >
212 {branch}
213 </a>
214 );
215 })}
216 </div>
217 </div>
218 );
219};
220
fc1817aClaude221export const Breadcrumb: FC<{
222 owner: string;
223 repo: string;
224 ref: string;
225 path: string;
226}> = ({ owner, repo, ref, path }) => {
227 const parts = path.split("/").filter(Boolean);
228 const crumbs: { name: string; href: string }[] = [
229 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
230 ];
231 let accumulated = "";
232 for (const part of parts) {
233 accumulated += (accumulated ? "/" : "") + part;
234 crumbs.push({
235 name: part,
236 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
237 });
238 }
239 return (
240 <div class="breadcrumb">
241 {crumbs.map((crumb, i) => (
242 <>
243 {i > 0 && <span>/</span>}
244 {i === crumbs.length - 1 ? (
245 <strong>{crumb.name}</strong>
246 ) : (
247 <a href={crumb.href}>{crumb.name}</a>
248 )}
249 </>
250 ))}
251 </div>
252 );
253};
254
255export const FileTable: FC<{
256 entries: GitTreeEntry[];
257 owner: string;
258 repo: string;
259 ref: string;
260 path: string;
261}> = ({ entries, owner, repo, ref, path }) => (
262 <table class="file-table">
263 <tbody>
264 {entries.map((entry) => {
265 const fullPath = path ? `${path}/${entry.name}` : entry.name;
266 const href =
267 entry.type === "tree"
268 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
269 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
270 return (
271 <tr>
272 <td class="file-icon">
273 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
274 </td>
275 <td class="file-name">
276 <a href={href}>{entry.name}</a>
277 </td>
278 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
279 {entry.size !== undefined ? formatSize(entry.size) : ""}
280 </td>
281 </tr>
282 );
283 })}
284 </tbody>
285 </table>
286);
287
06d5ffeClaude288export const HighlightedCode: FC<{
289 highlightedHtml: string;
290 lineCount: number;
291}> = ({ highlightedHtml, lineCount }) => {
292 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
293 return (
294 <div class="blob-code">
295 <table>
296 <tbody>
297 <tr>
298 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
299 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
300 {lineNums.map((n) => (
301 <>
302 <span>{n}</span>
303 {"\n"}
304 </>
305 ))}
306 </pre>
307 </td>
308 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
309 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
310 </td>
311 </tr>
312 </tbody>
313 </table>
314 </div>
315 );
316};
317
318export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
319 <div class="blob-code">
320 <table>
321 <tbody>
322 {lines.map((line, i) => (
323 <tr>
324 <td class="line-num">{i + 1}</td>
325 <td class="line-content">{line}</td>
326 </tr>
327 ))}
328 </tbody>
329 </table>
330 </div>
331);
332
fc1817aClaude333export const CommitList: FC<{
334 commits: GitCommit[];
335 owner: string;
336 repo: string;
3951454Claude337 verifications?: Record<string, { verified: boolean; reason: string }>;
338}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude339 <div class="commit-list">
3951454Claude340 {commits.map((commit) => {
341 const v = verifications?.[commit.sha];
342 return (
343 <div class="commit-item">
344 <div>
345 <div class="commit-message">
346 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
347 {commit.message}
348 </a>
349 {v?.verified && (
350 <span
351 title="Signed with a registered key"
352 style="margin-left:8px;font-size:10px;padding:1px 6px;border-radius:3px;background:var(--green,#2ea043);color:#fff;text-transform:uppercase;letter-spacing:.4px"
353 >
354 Verified
355 </span>
356 )}
357 </div>
358 <div class="commit-meta">
359 {commit.author} committed {formatRelativeDate(commit.date)}
360 </div>
fc1817aClaude361 </div>
3951454Claude362 <a
363 href={`/${owner}/${repo}/commit/${commit.sha}`}
364 class="commit-sha"
365 >
366 {commit.sha.slice(0, 7)}
367 </a>
fc1817aClaude368 </div>
3951454Claude369 );
370 })}
fc1817aClaude371 </div>
372);
373
374export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
375 raw,
376 files,
377}) => {
378 const sections = parseDiff(raw);
379
380 return (
381 <div class="diff-view">
382 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
383 Showing{" "}
384 <strong style="color: var(--text)">{files.length}</strong> changed
385 file{files.length !== 1 ? "s" : ""} with{" "}
386 <span class="stat-add">
387 +{files.reduce((s, f) => s + f.additions, 0)}
388 </span>{" "}
389 and{" "}
390 <span class="stat-del">
391 -{files.reduce((s, f) => s + f.deletions, 0)}
392 </span>
393 </div>
394 {sections.map((section) => (
395 <div class="diff-file">
396 <div class="diff-file-header">{section.path}</div>
397 <div class="diff-content">
398 {section.lines.map((line) => {
399 let cls = "line";
400 if (line.startsWith("+")) cls += " line-add";
401 else if (line.startsWith("-")) cls += " line-del";
402 else if (line.startsWith("@@")) cls += " line-hunk";
403 return <span class={cls}>{line + "\n"}</span>;
404 })}
405 </div>
406 </div>
407 ))}
408 </div>
409 );
410};
411
06d5ffeClaude412export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
413 repo,
414 ownerName,
415}) => (
416 <div class="card">
417 <h3>
418 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
419 </h3>
420 {repo.description && <p>{repo.description}</p>}
421 <div class="card-meta">
422 {repo.isPrivate && <span class="badge">Private</span>}
423 <span>{"\u2606"} {repo.starCount}</span>
424 {repo.pushedAt && (
425 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
426 )}
427 </div>
428 </div>
429);
430
fc1817aClaude431function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
432 const sections: Array<{ path: string; lines: string[] }> = [];
433 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
434 let current: { path: string; lines: string[] } | null = null;
435
436 for (const line of raw.split("\n")) {
437 const match = line.match(diffRegex);
438 if (match) {
439 if (current) sections.push(current);
440 current = { path: match[1], lines: [] };
441 continue;
442 }
443 if (current && !line.startsWith("diff --git")) {
444 if (
445 line.startsWith("index ") ||
446 line.startsWith("--- ") ||
447 line.startsWith("+++ ") ||
448 line.startsWith("new file") ||
449 line.startsWith("deleted file") ||
450 line.startsWith("old mode") ||
451 line.startsWith("new mode")
452 ) {
453 continue;
454 }
455 current.lines.push(line);
456 }
457 }
458 if (current) sections.push(current);
459 return sections;
460}
461
462function formatSize(bytes: number): string {
463 if (bytes < 1024) return `${bytes} B`;
464 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
465 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
466}
467
468function formatRelativeDate(dateStr: string): string {
469 const date = new Date(dateStr);
470 const now = new Date();
471 const diffMs = now.getTime() - date.getTime();
472 const diffMins = Math.floor(diffMs / 60000);
473 if (diffMins < 1) return "just now";
474 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
475 const diffHours = Math.floor(diffMins / 60);
476 if (diffHours < 24)
477 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
478 const diffDays = Math.floor(diffHours / 24);
479 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
480 return date.toLocaleDateString("en-US", {
481 month: "short",
482 day: "numeric",
483 year: "numeric",
484 });
485}