4.1 KB140 lines
Blame
1import type { Metadata } from "next";
2import Link from "next/link";
3import { groveApiUrl, timeAgo, encodePath } from "@/lib/utils";
4
5interface Props {
6 params: Promise<{ owner: string; repo: string; path: string[] }>;
7}
8
9export async function generateMetadata({ params }: Props): Promise<Metadata> {
10 const { repo, path: pathParts } = await params;
11 const filePath = pathParts.slice(1).join("/");
12 const fileName = filePath.split("/").pop() ?? filePath;
13 return { title: `Blame · ${fileName} · ${repo}` };
14}
15
16async function getBlame(owner: string, repo: string, ref: string, path: string) {
17 const res = await fetch(
18 `${groveApiUrl}/api/repos/${owner}/${repo}/blame/${ref}/${path}`,
19 { cache: "no-store" }
20 );
21 if (!res.ok) return null;
22 return res.json();
23}
24
25export default async function BlamePage({ params }: Props) {
26 const { owner, repo, path: pathParts } = await params;
27 const ref = pathParts[0] ?? "main";
28 const path = pathParts.slice(1).join("/");
29
30 const data = await getBlame(owner, repo, ref, path);
31
32 if (!data) {
33 return (
34 <div className="max-w-3xl mx-auto px-4 py-16">
35 <h1 className="text-lg" style={{ color: "var(--text-secondary)" }}>
36 Could not load blame
37 </h1>
38 </div>
39 );
40 }
41
42 // Track commit hash changes for alternating bands
43 let currentHash = "";
44 let bandIndex = 0;
45
46 return (
47 <div className="max-w-3xl mx-auto px-4 py-6">
48 <div className="flex items-center gap-1 text-sm mb-4">
49 <Link
50 href={`/${owner}/${repo}/blob/${ref}/${encodePath(path)}`}
51 style={{ color: "var(--accent)" }}
52 className="hover:underline"
53 >
54 {path}
55 </Link>
56 <span
57 className="ml-2 text-xs px-1.5 py-0.5"
58 style={{
59 color: "var(--text-muted)",
60 backgroundColor: "var(--bg-card)",
61 border: "1px solid var(--border-subtle)",
62 }}
63 >
64 blame
65 </span>
66 </div>
67
68 <div className="text-xs mb-4">
69 <Link
70 href={`/${owner}/${repo}/blob/${ref}/${encodePath(path)}`}
71 style={{ color: "var(--accent)" }}
72 className="hover:underline"
73 >
74 View source
75 </Link>
76 </div>
77
78 <div
79 className="overflow-x-auto"
80 style={{ border: "1px solid var(--border-subtle)" }}
81 >
82 <table className="w-full text-sm font-mono">
83 <tbody>
84 {data.blame.map((line: any, i: number) => {
85 if (line.hash !== currentHash) {
86 currentHash = line.hash;
87 bandIndex++;
88 }
89 const isAlt = bandIndex % 2 === 0;
90
91 return (
92 <tr
93 key={i}
94 style={{
95 backgroundColor: isAlt ? "var(--bg-card)" : undefined,
96 }}
97 >
98 <td
99 className="text-xs pr-3 py-0 w-16 truncate pl-3"
100 style={{ color: "var(--text-faint)" }}
101 >
102 {line.hash.slice(0, 7)}
103 </td>
104 <td
105 className="text-xs px-2 py-0 w-24 truncate"
106 style={{ color: "var(--text-muted)" }}
107 >
108 {line.author}
109 </td>
110 <td
111 className="text-xs px-2 py-0 w-16"
112 style={{ color: "var(--text-faint)" }}
113 >
114 {timeAgo(line.timestamp)}
115 </td>
116 <td
117 className="text-right select-none px-2 py-0 w-8"
118 style={{
119 color: "var(--text-faint)",
120 borderRight: "1px solid var(--divide)",
121 }}
122 >
123 {i + 1}
124 </td>
125 <td
126 className="pl-4 py-0 whitespace-pre"
127 style={{ color: "var(--text-secondary)" }}
128 >
129 {line.content}
130 </td>
131 </tr>
132 );
133 })}
134 </tbody>
135 </table>
136 </div>
137 </div>
138 );
139}
140