Skip to content
258 linesCodeBlameRaw

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.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { Mail } from "lucide-react";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)2import { useState } from "react";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look3import { Form, Link } from "react-router";
4
5import type { Invite, InvitesOverview } from "@g1t/contracts";
6
Merge branch 'worktree-agent-a985c01f43538af01'7import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)8import { CheckboxOption } from "./ui/checkbox";
9import { Hint } from "./ui/hint";
10import { G1T_INVITES, inviteFor, inviteKind, inviteLink, invitesPage, inviteState, moreInvitesMailto, remainingLine } from "../lib/invites";
11import type { BringIntoChoices } from "../lib/invites.server";
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)12
13const SELECT =
14 "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none hover:border-line-strong focus:border-accent-dim sm:w-auto";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15
16const TONE: Record<"pending" | "done" | "dead", string> = {
17 pending: "border-accent/40 text-accent",
18 done: "border-merged/40 text-merged",
19 dead: "border-line text-faint",
20};
21
22function InviteRow({ invite, origin }: { invite: Invite; origin: string }) {
23 const state = inviteState(invite);
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)24 const kind = inviteKind(invite);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25 return (
26 <li className="space-y-2 px-4 py-3">
27 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
28 <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs ${TONE[state.tone]}`}>{state.label}</span>
29 <span className="min-w-0 truncate text-sm">{inviteFor(invite)}</span>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)30 <Hint
31 label={
32 kind.kind === "g1t"
33 ? "Lets them make an account. It adds them to no workspace."
34 : "An invitation to join this workspace, which they accept or decline. Without an account, it lets them make one first."
35 }
36 >
37 <span className="rounded-full border border-line px-2 py-0.5 text-xs text-muted">{kind.label}</span>
38 </Hint>
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)39 {(invite.status === "pending" || invite.status === "awaiting_confirmation" || invite.status === "awaiting_answer") && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look40 <Form method="post" className="ml-auto">
41 <input type="hidden" name="intent" value="revoke-invite" />
42 <input type="hidden" name="id" value={invite.id} />
Merge branch 'worktree-agent-a985c01f43538af01'43 <SubmitButton variant="quiet" match={{ intent: "revoke-invite", id: invite.id }} pending="Revoking…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44 Revoke
Merge branch 'worktree-agent-a985c01f43538af01'45 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46 </Form>
47 )}
48 </div>
49 <p className="text-xs text-faint">
50 <span className="font-mono">{invite.hint}…</span> · made <TimeAgo at={invite.createdAt} />
51 {invite.status === "pending" && (
52 <>
53 {" "}· works until {new Date(invite.expiresAt).toISOString().slice(0, 10)}
54 </>
55 )}
56 {invite.chargedTo === "workspace" && invite.workspace == null && " · from a workspace's invites"}
57 </p>
58 {invite.status === "pending" && invite.code && <CopyLine text={inviteLink(invite.code, origin)} />}
59 </li>
60 );
61}
62
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)63/** "To bring someone into a workspace, invite them from that workspace's People page", with those pages. */
64function Elsewhere({ people }: { people: BringIntoChoices["people"] }) {
65 return (
66 <p className="mt-4 text-sm text-muted">
67 {G1T_INVITES.elsewhere}
68 {people.length > 0 ? ": " : "."}
69 {people.map((workspace, index) => (
70 <span key={workspace.slug}>
71 {index > 0 && ", "}
72 <Link to={workspace.to} className="text-fg underline underline-offset-4 hover:text-accent">
73 {workspace.name}
74 </Link>
75 </span>
76 ))}
77 {people.length > 0 && "."}
78 </p>
79 );
80}
81
82/**
83 * Making an invite to g1t: an address (or anyone with the link), whose
84 * invites it uses, and, only when its box is ticked, a workspace to invite
85 * them to as well. Unticked, nothing about a workspace is sent.
86 */
87function InviteForm({ overview, bringInto }: { overview: InvitesOverview; bringInto: BringIntoChoices }) {
88 const [alsoJoin, setAlsoJoin] = useState(false);
89 const { workspaces } = overview;
90 return (
91 <Form method="post" className="mt-4 space-y-4">
92 <input type="hidden" name="intent" value="create-invite" />
93 <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
94 <div className="grow">
95 <Field label="Email (optional)">
96 <Input name="email" type="email" maxLength={254} placeholder="Anyone with the link" />
97 </Field>
98 </div>
99 {workspaces.length > 0 && (
100 <label className="block">
101 <span className="mb-1.5 block text-sm font-medium text-muted">Use</span>
102 <select name="charge" className={SELECT}>
103 <option value="mine">Your invites</option>
104 {workspaces
105 .filter((workspace) => workspace.allowance.remaining !== 0)
106 .map((workspace) => (
107 <option key={workspace.slug} value={workspace.slug}>
108 {workspace.slug}'s invites
109 </option>
110 ))}
111 </select>
112 </label>
113 )}
114 <SubmitButton match={{ intent: "create-invite" }} pending="Creating…">
115 Create invite
116 </SubmitButton>
117 </div>
118 <p className="text-xs text-faint">With an email, the invite is sent there and only that address can use it.</p>
119
120 {bringInto.options.length > 0 && (
121 <div className="space-y-3 rounded-md border border-line p-3">
122 <CheckboxOption
123 name="also_join"
124 checked={alsoJoin}
125 onCheckedChange={(checked) => setAlsoJoin(checked === true)}
126 label={G1T_INVITES.alsoJoin}
127 description={G1T_INVITES.alsoJoinHint}
128 />
129 {alsoJoin && (
130 <div className="flex flex-col gap-3 pl-6 sm:flex-row sm:items-end">
131 <label className="block">
132 <span className="mb-1.5 block text-sm font-medium text-muted">Workspace</span>
133 <select name="join" required defaultValue="" className={SELECT}>
134 <option value="" disabled>
135 Choose a workspace
136 </option>
137 {bringInto.options.map((workspace) => (
138 <option key={workspace.slug} value={workspace.slug}>
139 {workspace.name === workspace.slug ? workspace.slug : `${workspace.name} (${workspace.slug})`}
140 </option>
141 ))}
142 </select>
143 </label>
144 <label className="block">
145 <span className="mb-1.5 block text-sm font-medium text-muted">Role</span>
146 <select name="join_role" defaultValue="member" className={SELECT}>
147 <option value="member">Member</option>
148 <option value="owner">Owner</option>
149 </select>
150 </label>
151 </div>
152 )}
153 {alsoJoin && bringInto.note && <p className="pl-6 text-xs text-faint">{bringInto.note}</p>}
154 </div>
155 )}
156 </Form>
157 );
158}
159
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160/**
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)161 * Settings → Invites (`/settings/invites`): invites to g1t. Each lets one
162 * person make an account and adds them to no workspace, unless "Also
163 * invite them to a workspace" is ticked. They exist only while sign-up
164 * takes an invite; once anyone can sign up, the page keeps only the list
165 * of invites already made. An invitation to a workspace is made on its
166 * People page, which this page points to.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look167 */
168export function InvitesSection({
169 overview,
170 created,
171 error,
172 origin = "https://g1t.sh",
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)173 bringInto = { options: [], note: null, people: [] },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look174}: {
175 overview: InvitesOverview | null;
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)176 /** The workspaces an invite can also invite its person to, and the People pages (lib/invites.server.ts). */
177 bringInto?: BringIntoChoices;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look178 created?: Invite;
179 error?: string;
180 origin?: string;
181}) {
182 if (!overview) {
183 return (
184 <section id="invites" className="scroll-mt-20">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas185 <p className="text-sm text-muted">Confirm your email address to invite people to g1t.</p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look186 </section>
187 );
188 }
189 const { allowance, workspaces, invites } = overview;
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)190 const shows = invitesPage(overview.mode, invites.length);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look191 const out = allowance.remaining === 0 && workspaces.every((workspace) => workspace.allowance.remaining === 0);
192 return (
193 <section id="invites" className="scroll-mt-20">
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)194 {shows.form ? (
195 <>
196 <p className="text-sm text-muted">A revoked or expired invite that was never used comes back to you.</p>
197 <p className="mt-3 text-sm">
198 <span className="font-medium">{remainingLine(allowance)}</span>
199 {workspaces.map((workspace) => (
200 <span key={workspace.slug} className="text-muted">
201 {" "}· {workspace.slug} has {workspace.allowance.remaining ?? "unlimited"} to share
202 </span>
203 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 </p>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)205
206 {created?.code && (
207 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
208 <p className="text-sm">
209 {created.email ? `Invite sent to ${created.email}. ` : ""}Share this link; it works once
210 {created.email ? ", for that address only" : ""}.
211 </p>
212 <div className="mt-2">
213 <CopyLine text={inviteLink(created.code, origin)} />
214 </div>
215 </div>
216 )}
217
218 {out ? (
219 <div className="mt-4 flex items-start gap-3 rounded-md border border-line bg-surface p-4 text-sm">
220 <Mail size={16} className="mt-0.5 shrink-0 text-muted" />
221 <p>
222 Need more invites?{" "}
223 <a href={moreInvitesMailto()} className="text-accent underline underline-offset-4">
224 Contact us
225 </a>{" "}
226 and say who you would like to bring, or ask on the{" "}
227 <Link to="/support" className="underline underline-offset-4">
228 support page
229 </Link>
230 .
231 </p>
232 </div>
233 ) : (
234 // Empty again once the invite is made; kept as filled in when it failed.
235 <InviteForm key={created?.id ?? ""} overview={overview} bringInto={bringInto} />
236 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 <div className="mt-2">
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)238 <ErrorText>{error}</ErrorText>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look239 </div>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)240 <Elsewhere people={bringInto.people} />
241 </>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 ) : (
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)243 <p className="text-sm text-muted">{G1T_INVITES.open}</p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look244 )}
245
246 {invites.length > 0 && (
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)247 <>
248 {!shows.form && <h2 className="mt-6 text-sm font-medium">Invites you made</h2>}
249 <ul className="mt-4 divide-y divide-line rounded-md border border-line">
250 {invites.map((invite) => (
251 <InviteRow key={invite.id} invite={invite} origin={origin} />
252 ))}
253 </ul>
254 </>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look255 )}
256 </section>
257 );
258}

This file's history is long; its oldest lines are credited to the oldest commit read.