✨ feat: update [...path].astro page
swadhinbiswas committed 3 months ago
e27c69f
1 parent 8674de0
commit e27c69f
1 file changed
+54 -25
Showing 1 changed file+54-25
▼src/pages/[owner]/[repo]/blame/[branch]/[...path].astro
+54-25
| @@ -7,12 +7,10 @@ import { canReadRepo } from "@/lib/permissions"; | ||
7 | 7 | import { getBlame, getLastCommit } from "@/lib/git"; |
8 | 8 | import { resolveRepoPath } from "@/lib/git-storage"; |
9 | 9 | import { GitBranch, ChevronDown, FileCode } from "lucide-react"; |
10 | -import { createHighlighter } from "shiki"; | |
11 | 10 | |
12 | 11 | const { owner: ownerName, repo: repoName, branch, path } = Astro.params; |
13 | 12 | const db = getDatabase(); |
14 | 13 | |
15 | -// Fetch Owner | |
16 | 14 | const user = await db.query.users.findFirst({ |
17 | 15 | where: eq(schema.users.username, ownerName!), |
18 | 16 | }); |
| @@ -27,24 +25,19 @@ const repoData = await db.query.repositories.findFirst({ | ||
27 | 25 | }); |
28 | 26 | if (!repoData) return Astro.redirect("/404"); |
29 | 27 | |
30 | -// Check permissions | |
31 | 28 | const currentUser = Astro.locals.user; |
32 | 29 | const hasAccess = await canReadRepo(currentUser?.id, repoData); |
33 | 30 | if (!hasAccess) return Astro.redirect("/404"); |
34 | 31 | |
35 | 32 | const currentBranch = branch || repoData.defaultBranch; |
36 | 33 | const currentPath = path || ""; |
37 | - | |
38 | 34 | const resolvedLocalPath = await resolveRepoPath(repoData.diskPath); |
39 | 35 | |
40 | -// Fetch blame data | |
41 | 36 | const blameLines = await getBlame(resolvedLocalPath, currentPath, currentBranch); |
42 | - | |
43 | 37 | if (!blameLines || blameLines.length === 0) { |
44 | 38 | return Astro.redirect("/404"); |
45 | 39 | } |
46 | 40 | |
47 | -// Syntax highlighting | |
48 | 41 | const extension = currentPath.split(".").pop()?.toLowerCase() || ""; |
49 | 42 | const commonLangs: Record<string, string> = { |
50 | 43 | ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", |
| @@ -56,9 +49,9 @@ const commonLangs: Record<string, string> = { | ||
56 | 49 | }; |
57 | 50 | const lang = commonLangs[extension] || "text"; |
58 | 51 | |
59 | -// Highlight each line individually for blame row alignment | |
60 | 52 | let highlightedLines: string[] = []; |
61 | 53 | try { |
54 | + const { createHighlighter } = await import("shiki"); | |
62 | 55 | const highlighter = await createHighlighter({ |
63 | 56 | themes: ["github-dark"], |
64 | 57 | langs: [lang, "text"], |
| @@ -68,7 +61,6 @@ try { | ||
68 | 61 | lang: Object.values(commonLangs).includes(lang) ? lang : "text", |
69 | 62 | theme: "github-dark", |
70 | 63 | }); |
71 | - // Extract individual lines from the shiki output | |
72 | 64 | const lineMatches = html.match(/<span class="line">.*?<\/span>/gs); |
73 | 65 | highlightedLines = lineMatches || blameLines.map((b) => `<span class="line">${escapeHtml(b.content)}</span>`); |
74 | 66 | } catch { |
| @@ -76,25 +68,31 @@ try { | ||
76 | 68 | } |
77 | 69 | |
78 | 70 | function escapeHtml(text: string): string { |
79 | - return text | |
80 | - .replace(/&/g, "&") | |
81 | - .replace(/</g, "<") | |
82 | - .replace(/>/g, ">") | |
83 | - .replace(/"/g, """); | |
71 | + return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); | |
84 | 72 | } |
85 | 73 | |
86 | -// Group consecutive lines by same commit SHA to show commit info only once per block | |
87 | 74 | interface BlameBlock { |
88 | 75 | sha: string; |
89 | 76 | author: string; |
90 | 77 | email: string; |
91 | 78 | date: Date; |
79 | + message: string; | |
92 | 80 | lines: { lineNum: number; content: string; highlighted: string }[]; |
93 | 81 | } |
94 | 82 | |
95 | 83 | const blameBlocks: BlameBlock[] = []; |
96 | 84 | let currentBlock: BlameBlock | null = null; |
97 | 85 | |
86 | +// Fetch commit messages for unique SHAs | |
87 | +const uniqueShas = [...new Set(blameLines.map(b => b.sha))]; | |
88 | +const commitMessages = new Map<string, string>(); | |
89 | +for (const sha of uniqueShas.slice(0, 50)) { | |
90 | + try { | |
91 | + const commit = await getLastCommit(resolvedLocalPath, sha, ""); | |
92 | + if (commit) commitMessages.set(sha, commit.message.split('\n')[0]); | |
93 | + } catch {} | |
94 | +} | |
95 | + | |
98 | 96 | for (let i = 0; i < blameLines.length; i++) { |
99 | 97 | const bl = blameLines[i]; |
100 | 98 | if (!currentBlock || currentBlock.sha !== bl.sha) { |
| @@ -103,6 +101,7 @@ for (let i = 0; i < blameLines.length; i++) { | ||
103 | 101 | author: bl.author, |
104 | 102 | email: bl.email, |
105 | 103 | date: bl.date, |
104 | + message: commitMessages.get(bl.sha) || "", | |
106 | 105 | lines: [], |
107 | 106 | }; |
108 | 107 | blameBlocks.push(currentBlock); |
| @@ -129,6 +128,10 @@ function timeAgo(date: Date | string | number): string { | ||
129 | 128 | return `${Math.floor(months / 12)}y ago`; |
130 | 129 | } |
131 | 130 | |
131 | +function formatDate(date: Date): string { | |
132 | + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
133 | +} | |
134 | + | |
132 | 135 | const repo = { |
133 | 136 | owner: repoData.owner.username, |
134 | 137 | name: repoData.name, |
| @@ -149,7 +152,7 @@ const repo = { | ||
149 | 152 | <RepoHeader repo={repo} activeTab="code" /> |
150 | 153 | |
151 | 154 | <div class="space-y-4"> |
152 | - <!-- Breadcrumbs --> | |
155 | + <!-- Breadcrumbs & Actions --> | |
153 | 156 | <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> |
154 | 157 | <div class="flex items-center gap-2"> |
155 | 158 | <div class="relative"> |
| @@ -184,7 +187,7 @@ const repo = { | ||
184 | 187 | </div> |
185 | 188 | </div> |
186 | 189 | |
187 | - <!-- Header --> | |
190 | + <!-- Blame Header --> | |
188 | 191 | <div class="rounded-t-lg border bg-muted/30 p-3 border-b-0 flex items-center gap-2 text-sm text-muted-foreground"> |
189 | 192 | <FileCode className="h-4 w-4" /> |
190 | 193 | <span class="font-medium text-foreground">{blameLines.length} lines</span> |
| @@ -199,34 +202,60 @@ const repo = { | ||
199 | 202 | {blameBlocks.map((block, blockIdx) => ( |
200 | 203 | block.lines.map((line, lineIdx) => ( |
201 | 204 | <tr class:list={[ |
202 | - "hover:bg-accent transition-colors", | |
203 | - lineIdx === 0 && blockIdx > 0 ? "border-t border-gray-700" : "", | |
205 | + "group/line hover:bg-accent/50 transition-colors relative", | |
206 | + lineIdx === 0 && blockIdx > 0 ? "border-t border-border" : "", | |
204 | 207 | ]}> |
205 | 208 | {/* Blame info column — show only on first line of block */} |
206 | 209 | <td class:list={[ |
207 | - "align-top whitespace-nowrap px-3 py-0 text-xs text-muted-foreground select-none border-r border-gray-800 min-w-[220px] max-w-[280px]", | |
210 | + "align-top whitespace-nowrap px-3 py-0 text-xs select-none border-r border-border min-w-[220px] max-w-[280px] relative", | |
208 | 211 | lineIdx === 0 ? "pt-1" : "", |
209 | 212 | ]}> |
210 | - {lineIdx === 0 && ( | |
213 | + {lineIdx === 0 ? ( | |
211 | 214 | <div class="flex items-center gap-2 leading-6"> |
212 | 215 | <a |
213 | 216 | href={`/${repo.owner}/${repo.name}/commit/${block.sha}`} |
214 | 217 | class="text-blue-400 hover:text-blue-300 hover:underline font-mono" |
215 | 218 | title={block.sha} |
216 | 219 | > |
217 | - {block.sha.substring(0, 8)} | |
220 | + {block.sha.substring(0, 7)} | |
218 | 221 | </a> |
219 | - <span class="truncate max-w-[100px] text-muted-foreground" title={block.author}> | |
222 | + <span class="truncate max-w-[80px] text-muted-foreground" title={block.author}> | |
220 | 223 | {block.author} |
221 | 224 | </span> |
222 | - <span class="text-muted-foreground shrink-0"> | |
225 | + <span class="text-muted-foreground shrink-0 text-[10px]"> | |
223 | 226 | {timeAgo(block.date)} |
224 | 227 | </span> |
228 | + {/* Hover Popup */} | |
229 | + <div class="hidden group-hover/line:block absolute left-0 top-full z-50 mt-1 w-80 rounded-lg border border-border bg-card shadow-xl p-3"> | |
230 | + <div class="flex items-start gap-3"> | |
231 | + <div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary text-xs font-bold flex-shrink-0"> | |
232 | + {block.author[0]?.toUpperCase() || "?"} | |
233 | + </div> | |
234 | + <div class="min-w-0"> | |
235 | + <p class="text-sm font-semibold text-foreground truncate">{block.author}</p> | |
236 | + <p class="text-xs text-muted-foreground">{formatDate(block.date)}</p> | |
237 | + </div> | |
238 | + </div> | |
239 | + {block.message && ( | |
240 | + <p class="mt-2 text-xs text-foreground font-mono bg-muted/50 rounded p-2 line-clamp-2"> | |
241 | + {block.message} | |
242 | + </p> | |
243 | + )} | |
244 | + <div class="mt-2 flex items-center gap-2 text-[10px] text-muted-foreground"> | |
245 | + <a href={`/${repo.owner}/${repo.name}/commit/${block.sha}`} class="text-blue-400 hover:underline font-mono"> | |
246 | + {block.sha.substring(0, 7)} | |
247 | + </a> | |
248 | + <span>·</span> | |
249 | + <span>{block.lines.length} line(s)</span> | |
250 | + </div> | |
251 | + </div> | |
225 | 252 | </div> |
253 | + ) : ( | |
254 | + <div class="leading-6 text-muted-foreground/30">·</div> | |
226 | 255 | )} |
227 | 256 | </td> |
228 | 257 | {/* Line number */} |
229 | - <td class="text-right px-3 py-0 text-xs text-muted-foreground select-none border-r border-gray-800 w-[1%]"> | |
258 | + <td class="text-right px-3 py-0 text-xs text-muted-foreground select-none border-r border-border w-[1%]"> | |
230 | 259 | <div class="leading-6"> |
231 | 260 | <a |
232 | 261 | id={`L${line.lineNum}`} |
233 | 262 | |