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