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