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