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.tsxBlame503 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"
eb3b81aClaude107 | "settings"
108 | "ask"
a7361c0Claude109 | "spec"
110 | "health";
fc1817aClaude111}> = ({ owner, repo, active }) => (
112 <div class="repo-nav">
06d5ffeClaude113 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude114 Code
115 </a>
79136bbClaude116 <a
117 href={`/${owner}/${repo}/issues`}
118 class={active === "issues" ? "active" : ""}
119 >
120 Issues
121 </a>
1e162a8Claude122 <a
123 href={`/${owner}/${repo}/wiki`}
124 class={active === "wiki" ? "active" : ""}
125 >
126 Wiki
127 </a>
0074234Claude128 <a
129 href={`/${owner}/${repo}/pulls`}
130 class={active === "pulls" ? "active" : ""}
131 >
132 Pull Requests
133 </a>
1e162a8Claude134 <a
135 href={`/${owner}/${repo}/projects`}
136 class={active === "projects" ? "active" : ""}
137 >
138 Projects
139 </a>
fc1817aClaude140 <a
141 href={`/${owner}/${repo}/commits`}
142 class={active === "commits" ? "active" : ""}
143 >
144 Commits
145 </a>
eafe8c6Claude146 <a
147 href={`/${owner}/${repo}/actions`}
148 class={active === "actions" ? "active" : ""}
149 >
150 Actions
151 </a>
3ef4c9dClaude152 <a
153 href={`/${owner}/${repo}/releases`}
154 class={active === "releases" ? "active" : ""}
155 >
156 Releases
157 </a>
158 <a
159 href={`/${owner}/${repo}/gates`}
160 class={active === "gates" ? "active" : ""}
161 >
162 {"\u25CF"} Gates
163 </a>
164 <a
165 href={`/${owner}/${repo}/insights`}
166 class={active === "insights" ? "active" : ""}
167 >
168 Insights
169 </a>
a7361c0Claude170 <a
171 href={`/${owner}/${repo}/health`}
172 class={active === "health" ? "active" : ""}
173 title="Code Health Score"
174 >
175 {"⬡"} Health
176 </a>
3cbe3d6Claude177 <a
178 href={`/${owner}/${repo}/explain`}
179 class={active === "explain" ? "active" : ""}
180 style="margin-left: auto; color: #bc8cff"
181 >
182 {"\u2728"} Explain
183 </a>
eb3b81aClaude184 <a href={`/${owner}/${repo}/ask`} class={active === "ask" ? "active" : ""} style="color: #bc8cff">
3ef4c9dClaude185 {"\u2728"} Ask AI
186 </a>
14c3cc8Claude187 <a
188 href={`/${owner}/${repo}/spec`}
eb3b81aClaude189 class={active === "spec" ? "active" : ""}
14c3cc8Claude190 style="color: #bc8cff"
191 title="Spec to PR — paste a feature spec, AI opens a draft PR"
192 >
193 {"\u2728"} Spec
194 </a>
fc1817aClaude195 </div>
196);
197
06d5ffeClaude198export const BranchSwitcher: FC<{
199 owner: string;
200 repo: string;
201 currentRef: string;
202 branches: string[];
203 pathType: "tree" | "blob" | "commits";
204 subPath?: string;
205}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
206 if (branches.length <= 1) {
207 return <div class="branch-selector">{currentRef}</div>;
208 }
209
210 return (
211 <div class="branch-dropdown">
212 <button class="branch-selector" type="button">
213 {currentRef} &#9662;
214 </button>
215 <div class="branch-dropdown-content">
216 {branches.map((branch) => {
217 let href: string;
218 if (pathType === "commits") {
219 href = `/${owner}/${repo}/commits/${branch}`;
220 } else if (subPath) {
221 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
222 } else {
223 href = `/${owner}/${repo}/tree/${branch}`;
224 }
225 return (
226 <a
227 href={href}
228 class={branch === currentRef ? "active-branch" : ""}
229 >
230 {branch}
231 </a>
232 );
233 })}
234 </div>
235 </div>
236 );
237};
238
fc1817aClaude239export const Breadcrumb: FC<{
240 owner: string;
241 repo: string;
242 ref: string;
243 path: string;
244}> = ({ owner, repo, ref, path }) => {
245 const parts = path.split("/").filter(Boolean);
246 const crumbs: { name: string; href: string }[] = [
247 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
248 ];
249 let accumulated = "";
250 for (const part of parts) {
251 accumulated += (accumulated ? "/" : "") + part;
252 crumbs.push({
253 name: part,
254 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
255 });
256 }
257 return (
258 <div class="breadcrumb">
259 {crumbs.map((crumb, i) => (
eb3b81aClaude260 <span key={crumb.href}>
fc1817aClaude261 {i > 0 && <span>/</span>}
262 {i === crumbs.length - 1 ? (
263 <strong>{crumb.name}</strong>
264 ) : (
265 <a href={crumb.href}>{crumb.name}</a>
266 )}
eb3b81aClaude267 </span>
fc1817aClaude268 ))}
269 </div>
270 );
271};
272
273export const FileTable: FC<{
274 entries: GitTreeEntry[];
275 owner: string;
276 repo: string;
277 ref: string;
278 path: string;
279}> = ({ entries, owner, repo, ref, path }) => (
280 <table class="file-table">
281 <tbody>
282 {entries.map((entry) => {
283 const fullPath = path ? `${path}/${entry.name}` : entry.name;
284 const href =
285 entry.type === "tree"
286 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
287 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
288 return (
289 <tr>
290 <td class="file-icon">
291 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
292 </td>
293 <td class="file-name">
294 <a href={href}>{entry.name}</a>
295 </td>
296 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
297 {entry.size !== undefined ? formatSize(entry.size) : ""}
298 </td>
299 </tr>
300 );
301 })}
302 </tbody>
303 </table>
304);
305
06d5ffeClaude306export const HighlightedCode: FC<{
307 highlightedHtml: string;
308 lineCount: number;
309}> = ({ highlightedHtml, lineCount }) => {
310 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
311 return (
312 <div class="blob-code">
313 <table>
314 <tbody>
315 <tr>
316 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
317 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
318 {lineNums.map((n) => (
319 <>
320 <span>{n}</span>
321 {"\n"}
322 </>
323 ))}
324 </pre>
325 </td>
326 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
327 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
328 </td>
329 </tr>
330 </tbody>
331 </table>
332 </div>
333 );
334};
335
336export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
337 <div class="blob-code">
338 <table>
339 <tbody>
340 {lines.map((line, i) => (
341 <tr>
342 <td class="line-num">{i + 1}</td>
343 <td class="line-content">{line}</td>
344 </tr>
345 ))}
346 </tbody>
347 </table>
348 </div>
349);
350
fc1817aClaude351export const CommitList: FC<{
352 commits: GitCommit[];
353 owner: string;
354 repo: string;
3951454Claude355 verifications?: Record<string, { verified: boolean; reason: string }>;
356}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude357 <div class="commit-list">
3951454Claude358 {commits.map((commit) => {
359 const v = verifications?.[commit.sha];
360 return (
361 <div class="commit-item">
362 <div>
363 <div class="commit-message">
364 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
365 {commit.message}
366 </a>
367 {v?.verified && (
368 <span
369 title="Signed with a registered key"
370 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"
371 >
372 Verified
373 </span>
374 )}
375 </div>
376 <div class="commit-meta">
377 {commit.author} committed {formatRelativeDate(commit.date)}
378 </div>
fc1817aClaude379 </div>
3951454Claude380 <a
381 href={`/${owner}/${repo}/commit/${commit.sha}`}
382 class="commit-sha"
383 >
384 {commit.sha.slice(0, 7)}
385 </a>
fc1817aClaude386 </div>
3951454Claude387 );
388 })}
fc1817aClaude389 </div>
390);
391
392export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
393 raw,
394 files,
395}) => {
396 const sections = parseDiff(raw);
397
398 return (
399 <div class="diff-view">
400 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
401 Showing{" "}
402 <strong style="color: var(--text)">{files.length}</strong> changed
403 file{files.length !== 1 ? "s" : ""} with{" "}
404 <span class="stat-add">
405 +{files.reduce((s, f) => s + f.additions, 0)}
406 </span>{" "}
407 and{" "}
408 <span class="stat-del">
409 -{files.reduce((s, f) => s + f.deletions, 0)}
410 </span>
411 </div>
412 {sections.map((section) => (
413 <div class="diff-file">
414 <div class="diff-file-header">{section.path}</div>
415 <div class="diff-content">
416 {section.lines.map((line) => {
417 let cls = "line";
418 if (line.startsWith("+")) cls += " line-add";
419 else if (line.startsWith("-")) cls += " line-del";
420 else if (line.startsWith("@@")) cls += " line-hunk";
421 return <span class={cls}>{line + "\n"}</span>;
422 })}
423 </div>
424 </div>
425 ))}
426 </div>
427 );
428};
429
06d5ffeClaude430export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
431 repo,
432 ownerName,
433}) => (
434 <div class="card">
435 <h3>
436 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
437 </h3>
438 {repo.description && <p>{repo.description}</p>}
439 <div class="card-meta">
440 {repo.isPrivate && <span class="badge">Private</span>}
441 <span>{"\u2606"} {repo.starCount}</span>
442 {repo.pushedAt && (
443 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
444 )}
445 </div>
446 </div>
447);
448
fc1817aClaude449function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
450 const sections: Array<{ path: string; lines: string[] }> = [];
451 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
452 let current: { path: string; lines: string[] } | null = null;
453
454 for (const line of raw.split("\n")) {
455 const match = line.match(diffRegex);
456 if (match) {
457 if (current) sections.push(current);
458 current = { path: match[1], lines: [] };
459 continue;
460 }
461 if (current && !line.startsWith("diff --git")) {
462 if (
463 line.startsWith("index ") ||
464 line.startsWith("--- ") ||
465 line.startsWith("+++ ") ||
466 line.startsWith("new file") ||
467 line.startsWith("deleted file") ||
468 line.startsWith("old mode") ||
469 line.startsWith("new mode")
470 ) {
471 continue;
472 }
473 current.lines.push(line);
474 }
475 }
476 if (current) sections.push(current);
477 return sections;
478}
479
480function formatSize(bytes: number): string {
481 if (bytes < 1024) return `${bytes} B`;
482 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
483 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
484}
485
486function formatRelativeDate(dateStr: string): string {
487 const date = new Date(dateStr);
488 const now = new Date();
489 const diffMs = now.getTime() - date.getTime();
490 const diffMins = Math.floor(diffMs / 60000);
491 if (diffMins < 1) return "just now";
492 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
493 const diffHours = Math.floor(diffMins / 60);
494 if (diffHours < 24)
495 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
496 const diffDays = Math.floor(diffHours / 24);
497 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
498 return date.toLocaleDateString("en-US", {
499 month: "short",
500 day: "numeric",
501 year: "numeric",
502 });
503}