FileTree.tsx

  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}