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