g1t/apps/web/app/routes/repo/layout.tsx

110 lines3,377 bytesCodeBlame
1import { env } from "cloudflare:workers";
2import { BookMarked, Code2, History, Lock, Target } from "lucide-react";
3import type { ReactNode } from "react";
4import { Link, NavLink, Outlet } from "react-router";
5
6import type { Route } from "./+types/layout";
7import { Pill } from "../../components/ui";
8import { repos } from "../../lib/services.server";
9import { getViewer, unwrap } from "../../lib/session.server";
10
11export function meta({ params }: Route.MetaArgs) {
12 return [{ title: `${params.owner}/${params.repo} · g1t` }];
13}
14
15export async function loader({ params, context }: Route.LoaderArgs) {
16 const viewer = getViewer(context);
17 const path = { namespace: params.owner, name: params.repo };
18 const [repo, intents] = await Promise.all([
19 repos.get(path, viewer),
20 env.WORK.listIntents(path, viewer, "open"),
21 ]);
22 return {
23 repo: unwrap(repo),
24 openIntents: intents.ok ? intents.value.length : 0,
25 };
26}
27
28function Tab({
29 to,
30 end,
31 icon,
32 count,
33 children,
34}: {
35 to: string;
36 end?: boolean;
37 icon: ReactNode;
38 count?: number;
39 children: ReactNode;
40}) {
41 return (
42 <NavLink
43 to={to}
44 end={end}
45 className={({ isActive }) =>
46 `-mb-px flex items-center gap-2 border-b-2 px-1 pb-3 text-sm transition-colors ${
47 isActive
48 ? "border-accent font-medium text-fg"
49 : "border-transparent text-muted hover:text-fg"
50 }`
51 }
52 >
53 {icon}
54 {children}
55 {count != null && count > 0 && (
56 <span className="rounded-full bg-raised px-1.5 py-px text-xs text-muted">
57 {count}
58 </span>
59 )}
60 </NavLink>
61 );
62}
63
64export default function RepoLayout({ loaderData }: Route.ComponentProps) {
65 const { repo, openIntents } = loaderData;
66 const base = `/${repo.namespace}/${repo.name}`;
67 return (
68 <>
69 {/* The repository's own header band, under the site header. */}
70 <div className="border-b border-line bg-surface/60">
71 <div className="mx-auto max-w-6xl px-4 pt-6">
72 <div className="flex flex-wrap items-center gap-2.5">
73 {repo.isPrivate ? (
74 <Lock size={17} className="text-faint" />
75 ) : (
76 <BookMarked size={17} className="text-faint" />
77 )}
78 <h1 className="font-mono text-lg">
79 <Link to={`/${repo.namespace}`} className="text-muted hover:text-fg">
80 {repo.namespace}
81 </Link>
82 <span className="mx-1 text-faint">/</span>
83 <Link to={base} className="font-semibold hover:underline">
84 {repo.name}
85 </Link>
86 </h1>
87 <Pill>{repo.isPrivate ? "private" : "public"}</Pill>
88 </div>
89 {repo.description && (
90 <p className="mt-2 max-w-2xl text-sm text-muted">{repo.description}</p>
91 )}
92 <nav className="mt-5 flex gap-6">
93 <Tab to={base} end icon={<Code2 size={15} />}>
94 Code
95 </Tab>
96 <Tab to={`${base}/intents`} icon={<Target size={15} />} count={openIntents}>
97 Intents
98 </Tab>
99 <Tab to={`${base}/commits`} icon={<History size={15} />}>
100 Commits
101 </Tab>
102 </nav>
103 </div>
104 </div>
105 <div className="mx-auto max-w-6xl px-4 py-6">
106 <Outlet />
107 </div>
108 </>
109 );
110}