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.tsxBlame442 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"
3cbe3d6Claude71 | "insights"
72 | "explain"
73 | "changelog"
1e162a8Claude74 | "semantic"
75 | "wiki"
76 | "projects";
fc1817aClaude77}> = ({ owner, repo, active }) => (
78 <div class="repo-nav">
06d5ffeClaude79 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude80 Code
81 </a>
79136bbClaude82 <a
83 href={`/${owner}/${repo}/issues`}
84 class={active === "issues" ? "active" : ""}
85 >
86 Issues
87 </a>
1e162a8Claude88 <a
89 href={`/${owner}/${repo}/wiki`}
90 class={active === "wiki" ? "active" : ""}
91 >
92 Wiki
93 </a>
0074234Claude94 <a
95 href={`/${owner}/${repo}/pulls`}
96 class={active === "pulls" ? "active" : ""}
97 >
98 Pull Requests
99 </a>
1e162a8Claude100 <a
101 href={`/${owner}/${repo}/projects`}
102 class={active === "projects" ? "active" : ""}
103 >
104 Projects
105 </a>
fc1817aClaude106 <a
107 href={`/${owner}/${repo}/commits`}
108 class={active === "commits" ? "active" : ""}
109 >
110 Commits
111 </a>
eafe8c6Claude112 <a
113 href={`/${owner}/${repo}/actions`}
114 class={active === "actions" ? "active" : ""}
115 >
116 Actions
117 </a>
3ef4c9dClaude118 <a
119 href={`/${owner}/${repo}/releases`}
120 class={active === "releases" ? "active" : ""}
121 >
122 Releases
123 </a>
124 <a
125 href={`/${owner}/${repo}/gates`}
126 class={active === "gates" ? "active" : ""}
127 >
128 {"\u25CF"} Gates
129 </a>
130 <a
131 href={`/${owner}/${repo}/insights`}
132 class={active === "insights" ? "active" : ""}
133 >
134 Insights
135 </a>
3cbe3d6Claude136 <a
137 href={`/${owner}/${repo}/explain`}
138 class={active === "explain" ? "active" : ""}
139 style="margin-left: auto; color: #bc8cff"
140 >
141 {"\u2728"} Explain
142 </a>
143 <a href={`/${owner}/${repo}/ask`} style="color: #bc8cff">
3ef4c9dClaude144 {"\u2728"} Ask AI
145 </a>
fc1817aClaude146 </div>
147);
148
06d5ffeClaude149export const BranchSwitcher: FC<{
150 owner: string;
151 repo: string;
152 currentRef: string;
153 branches: string[];
154 pathType: "tree" | "blob" | "commits";
155 subPath?: string;
156}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
157 if (branches.length <= 1) {
158 return <div class="branch-selector">{currentRef}</div>;
159 }
160
161 return (
162 <div class="branch-dropdown">
163 <button class="branch-selector" type="button">
164 {currentRef} &#9662;
165 </button>
166 <div class="branch-dropdown-content">
167 {branches.map((branch) => {
168 let href: string;
169 if (pathType === "commits") {
170 href = `/${owner}/${repo}/commits/${branch}`;
171 } else if (subPath) {
172 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
173 } else {
174 href = `/${owner}/${repo}/tree/${branch}`;
175 }
176 return (
177 <a
178 href={href}
179 class={branch === currentRef ? "active-branch" : ""}
180 >
181 {branch}
182 </a>
183 );
184 })}
185 </div>
186 </div>
187 );
188};
189
fc1817aClaude190export const Breadcrumb: FC<{
191 owner: string;
192 repo: string;
193 ref: string;
194 path: string;
195}> = ({ owner, repo, ref, path }) => {
196 const parts = path.split("/").filter(Boolean);
197 const crumbs: { name: string; href: string }[] = [
198 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
199 ];
200 let accumulated = "";
201 for (const part of parts) {
202 accumulated += (accumulated ? "/" : "") + part;
203 crumbs.push({
204 name: part,
205 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
206 });
207 }
208 return (
209 <div class="breadcrumb">
210 {crumbs.map((crumb, i) => (
211 <>
212 {i > 0 && <span>/</span>}
213 {i === crumbs.length - 1 ? (
214 <strong>{crumb.name}</strong>
215 ) : (
216 <a href={crumb.href}>{crumb.name}</a>
217 )}
218 </>
219 ))}
220 </div>
221 );
222};
223
224export const FileTable: FC<{
225 entries: GitTreeEntry[];
226 owner: string;
227 repo: string;
228 ref: string;
229 path: string;
230}> = ({ entries, owner, repo, ref, path }) => (
231 <table class="file-table">
232 <tbody>
233 {entries.map((entry) => {
234 const fullPath = path ? `${path}/${entry.name}` : entry.name;
235 const href =
236 entry.type === "tree"
237 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
238 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
239 return (
240 <tr>
241 <td class="file-icon">
242 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
243 </td>
244 <td class="file-name">
245 <a href={href}>{entry.name}</a>
246 </td>
247 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
248 {entry.size !== undefined ? formatSize(entry.size) : ""}
249 </td>
250 </tr>
251 );
252 })}
253 </tbody>
254 </table>
255);
256
06d5ffeClaude257export const HighlightedCode: FC<{
258 highlightedHtml: string;
259 lineCount: number;
260}> = ({ highlightedHtml, lineCount }) => {
261 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
262 return (
263 <div class="blob-code">
264 <table>
265 <tbody>
266 <tr>
267 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
268 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
269 {lineNums.map((n) => (
270 <>
271 <span>{n}</span>
272 {"\n"}
273 </>
274 ))}
275 </pre>
276 </td>
277 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
278 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
279 </td>
280 </tr>
281 </tbody>
282 </table>
283 </div>
284 );
285};
286
287export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
288 <div class="blob-code">
289 <table>
290 <tbody>
291 {lines.map((line, i) => (
292 <tr>
293 <td class="line-num">{i + 1}</td>
294 <td class="line-content">{line}</td>
295 </tr>
296 ))}
297 </tbody>
298 </table>
299 </div>
300);
301
fc1817aClaude302export const CommitList: FC<{
303 commits: GitCommit[];
304 owner: string;
305 repo: string;
306}> = ({ commits, owner, repo }) => (
307 <div class="commit-list">
308 {commits.map((commit) => (
309 <div class="commit-item">
310 <div>
311 <div class="commit-message">
312 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
313 {commit.message}
314 </a>
315 </div>
316 <div class="commit-meta">
06d5ffeClaude317 {commit.author} committed {formatRelativeDate(commit.date)}
fc1817aClaude318 </div>
319 </div>
320 <a
321 href={`/${owner}/${repo}/commit/${commit.sha}`}
322 class="commit-sha"
323 >
324 {commit.sha.slice(0, 7)}
325 </a>
326 </div>
327 ))}
328 </div>
329);
330
331export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
332 raw,
333 files,
334}) => {
335 const sections = parseDiff(raw);
336
337 return (
338 <div class="diff-view">
339 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
340 Showing{" "}
341 <strong style="color: var(--text)">{files.length}</strong> changed
342 file{files.length !== 1 ? "s" : ""} with{" "}
343 <span class="stat-add">
344 +{files.reduce((s, f) => s + f.additions, 0)}
345 </span>{" "}
346 and{" "}
347 <span class="stat-del">
348 -{files.reduce((s, f) => s + f.deletions, 0)}
349 </span>
350 </div>
351 {sections.map((section) => (
352 <div class="diff-file">
353 <div class="diff-file-header">{section.path}</div>
354 <div class="diff-content">
355 {section.lines.map((line) => {
356 let cls = "line";
357 if (line.startsWith("+")) cls += " line-add";
358 else if (line.startsWith("-")) cls += " line-del";
359 else if (line.startsWith("@@")) cls += " line-hunk";
360 return <span class={cls}>{line + "\n"}</span>;
361 })}
362 </div>
363 </div>
364 ))}
365 </div>
366 );
367};
368
06d5ffeClaude369export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
370 repo,
371 ownerName,
372}) => (
373 <div class="card">
374 <h3>
375 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
376 </h3>
377 {repo.description && <p>{repo.description}</p>}
378 <div class="card-meta">
379 {repo.isPrivate && <span class="badge">Private</span>}
380 <span>{"\u2606"} {repo.starCount}</span>
381 {repo.pushedAt && (
382 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
383 )}
384 </div>
385 </div>
386);
387
fc1817aClaude388function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
389 const sections: Array<{ path: string; lines: string[] }> = [];
390 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
391 let current: { path: string; lines: string[] } | null = null;
392
393 for (const line of raw.split("\n")) {
394 const match = line.match(diffRegex);
395 if (match) {
396 if (current) sections.push(current);
397 current = { path: match[1], lines: [] };
398 continue;
399 }
400 if (current && !line.startsWith("diff --git")) {
401 if (
402 line.startsWith("index ") ||
403 line.startsWith("--- ") ||
404 line.startsWith("+++ ") ||
405 line.startsWith("new file") ||
406 line.startsWith("deleted file") ||
407 line.startsWith("old mode") ||
408 line.startsWith("new mode")
409 ) {
410 continue;
411 }
412 current.lines.push(line);
413 }
414 }
415 if (current) sections.push(current);
416 return sections;
417}
418
419function formatSize(bytes: number): string {
420 if (bytes < 1024) return `${bytes} B`;
421 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
422 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
423}
424
425function formatRelativeDate(dateStr: string): string {
426 const date = new Date(dateStr);
427 const now = new Date();
428 const diffMs = now.getTime() - date.getTime();
429 const diffMins = Math.floor(diffMs / 60000);
430 if (diffMins < 1) return "just now";
431 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
432 const diffHours = Math.floor(diffMins / 60);
433 if (diffHours < 24)
434 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
435 const diffDays = Math.floor(diffHours / 24);
436 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
437 return date.toLocaleDateString("en-US", {
438 month: "short",
439 day: "numeric",
440 year: "numeric",
441 });
442}