Skip to content

g1t/apps/web/app/routes/workspace/team/layout.tsx

79 lines3,240 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { Box, ChevronRight, Network, Settings, Users } from "lucide-react";
2import { Link, Outlet, data } from "react-router";
3
4import { teamHandle } from "@g1t/contracts";
5
6import type { Route } from "./+types/layout";
7import { page } from "../../../lib/meta";
8import { TeamBadges, type TeamContext } from "../../../components/teams";
9import { CopyLine, TabLink } from "../../../components/ui";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit10import { TabStrip } from "../../../components/ui/tab-strip";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar11import { teamPath } from "../../../lib/teams";
12import { identity } from "../../../lib/services.server";
13import { getViewer, roleIn } from "../../../lib/session.server";
14
15export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `${loaderData?.team.name ?? params.team} · ${params.owner} · g1t` });
17}
18
19export async function loader({ params, context }: Route.LoaderArgs) {
20 const viewer = getViewer(context);
21 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
22 const found = await identity.getTeam(viewer, params.owner, params.team);
23 // A secret team someone is not in looks like no team at all.
24 if (!found.ok) throw data(null, { status: 404 });
25 return { team: found.value };
26}
27
28export default function TeamLayout({ loaderData }: Route.ComponentProps) {
29 const { team } = loaderData;
30 const base = teamPath(team.workspace, team.slug);
31 return (
32 <div>
33 <nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1 text-sm text-muted">
34 <Link to={`/${team.workspace}/-/teams`} className="hover:text-fg">
35 Teams
36 </Link>
37 {team.parent && (
38 <>
39 <ChevronRight size={13} className="text-faint" />
40 <Link to={teamPath(team.workspace, team.parent.slug)} className="hover:text-fg">
41 {team.parent.name}
42 </Link>
43 </>
44 )}
45 <ChevronRight size={13} className="text-faint" />
46 <span className="text-fg">{team.name}</span>
47 </nav>
48 <header className="mt-4 border-b border-line">
49 <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
50 <h1 className="text-2xl font-semibold tracking-tight">{team.name}</h1>
51 <TeamBadges team={team} />
52 </div>
53 {team.description && <p className="mt-1.5 text-sm text-muted">{team.description}</p>}
54 <div className="mt-3 max-w-xs">
55 <CopyLine text={teamHandle(team)} />
56 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit57 <TabStrip label="Team" className="mt-5 gap-1">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar58 <TabLink to={base} end icon={<Users size={15} />} count={team.members_count}>
59 Members
60 </TabLink>
61 <TabLink to={`${base}/teams`} icon={<Network size={15} />} count={team.child_teams_count}>
62 Child teams
63 </TabLink>
64 <TabLink to={`${base}/repositories`} icon={<Box size={15} />} count={team.repos_count}>
65 Repositories
66 </TabLink>
67 {team.can_manage && (
68 <TabLink to={`${base}/settings`} icon={<Settings size={15} />}>
69 Settings
70 </TabLink>
71 )}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit72 </TabStrip>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar73 </header>
74 <div className="pt-6">
75 <Outlet context={{ team } satisfies TeamContext} />
76 </div>
77 </div>
78 );
79}