Skip to content
78 linesCodeBlameRaw
1import { 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";
10import { teamPath } from "../../../lib/teams";
11import { identity } from "../../../lib/services.server";
12import { getViewer, roleIn } from "../../../lib/session.server";
13
14export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `${loaderData?.team.name ?? params.team} · ${params.owner} · g1t` });
16}
17
18export async function loader({ params, context }: Route.LoaderArgs) {
19 const viewer = getViewer(context);
20 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
21 const found = await identity.getTeam(viewer, params.owner, params.team);
22 // A secret team someone is not in looks like no team at all.
23 if (!found.ok) throw data(null, { status: 404 });
24 return { team: found.value };
25}
26
27export default function TeamLayout({ loaderData }: Route.ComponentProps) {
28 const { team } = loaderData;
29 const base = teamPath(team.workspace, team.slug);
30 return (
31 <div>
32 <nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1 text-sm text-muted">
33 <Link to={`/${team.workspace}/-/teams`} className="hover:text-fg">
34 Teams
35 </Link>
36 {team.parent && (
37 <>
38 <ChevronRight size={13} className="text-faint" />
39 <Link to={teamPath(team.workspace, team.parent.slug)} className="hover:text-fg">
40 {team.parent.name}
41 </Link>
42 </>
43 )}
44 <ChevronRight size={13} className="text-faint" />
45 <span className="text-fg">{team.name}</span>
46 </nav>
47 <header className="mt-4 border-b border-line">
48 <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
49 <h1 className="text-2xl font-semibold tracking-tight">{team.name}</h1>
50 <TeamBadges team={team} />
51 </div>
52 {team.description && <p className="mt-1.5 text-sm text-muted">{team.description}</p>}
53 <div className="mt-3 max-w-xs">
54 <CopyLine text={teamHandle(team)} />
55 </div>
56 <nav aria-label="Team" className="mt-5 flex gap-1 overflow-x-auto">
57 <TabLink to={base} end icon={<Users size={15} />} count={team.members_count}>
58 Members
59 </TabLink>
60 <TabLink to={`${base}/teams`} icon={<Network size={15} />} count={team.child_teams_count}>
61 Child teams
62 </TabLink>
63 <TabLink to={`${base}/repositories`} icon={<Box size={15} />} count={team.repos_count}>
64 Repositories
65 </TabLink>
66 {team.can_manage && (
67 <TabLink to={`${base}/settings`} icon={<Settings size={15} />}>
68 Settings
69 </TabLink>
70 )}
71 </nav>
72 </header>
73 <div className="pt-6">
74 <Outlet context={{ team } satisfies TeamContext} />
75 </div>
76 </div>
77 );
78}