1import { Link } from "@tanstack/react-router";
2import { formatDistanceToNow } from "date-fns";
3import { Folder, File } from "lucide-react";
4
5import { GitObjectType, type GitTreeEntry } from "@/__generated__/graphql";
6import { Skeleton } from "@/components/ui/skeleton";
7
8export interface TreeEntryWithCommit extends GitTreeEntry {
9 lastCommit?: {
10 hash: string;
11 shortHash: string;
12 message: string;
13 date: string;
14 };
15}
16
17interface FileTreeProps {
18 repo: string;
19 currentRef: string;
20 currentPath: string;
21 entries: TreeEntryWithCommit[];
22 loading?: boolean;
23}
24
25// Directory listing table for the code browser. Shows each entry's icon,
26// name, last-commit message (linked to commit detail), and relative date.
27export function FileTree({ repo, currentRef, currentPath, entries, loading }: FileTreeProps) {
28 // Directories first, then files — each group alphabetical
29 const sorted = entries.toSorted((a, b) => {
30 if (a.type !== b.type) return a.type === GitObjectType.Tree ? -1 : 1;
31 return a.name.localeCompare(b.name);
32 });
33
34 if (loading) return <FileTreeSkeleton />;
35
36 return (
37 <div className="border-border overflow-hidden rounded-md border">
38 <table className="w-full text-sm">
39 <tbody className="divide-border divide-y">
40 {currentPath && (
41 <tr className="hover:bg-muted/40">
42 <td colSpan={4}>
43 <Link
44 to="/$repo/tree/$ref/$"
45 params={{
46 repo,
47 ref: currentRef,
48 _splat: currentPath.split("/").slice(0, -1).join("/"),
49 }}
50 className="flex items-center gap-3 py-2 pl-4"
51 >
52 <Folder className="size-4 text-blue-500 dark:text-blue-400" />
53 <span className="text-muted-foreground font-mono">..</span>
54 </Link>
55 </td>
56 </tr>
57 )}
58 {sorted.map((entry) => (
59 <FileTreeRow
60 key={entry.name}
61 entry={entry}
62 repo={repo}
63 currentRef={currentRef}
64 currentPath={currentPath}
65 />
66 ))}
67 </tbody>
68 </table>
69 </div>
70 );
71}
72
73function FileTreeRow({
74 entry,
75 repo,
76 currentRef,
77 currentPath,
78}: {
79 entry: TreeEntryWithCommit;
80 repo: string;
81 currentRef: string;
82 currentPath: string;
83}) {
84 const isDir = entry.type === GitObjectType.Tree;
85 const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
86
87 const entryLink = isDir
88 ? { to: "/$repo/tree/$ref/$" as const, params: { repo, ref: currentRef, _splat: entryPath } }
89 : { to: "/$repo/blob/$ref/$" as const, params: { repo, ref: currentRef, _splat: entryPath } };
90
91 return (
92 <tr className="hover:bg-muted/40">
93 <td className="w-6 py-2 pl-4">
94 {isDir ? (
95 <Folder className="size-4 text-blue-500 dark:text-blue-400" />
96 ) : (
97 <File className="text-muted-foreground size-4" />
98 )}
99 </td>
100 <td className="px-3 py-2">
101 <Link {...entryLink} className={`font-mono ${isDir ? "font-medium" : ""} hover:underline`}>
102 {entry.name}
103 </Link>
104 </td>
105 <td className="text-muted-foreground hidden max-w-xs truncate px-3 py-2 md:table-cell">
106 {entry.lastCommit && (
107 <Link
108 to="/$repo/commit/$hash"
109 params={{ repo, hash: entry.lastCommit.hash }}
110 className="hover:text-foreground hover:underline"
111 >
112 {entry.lastCommit.message}
113 </Link>
114 )}
115 </td>
116 <td className="text-muted-foreground hidden px-4 py-2 text-right text-xs whitespace-nowrap md:table-cell">
117 {entry.lastCommit &&
118 formatDistanceToNow(new Date(entry.lastCommit.date), { addSuffix: true })}
119 </td>
120 </tr>
121 );
122}
123
124function FileTreeSkeleton() {
125 return (
126 <div className="border-border overflow-hidden rounded-md border">
127 <div className="divide-border divide-y">
128 {Array.from({ length: 8 }).map((_, i) => (
129 <div key={i} className="flex items-center gap-3 px-4 py-2">
130 <Skeleton className="size-4 rounded-sm" />
131 <Skeleton className="h-4 w-32" />
132 <Skeleton className="ml-6 hidden h-4 w-64 md:block" />
133 <Skeleton className="ml-auto hidden h-4 w-20 md:block" />
134 </div>
135 ))}
136 </div>
137 </div>
138 );
139}