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