FileTree.tsx

  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}