Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
130 linesCodeBlameRaw
1import { Box, ChevronRight, Code2, Crown, Database, Hash, Network, Settings, Users } from "lucide-react";
2import type { ReactNode } from "react";
3import { Link, Outlet, data } from "react-router";
4
5import { teamHandle } from "@g1t/contracts";
6
7import type { Route } from "./+types/layout";
8import { page } from "../../../lib/meta";
9import { TeamBadges, type TeamContext } from "../../../components/teams";
10import { Coming } from "../../../components/people";
11import { CopyLine, TabLink } from "../../../components/ui";
12import { Hint } from "../../../components/ui/hint";
13import { TabStrip } from "../../../components/ui/tab-strip";
14import { personPath } from "../../../lib/people";
15import { teamPath } from "../../../lib/teams";
16import { identity } from "../../../lib/services.server";
17import { getViewer, roleIn } from "../../../lib/session.server";
18
19export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
20 return page(args, { title: `${loaderData?.team.name ?? params.team} · ${params.owner} · g1t` });
21}
22
23export async function loader({ params, context }: Route.LoaderArgs) {
24 const viewer = getViewer(context);
25 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
26 const found = await identity.getTeam(viewer, params.owner, params.team);
27 // A secret team someone is not in looks like no team at all.
28 if (!found.ok) throw data(null, { status: 404 });
29 return { team: found.value };
30}
31
32export default function TeamLayout({ loaderData }: Route.ComponentProps) {
33 const { team } = loaderData;
34 const base = teamPath(team.workspace, team.slug);
35 // A team is led by a person; an old agent lead (agents no longer exist) is no lead.
36 const lead = team.lead?.kind === "user" ? { name: team.lead.name ?? team.lead.username, to: personPath(team.workspace, team.lead.username) } : null;
37 return (
38 <div>
39 <nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1 text-sm text-muted">
40 <Link to={`/${team.workspace}/-/teams`} className="hover:text-fg">
41 Teams
42 </Link>
43 {team.parent && (
44 <>
45 <ChevronRight size={13} className="text-faint" />
46 <Link to={teamPath(team.workspace, team.parent.slug)} className="hover:text-fg">
47 {team.parent.name}
48 </Link>
49 </>
50 )}
51 <ChevronRight size={13} className="text-faint" />
52 <span className="text-fg">{team.name}</span>
53 </nav>
54 <header className="mt-4 border-b border-line">
55 <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
56 <h1 className="text-2xl font-semibold tracking-tight">{team.name}</h1>
57 <TeamBadges team={team} />
58 </div>
59 {team.description && <p className="mt-1.5 text-sm text-muted">{team.description}</p>}
60 <ul aria-label="What the team has" className="mt-3 flex flex-wrap gap-1.5">
61 <Fact icon={<Users size={12} />}>
62 {team.members_count ? `${team.members_count} ${team.members_count === 1 ? "person" : "people"}` : "No one yet"}
63 </Fact>
64 <Fact icon={<Crown size={12} />}>
65 {lead ? (
66 <>
67 Led by{" "}
68 <Link to={lead.to} className="font-medium text-fg hover:text-accent">
69 {lead.name}
70 </Link>
71 </>
72 ) : (
73 "No lead"
74 )}
75 </Fact>
76 <Fact icon={<Hash size={12} />}>
77 {team.channel ? (
78 <Link to={`/${team.workspace}/-/chat/${team.channel.name}`} className="font-medium text-fg hover:text-accent">
79 {team.channel.name}
80 </Link>
81 ) : (
82 "No channel"
83 )}
84 </Fact>
85 <Fact icon={<Code2 size={12} />}>
86 <Hint label="Its roles on repositories, given to everyone on it and its child teams.">
87 <Link to={`${base}/repositories`} className="hover:text-fg">
88 Code: {team.repos_count ? `${team.repos_count} ${team.repos_count === 1 ? "repository" : "repositories"}` : "no roles yet"}
89 </Link>
90 </Hint>
91 </Fact>
92 <Fact icon={<Database size={12} />}>
93 Storage <Coming />
94 </Fact>
95 </ul>
96 <div className="mt-3 max-w-xs">
97 <CopyLine text={teamHandle(team)} />
98 </div>
99 <TabStrip label="Team" className="mt-5 gap-1">
100 <TabLink to={base} end icon={<Users size={15} />} count={team.members_count}>
101 People
102 </TabLink>
103 <TabLink to={`${base}/teams`} icon={<Network size={15} />} count={team.child_teams_count}>
104 Child teams
105 </TabLink>
106 <TabLink to={`${base}/repositories`} icon={<Box size={15} />} count={team.repos_count}>
107 Repositories
108 </TabLink>
109 {team.can_manage && (
110 <TabLink to={`${base}/settings`} icon={<Settings size={15} />}>
111 Settings
112 </TabLink>
113 )}
114 </TabStrip>
115 </header>
116 <div className="pt-6">
117 <Outlet context={{ team } satisfies TeamContext} />
118 </div>
119 </div>
120 );
121}
122
123function Fact({ icon, children }: { icon?: ReactNode; children: ReactNode }) {
124 return (
125 <li className="inline-flex items-center gap-1.5 rounded-full border border-line bg-surface px-2.5 py-0.5 text-xs text-muted">
126 {icon && <span className="text-faint">{icon}</span>}
127 {children}
128 </li>
129 );
130}