_code.tsx

  1// Pathless layout for the code browser. Reads preloaded refs from the
  2// $repo context and renders the shared header (breadcrumb + ref selector
  3// + history toggle). Child routes (tree, blob, commits) render in Outlet.
  4
  5import { useReadQuery } from "@apollo/client/react";
  6import {
  7  createFileRoute,
  8  Outlet,
  9  useNavigate,
 10  useParams,
 11  useRouterState,
 12} from "@tanstack/react-router";
 13import { GitCommit } from "lucide-react";
 14
 15import type { GitRef } from "@/__generated__/graphql";
 16import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb";
 17import { RefSelector } from "@/components/code/RefSelector";
 18import { ButtonLink } from "@/components/ui/button-link";
 19import { Skeleton } from "@/components/ui/skeleton";
 20
 21export type CodeViewMode = "tree" | "blob" | "commits";
 22
 23export const Route = createFileRoute("/$repo/_code")({
 24  component: CodeLayout,
 25  pendingComponent: CodeLayoutSkeleton,
 26});
 27
 28function CodeLayout() {
 29  const { repo } = Route.useParams();
 30  const { ref: repoRef, refsRef } = Route.useRouteContext();
 31  const { data: refsData } = useReadQuery(refsRef);
 32  const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? [];
 33  const repoName = refsData?.repository?.name ?? repoRef ?? "default-repo";
 34
 35  // Read child route params (ref and splat path)
 36  const allParams = useParams({ strict: false }) as {
 37    ref?: string;
 38    _splat?: string;
 39  };
 40  const currentRef = allParams.ref ?? "";
 41  const currentPath = allParams._splat ?? "";
 42
 43  // Read viewMode from the deepest child route's context.
 44  // Each child (tree, blob, commits) sets viewMode in its beforeLoad.
 45  const viewMode: CodeViewMode = useRouterState({
 46    select: (s) => {
 47      const ctx = s.matches.at(-1)?.context;
 48      if (ctx && typeof ctx === "object" && "viewMode" in ctx) {
 49        return ctx.viewMode as CodeViewMode;
 50      }
 51      return "tree";
 52    },
 53  });
 54
 55  const navigate = useNavigate();
 56
 57  function handleRefSelect(newRef: GitRef) {
 58    const refName = newRef.shortName;
 59    if (viewMode === "commits") {
 60      void navigate({ to: "/$repo/commits/$ref", params: { repo, ref: refName } });
 61    } else if (viewMode === "blob") {
 62      void navigate({
 63        to: "/$repo/blob/$ref/$",
 64        params: { repo, ref: refName, _splat: currentPath },
 65      });
 66    } else {
 67      void navigate({
 68        to: "/$repo/tree/$ref/$",
 69        params: { repo, ref: refName, _splat: currentPath },
 70      });
 71    }
 72  }
 73
 74  return (
 75    <div className="space-y-4">
 76      <div className="flex flex-wrap items-center justify-between gap-3">
 77        <CodeBreadcrumb
 78          repoName={repoName}
 79          currentRef={currentRef}
 80          path={currentPath}
 81          repo={repo}
 82        />
 83        <div className="flex items-center gap-2">
 84          {viewMode === "commits" ? (
 85            <ButtonLink
 86              to="/$repo/tree/$ref/$"
 87              params={{ repo, ref: currentRef, _splat: currentPath }}
 88              variant="secondary"
 89              size="sm"
 90            >
 91              <GitCommit className="size-3.5" />
 92              History
 93            </ButtonLink>
 94          ) : (
 95            <ButtonLink
 96              to="/$repo/commits/$ref"
 97              params={{ repo, ref: currentRef }}
 98              variant="outline"
 99              size="sm"
100            >
101              <GitCommit className="size-3.5" />
102              History
103            </ButtonLink>
104          )}
105          <RefSelector refs={refs} currentRef={currentRef} onSelect={handleRefSelect} />
106        </div>
107      </div>
108
109      <Outlet />
110    </div>
111  );
112}
113
114function CodeLayoutSkeleton() {
115  return (
116    <div className="space-y-4">
117      <div className="flex items-center justify-between">
118        <Skeleton className="h-5 w-48" />
119        <Skeleton className="h-8 w-28" />
120      </div>
121      <div className="divide-border border-border divide-y rounded-md border">
122        {Array.from({ length: 8 }).map((_, i) => (
123          <div key={i} className="flex items-center gap-3 px-4 py-2">
124            <Skeleton className="size-4 rounded-sm" />
125            <Skeleton className="h-4 w-32" />
126          </div>
127        ))}
128      </div>
129    </div>
130  );
131}