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 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}