Skip to content
479 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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1import { CircleAlert, Lock, Ticket } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { Form, Link, data, redirect } from "react-router";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look3
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas4import type { InvitePreview, User } from "@g1t/contracts";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5
6import type { Route } from "./+types/invite";
7import { page } from "../lib/meta";
8import { Mark } from "../components/logo";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas9import { ContinueWithGithub, OrDivider } from "../components/github";
10import { Honeypot } from "../components/honeypot";
Merge branch 'worktree-agent-a985c01f43538af01'11import { Avatar, ButtonLink, ErrorText, Field, Input, SubmitButton } from "../components/ui";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12import { githubSignInEnabled } from "../lib/github.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look13import { identity } from "../lib/services.server";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas14import { cleanCode, landingFor, looksAutomated, suggestUsername, welcomeCookie } from "../lib/invites";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15import { clientKey } from "../lib/registration.server";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas16import { assertSameOrigin, getViewer, requireUser, roleIn, startSession } from "../lib/session.server";
17import { rememberWorkspace } from "../lib/workspace-choice";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look18
19export function meta(args: Route.MetaArgs) {
20 return page(args, {
21 title: "You're invited · g1t",
22 description: "An invite to g1t, where people and agents ship software together.",
23 });
24}
25
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas26/**
27 * Where an invite is used, from first click to landing inside: who sent
28 * it and what it is for, then signing up on this page (or signing in, for
29 * an address that has an account), and the workspace or repository it
30 * gives. Signing in or up elsewhere (GitHub, /login) comes back here with
31 * `?accept=1`, which finishes the job.
32 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33export async function loader({ request, context, params }: Route.LoaderArgs) {
34 const code = cleanCode(params.code);
35 const viewer = getViewer(context);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas36 const accepting = new URL(request.url).searchParams.get("accept") === "1";
37 const checked = await identity.checkInvite(code, clientKey(request), { viewer, anyStatus: true });
38 const invite = checked.ok ? checked.value : null;
Shared invite links: one staff-made link for a group, up to 1000 new accounts (identity 0038)39 // A shared link for a group signs up on /register, which names the group.
40 if (invite?.sharedLabel) throw redirect(`/register?invite=${encodeURIComponent(code)}`);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas41 const lands = invite ? landingFor(invite) : null;
42
43 if (viewer && invite) {
44 // Used by this person already: just made the account with it (through
45 // GitHub), or an old link opened again.
46 if (invite.status === "redeemed" && invite.forViewer) throw landIn(request, invite, [], accepting);
47 // Back from signing in to accept an invite sent to their address.
48 if (accepting && invite.status === "pending" && invite.forViewer === true && lands && !alreadyIn(viewer, invite)) {
49 const accepted = await identity.acceptInvite(viewer, code);
50 if (accepted.ok) throw landIn(request, invite, [], true);
51 return { ...base(), acceptError: accepted.error.message };
52 }
53 }
54
55 function base() {
56 return {
57 code,
58 invite,
59 error: checked.ok ? null : checked.error.message,
60 viewer: viewer ? { username: viewer.username, avatar: viewer.avatar ?? null } : null,
61 alreadyIn: viewer && invite ? alreadyIn(viewer, invite) : false,
62 github: false,
63 suggestion: suggestUsername(invite?.address),
64 started: Date.now(),
65 acceptError: null as string | null,
66 };
67 }
68 const signingUp = !viewer && invite?.status === "pending" && invite.kind === "account" && !invite.hasAccount;
69 return { ...base(), github: signingUp ? await githubSignInEnabled() : false };
70}
71
72/** Whether the viewer is in what the invite gives already. */
73function alreadyIn(viewer: User, invite: InvitePreview): boolean {
74 if (invite.workspace) return roleIn(viewer, invite.workspace.slug) !== null;
75 return false;
76}
77
78/**
79 * The redirect into what the invite gave, with the session (when one was
80 * just made), the welcome for that first view, and the workspace made the
81 * one the sidebar is about.
82 */
83function landIn(request: Request, invite: InvitePreview, cookies: string[], welcome: boolean): Response {
84 const secure = new URL(request.url).protocol === "https:";
85 const target = landingFor(invite);
86 const headers = new Headers();
87 for (const cookie of cookies) headers.append("set-cookie", cookie);
88 if (target && welcome) headers.append("set-cookie", welcomeCookie(target, secure));
89 if (invite.workspace) headers.append("set-cookie", rememberWorkspace(invite.workspace.slug, secure));
90 return redirect(target ? `/${target}` : "/", { headers });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91}
92
93export async function action({ request, context, params }: Route.ActionArgs) {
94 assertSameOrigin(request);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas95 const code = cleanCode(params.code);
96 const form = await request.formData();
97 const client = clientKey(request);
98 const checked = await identity.checkInvite(code, client, { viewer: getViewer(context) });
99 if (!checked.ok) return data({ error: checked.error.message }, { status: 422 });
100 const invite = checked.value;
101
102 if (form.get("intent") === "register") {
103 if (looksAutomated(form)) return data({ error: "Something went wrong. Try again." }, { status: 422 });
104 // An invite for one address makes the account with that address,
105 // whatever the form sent.
106 const email = invite.address ?? String(form.get("email") ?? "");
107 const result = await identity.register(
108 String(form.get("username") ?? ""),
109 email,
110 String(form.get("password") ?? ""),
111 code,
112 client,
113 );
114 if (!result.ok) return data({ error: result.error.message }, { status: 422 });
115 throw landIn(request, invite, [startSession(result.value.sessionToken)], true);
116 }
117
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look118 const user = requireUser(context, request);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas119 const result = await identity.acceptInvite(user, code);
Merge branch 'worktree-agent-a985c01f43538af01'120 // Said with a 200, so the page loads again and shows the invite as it
121 // now stands (used up, revoked) beside the reason: after a 4xx answer
122 // React Router keeps the page's data as it was.
123 if (!result.ok) return { error: result.error.message };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas124 throw landIn(request, invite, [], true);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look125}
126
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas127/** Who sent it, and the workspace it joins, as faces. */
128function Faces({ invite }: { invite: InvitePreview }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 const from = invite.invitedBy;
130 return (
131 <div className="flex items-center gap-3">
132 {from ? (
133 <Avatar name={from.username} image={from.avatar} size={44} />
134 ) : (
135 <span className="inline-flex size-11 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent">
136 <Ticket size={20} />
137 </span>
138 )}
139 {invite.workspace && (
140 <>
141 <span className="text-faint">→</span>
142 <Avatar name={invite.workspace.slug} image={invite.workspace.avatar} size={44} square />
143 </>
144 )}
145 </div>
146 );
147}
148
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas149function senderName(invite: InvitePreview): string {
150 return invite.invitedBy ? (invite.invitedBy.name ?? invite.invitedBy.username) : "The g1t team";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look151}
152
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas153/** "Chase Pierce invited you to join Flagon, Inc. on g1t", with the place in bold. */
154function Headline({ invite }: { invite: InvitePreview }) {
155 const from = senderName(invite);
156 if (invite.workspace) {
157 return (
158 <>
159 {from} invited you to join <strong className="font-semibold text-fg">{invite.workspace.name}</strong> on g1t
160 </>
161 );
162 }
163 if (invite.repository) {
164 return (
165 <>
166 {from} invited you to collaborate on <strong className="font-mono font-semibold text-fg">{invite.repository.name}</strong>
167 </>
168 );
169 }
170 return <>{from} invited you to g1t</>;
171}
172
173function about(invite: InvitePreview, signedIn: boolean): string {
174 const signingUp = !signedIn && !invite.hasAccount && invite.kind === "account";
175 if (invite.workspace) {
176 return `g1t is where people and agents ship software together. ${
177 signingUp ? "Make your account below and you join" : "Accepting joins you to"
178 } ${invite.workspace.name} as a member.`;
179 }
180 if (invite.repository) {
181 return `g1t is where people and agents ship software together. ${
182 signingUp ? "Make your account below and you get" : "Accepting gives you"
183 } the ${invite.repository.role} role on ${invite.repository.name}.`;
184 }
185 return "g1t is where people and agents ship software together: plan in issues, assign work to agents like teammates, and land it through checks that hold. It is invite-only for now; this invite gets you in.";
186}
187
188/** What accepting is called on its button. */
189function joinLabel(invite: InvitePreview): string {
190 if (invite.workspace) return `Join ${invite.workspace.name}`;
191 if (invite.repository) return `Accept access to ${invite.repository.name}`;
192 return "Accept invite";
193}
194
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look195const LONG_DATE = new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
196
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas197type Loaded = Route.ComponentProps["loaderData"];
198
199/** Signing up, on this page: the address the invite was sent to, a username, a password. */
200function SignUp({ loaded, error }: { loaded: Loaded; error: string | null }) {
201 const invite = loaded.invite!;
202 const here = `/invite/${loaded.code}`;
203 const back = `${here}?accept=1`;
204 const github = `/auth/github?${new URLSearchParams({ invite: loaded.code, next: back })}`;
205 return (
206 <section aria-labelledby="sign-up" className="rounded-xl border border-line bg-surface/60 p-5 sm:p-6">
207 <h2 id="sign-up" className="text-base font-semibold">
208 Create your account
209 </h2>
210 <p className="mt-1 text-sm text-muted">
211 {invite.workspace
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)212 ? `You join ${invite.workspace.name} as soon as you confirm your email.`
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas213 : invite.repository
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)214 ? `You get ${invite.repository.name} as soon as you confirm your email.`
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas215 : "It takes a minute."}
216 </p>
217 {loaded.github && (
218 <div className="mt-5">
219 <ContinueWithGithub href={github} />
220 <OrDivider />
221 </div>
222 )}
223 <Form method="post" className={`relative space-y-4 ${loaded.github ? "" : "mt-5"}`}>
224 <input type="hidden" name="intent" value="register" />
225 <Honeypot started={loaded.started} />
226 {invite.address ? (
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)227 <Field label="Email" hint="Your invite was sent here. We email it a code to confirm it before you start.">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas228 <span className="relative block">
229 <Input name="email" type="email" value={invite.address} readOnly aria-readonly="true" autoComplete="email" />
230 <Lock size={14} aria-hidden="true" className="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-faint" />
231 </span>
232 </Field>
233 ) : (
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)234 <Field label="Email" hint="We email it a code to confirm it before you start.">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas235 <Input name="email" type="email" autoComplete="email" required maxLength={254} />
236 </Field>
237 )}
238 <Field label="Username" hint="Lowercase letters, digits and hyphens. It is how you sign in and how others see you.">
239 <Input
240 name="username"
241 autoComplete="username"
242 required
243 autoFocus
244 maxLength={39}
245 defaultValue={loaded.suggestion}
246 pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*"
247 />
248 </Field>
249 <Field label="Password" hint="At least 10 characters.">
250 <Input name="password" type="password" autoComplete="new-password" required minLength={10} />
251 </Field>
252 <ErrorText>{error}</ErrorText>
253 <div className="pt-1 *:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'254 <SubmitButton pending="Creating account…" match={{ intent: "register" }}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas255 {invite.workspace
256 ? `Create account and join ${invite.workspace.name}`
257 : invite.repository
258 ? "Create account and accept"
259 : "Create account"}
Merge branch 'worktree-agent-a985c01f43538af01'260 </SubmitButton>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas261 </div>
262 </Form>
263 <p className="mt-5 text-center text-sm text-muted">
264 Already on g1t?{" "}
265 <Link to={`/login?next=${encodeURIComponent(back)}`} className="text-fg underline underline-offset-4">
266 Sign in to accept
267 </Link>
268 </p>
269 </section>
270 );
271}
272
273/** What to do next: join, sign in, sign out, or sign up. */
274function Next({ loaded, error }: { loaded: Loaded; error: string | null }) {
275 const invite = loaded.invite!;
276 const viewer = loaded.viewer;
277 const here = `/invite/${loaded.code}`;
278 const back = `${here}?accept=1`;
279 const joinable = invite.workspace !== null || invite.repository !== null;
280
281 if (viewer) {
282 const signedInAs = (
283 <p className="flex items-center gap-2 text-sm text-muted">
284 <Avatar name={viewer.username} image={viewer.avatar} size={20} />
285 Signed in as <span className="font-mono text-fg">{viewer.username}</span>
286 </p>
287 );
Merge branch 'worktree-agent-a985c01f43538af01'288 // Its own intent, which /logout ignores, so only its button says it is working.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas289 const signOut = (label: string, variant: "primary" | "quiet") => (
290 <Form method="post" action={`/logout?next=${encodeURIComponent(here)}`}>
Merge branch 'worktree-agent-a985c01f43538af01'291 <input type="hidden" name="intent" value="sign-out" />
292 <SubmitButton variant={variant} pending="Signing out…" match={{ intent: "sign-out" }}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas293 {label}
Merge branch 'worktree-agent-a985c01f43538af01'294 </SubmitButton>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas295 </Form>
296 );
297 if (invite.forViewer === false) {
298 return (
299 <div className="space-y-4">
300 {signedInAs}
301 <div className="rounded-md border border-warn/40 bg-warn/5 p-4 text-sm" role="status">
302 <p className="font-medium text-fg">This invite is for {invite.address ?? invite.email}, not this account.</p>
303 <p className="mt-1 text-muted">
304 Only an account with that address confirmed can use it. Sign out, then sign in or sign up with it.
305 </p>
306 </div>
307 <ErrorText>{error}</ErrorText>
308 <div className="flex flex-wrap gap-3">{signOut("Sign out and continue", "primary")}</div>
309 </div>
310 );
311 }
312 if (loaded.alreadyIn && invite.workspace) {
313 return (
314 <div className="space-y-4">
315 {signedInAs}
316 <p className="text-sm text-muted">You are in {invite.workspace.name} already.</p>
317 <ButtonLink to={`/${invite.workspace.slug}`}>Go to {invite.workspace.name}</ButtonLink>
318 </div>
319 );
320 }
321 if (!joinable) {
322 return (
323 <div className="space-y-4">
324 {signedInAs}
325 <div className="rounded-md border border-line bg-surface p-4 text-sm">
326 <p>
327 You already have a g1t account, <span className="font-mono">{viewer.username}</span>, so this invite has
328 nothing more to give you.
329 </p>
330 <p className="mt-2 text-muted">Pass it on to whoever it was meant for, or keep it for someone else.</p>
331 </div>
332 <div className="flex flex-wrap gap-3">{signOut("Sign out to use it", "quiet")}</div>
333 </div>
334 );
335 }
336 return (
Merge branch 'worktree-agent-a985c01f43538af01'337 <>
338 {/* "Not you?" posts this form, so it does not carry the accept form's intent. */}
339 <Form id="invite-sign-out" method="post" action={`/logout?next=${encodeURIComponent(here)}`} hidden>
340 <input type="hidden" name="intent" value="sign-out" />
341 </Form>
342 <Form method="post" className="space-y-4">
343 <input type="hidden" name="intent" value="accept" />
344 {signedInAs}
345 {invite.forViewer === null && (
346 <p className="text-sm text-muted">This invite is for anyone with the link. Accepting uses it up.</p>
347 )}
348 <ErrorText>{error}</ErrorText>
349 <div className="flex flex-wrap items-center gap-3">
350 <SubmitButton pending={invite.workspace ? "Joining…" : "Accepting…"} match={{ intent: "accept" }}>
351 {joinLabel(invite)}
352 </SubmitButton>
353 <span className="text-sm text-muted">
354 Not you?{" "}
355 <SubmitButton
356 form="invite-sign-out"
357 pending="Signing out…"
358 match={{ intent: "sign-out" }}
359 className="inline-flex items-center gap-1 text-fg underline underline-offset-4 disabled:opacity-50"
360 >
361 Sign out
362 </SubmitButton>
363 </span>
364 </div>
365 </Form>
366 </>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas367 );
368 }
369
370 // Someone already on g1t: sign in, and the invite is accepted on return.
371 if (invite.hasAccount || invite.kind === "workspace") {
372 return (
373 <div className="space-y-4">
374 <div className="rounded-md border border-line bg-surface p-4 text-sm">
375 <p className="font-medium text-fg">{invite.address ?? invite.email ?? "This address"} has a g1t account.</p>
376 <p className="mt-1 text-muted">
377 Sign in to it and {joinable ? `you ${invite.workspace ? `join ${invite.workspace.name}` : `get access to ${invite.repository!.name}`} straight away` : "the invite is accepted"}.
378 </p>
379 </div>
380 <ErrorText>{error}</ErrorText>
381 <ButtonLink to={`/login?next=${encodeURIComponent(back)}`}>Sign in to accept</ButtonLink>
382 </div>
383 );
384 }
385 return <SignUp loaded={loaded} error={error} />;
386}
387
388/** A code that cannot be used: why, and whom to ask. */
389function Dead({ loaded }: { loaded: Loaded }) {
390 const invite = loaded.invite;
391 const title = !invite
392 ? "This invite link does not work"
393 : invite.status === "expired"
394 ? "This invite has expired"
395 : invite.status === "revoked"
396 ? "This invite was withdrawn"
397 : "This invite has been used";
398 const from = invite?.invitedBy;
399 return (
400 <>
401 <h1 className="mt-6 flex items-center gap-2 text-2xl font-semibold tracking-tight">
402 <CircleAlert size={22} className="shrink-0 text-warn" />
403 {title}
404 </h1>
405 {invite ? (
406 <p className="mt-3 text-sm leading-6 text-muted">
407 It was {invite.workspace ? `an invite to join ${invite.workspace.name}` : invite.repository ? `an invite to ${invite.repository.name}` : "an invite to g1t"}
408 {invite.status === "expired" && `, and stopped working on ${LONG_DATE.format(new Date(invite.expiresAt))}`}.{" "}
409 {from ? "Ask the person who sent it for a new one." : "Ask for access and we will send a new one."}
410 </p>
411 ) : (
412 <p className="mt-3 text-sm leading-6 text-muted">{loaded.error}</p>
413 )}
414 {from && (
415 <div className="mt-5 flex items-center gap-3 rounded-lg border border-line bg-surface p-3">
416 <Avatar name={from.username} image={from.avatar} size={36} />
417 <p className="min-w-0 text-sm">
418 Ask <span className="font-medium text-fg">{from.name ?? from.username}</span>{" "}
419 <Link to={`/u/${from.username}`} className="font-mono text-muted hover:text-fg">
420 @{from.username}
421 </Link>{" "}
422 <span className="text-muted">for a new invite.</span>
423 </p>
424 </div>
425 )}
426 <div className="mt-8 flex flex-wrap gap-3">
427 <ButtonLink to="/register#request">Request access</ButtonLink>
428 <ButtonLink to={loaded.viewer ? "/" : "/login"} variant="quiet">
429 {loaded.viewer ? "Go to g1t" : "Sign in"}
430 </ButtonLink>
431 </div>
432 </>
433 );
434}
435
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look436export default function Invite({ loaderData, actionData }: Route.ComponentProps) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas437 const { invite } = loaderData;
438 const error = (actionData && "error" in actionData ? actionData.error : null) ?? loaderData.acceptError;
439 const usable = invite?.status === "pending";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look440 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas441 <main className="mx-auto flex max-w-md flex-col px-4 pt-16 pb-12 sm:pt-20">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look442 <Mark className="size-9" />
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas443 {usable ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look444 <>
445 <div className="mt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas446 <Faces invite={invite} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look447 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas448 <h1 className="mt-6 text-2xl font-semibold tracking-tight text-balance text-fg-soft">
449 <Headline invite={invite} />
450 </h1>
451 <p className="mt-2 text-sm leading-6 text-muted">{about(invite, loaderData.viewer !== null)}</p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look452 <dl className="mt-5 space-y-1 text-sm">
453 {invite.invitedBy && (
454 <div className="flex gap-2">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas455 <dt className="w-24 shrink-0 text-faint">From</dt>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look456 <dd className="font-mono text-fg-soft">@{invite.invitedBy.username}</dd>
457 </div>
458 )}
459 {invite.email && (
460 <div className="flex gap-2">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas461 <dt className="w-24 shrink-0 text-faint">For</dt>
462 <dd className="font-mono break-all text-fg-soft">{invite.address ?? invite.email}</dd>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look463 </div>
464 )}
465 <div className="flex gap-2">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas466 <dt className="w-24 shrink-0 text-faint">Works until</dt>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look467 <dd className="text-fg-soft">{LONG_DATE.format(new Date(invite.expiresAt))}</dd>
468 </div>
469 </dl>
470 <div className="mt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas471 <Next loaded={loaderData} error={error} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look472 </div>
473 </>
474 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas475 <Dead loaded={loaderData} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look476 )}
477 </main>
478 );
479}

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