Skip to content
182 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";
2import { Form, Link } from "react-router";
3
4import type { Invite, InvitesOverview } from "@g1t/contracts";
5
Merge branch 'worktree-agent-a985c01f43538af01'6import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
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)7import { type BringInto, OWN_WORKSPACE, inviteFor, inviteLink, inviteState, moreInvitesMailto, remainingLine } from "../lib/invites";
8
9const SELECT =
10 "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 look11
12const TONE: Record<"pending" | "done" | "dead", string> = {
13 pending: "border-accent/40 text-accent",
14 done: "border-merged/40 text-merged",
15 dead: "border-line text-faint",
16};
17
18function InviteRow({ invite, origin }: { invite: Invite; origin: string }) {
19 const state = inviteState(invite);
20 return (
21 <li className="space-y-2 px-4 py-3">
22 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
23 <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs ${TONE[state.tone]}`}>{state.label}</span>
24 <span className="min-w-0 truncate text-sm">{inviteFor(invite)}</span>
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)25 {(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 look26 <Form method="post" className="ml-auto">
27 <input type="hidden" name="intent" value="revoke-invite" />
28 <input type="hidden" name="id" value={invite.id} />
Merge branch 'worktree-agent-a985c01f43538af01'29 <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 look30 Revoke
Merge branch 'worktree-agent-a985c01f43538af01'31 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32 </Form>
33 )}
34 </div>
35 <p className="text-xs text-faint">
36 <span className="font-mono">{invite.hint}…</span> · made <TimeAgo at={invite.createdAt} />
37 {invite.status === "pending" && (
38 <>
39 {" "}· works until {new Date(invite.expiresAt).toISOString().slice(0, 10)}
40 </>
41 )}
42 {invite.chargedTo === "workspace" && invite.workspace == null && " · from a workspace's invites"}
43 </p>
44 {invite.status === "pending" && invite.code && <CopyLine text={inviteLink(invite.code, origin)} />}
45 </li>
46 );
47}
48
49/**
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas50 * Settings → Invites (`/settings/invites`): what is left, making one (for anyone with the
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51 * link, or for one address), copying links, and revoking.
52 */
53export function InvitesSection({
54 overview,
55 created,
56 error,
57 origin = "https://g1t.sh",
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)58 bringInto = { options: [], chosen: OWN_WORKSPACE, note: null },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59}: {
60 overview: InvitesOverview | null;
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)61 /** The workspaces an invite can bring its person into (lib/invites.ts, `bringIntoChoices`). */
62 bringInto?: { options: BringInto[]; chosen: string; note: string | null };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63 created?: Invite;
64 error?: string;
65 origin?: string;
66}) {
67 if (!overview) {
68 return (
69 <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 quotas70 <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 look71 </section>
72 );
73 }
74 const { allowance, workspaces, invites } = overview;
75 const out = allowance.remaining === 0 && workspaces.every((workspace) => workspace.allowance.remaining === 0);
76 return (
77 <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 quotas78 <p className="text-sm text-muted">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look79 {overview.mode === "invite"
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite80 ? "Each invite lets one person make an account. "
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81 : "Anyone can make an account, but an invite still says who sent it. "}
82 A revoked or expired invite that was never used comes back to you.
83 </p>
84 <p className="mt-3 text-sm">
85 <span className="font-medium">{remainingLine(allowance)}</span>
86 {workspaces.map((workspace) => (
87 <span key={workspace.slug} className="text-muted">
88 {" "}· {workspace.slug} has {workspace.allowance.remaining ?? "unlimited"} to share
89 </span>
90 ))}
91 </p>
92
93 {created?.code && (
94 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
95 <p className="text-sm">
96 {created.email ? `Invite sent to ${created.email}. ` : ""}Share this link; it works once
97 {created.email ? ", for that address only" : ""}.
98 </p>
99 <div className="mt-2">
100 <CopyLine text={inviteLink(created.code, origin)} />
101 </div>
102 </div>
103 )}
104
105 {out ? (
106 <div className="mt-4 flex items-start gap-3 rounded-md border border-line bg-surface p-4 text-sm">
107 <Mail size={16} className="mt-0.5 shrink-0 text-muted" />
108 <p>
109 Need more invites?{" "}
110 <a href={moreInvitesMailto()} className="text-accent underline underline-offset-4">
111 Contact us
112 </a>{" "}
113 and say who you would like to bring, or ask on the{" "}
114 <Link to="/support" className="underline underline-offset-4">
115 support page
116 </Link>
117 .
118 </p>
119 </div>
120 ) : (
Merge branch 'worktree-agent-a985c01f43538af01'121 // Empty again once the invite is made; kept as filled in when it failed.
122 <Form method="post" key={created?.id ?? ""} className="mt-4 space-y-3">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123 <input type="hidden" name="intent" value="create-invite" />
124 <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
125 <div className="grow">
126 <Field label="Email (optional)">
127 <Input name="email" type="email" maxLength={254} placeholder="Anyone with the link" />
128 </Field>
129 </div>
130 {workspaces.length > 0 && (
131 <label className="block">
132 <span className="mb-1.5 block text-sm font-medium text-muted">Use</span>
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)133 <select name="charge" className={SELECT}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look134 <option value="mine">Your invites</option>
135 {workspaces
136 .filter((workspace) => workspace.allowance.remaining !== 0)
137 .map((workspace) => (
138 <option key={workspace.slug} value={workspace.slug}>
139 {workspace.slug}'s invites
140 </option>
141 ))}
142 </select>
143 </label>
144 )}
Merge branch 'worktree-agent-a985c01f43538af01'145 <SubmitButton match={{ intent: "create-invite" }} pending="Creating…">
146 Create invite
147 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look148 </div>
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)149 <label className="block">
150 <span className="mb-1.5 block text-sm font-medium text-muted">Bring them into</span>
151 <select name="join" defaultValue={bringInto.chosen} className={SELECT} aria-describedby="bring-into-hint">
152 {bringInto.options.map((workspace) => (
153 <option key={workspace.slug} value={workspace.slug}>
154 {workspace.name === workspace.slug ? workspace.slug : `${workspace.name} (${workspace.slug})`}
155 </option>
156 ))}
157 <option value={OWN_WORKSPACE}>No workspace — they'll get their own</option>
158 </select>
159 </label>
160 <p id="bring-into-hint" className="text-xs text-faint">
161 With a workspace, they are invited to join it as a member once their account is made, and accept or decline it. Without one, their account gets a workspace of its own.
162 {bringInto.note && <> {bringInto.note}</>}
163 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look164 <p className="text-xs text-faint">
165 With an email, the invite is sent there and only that address can use it.
166 </p>
167 </Form>
168 )}
169 <div className="mt-2">
170 <ErrorText>{error}</ErrorText>
171 </div>
172
173 {invites.length > 0 && (
174 <ul className="mt-4 divide-y divide-line rounded-md border border-line">
175 {invites.map((invite) => (
176 <InviteRow key={invite.id} invite={invite} origin={origin} />
177 ))}
178 </ul>
179 )}
180 </section>
181 );
182}

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