g1t/apps/web/app/components/invites-section.tsx

164 lines6,604 bytesCodeBlame

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";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look7import { inviteFor, inviteLink, inviteState, moreInvitesMailto, remainingLine } from "../lib/invites";
8
9const TONE: Record<"pending" | "done" | "dead", string> = {
10 pending: "border-accent/40 text-accent",
11 done: "border-merged/40 text-merged",
12 dead: "border-line text-faint",
13};
14
15function InviteRow({ invite, origin }: { invite: Invite; origin: string }) {
16 const state = inviteState(invite);
17 return (
18 <li className="space-y-2 px-4 py-3">
19 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
20 <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs ${TONE[state.tone]}`}>{state.label}</span>
21 <span className="min-w-0 truncate text-sm">{inviteFor(invite)}</span>
22 {invite.status === "pending" && (
23 <Form method="post" className="ml-auto">
24 <input type="hidden" name="intent" value="revoke-invite" />
25 <input type="hidden" name="id" value={invite.id} />
Merge branch 'worktree-agent-a985c01f43538af01'26 <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 look27 Revoke
Merge branch 'worktree-agent-a985c01f43538af01'28 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29 </Form>
30 )}
31 </div>
32 <p className="text-xs text-faint">
33 <span className="font-mono">{invite.hint}…</span> · made <TimeAgo at={invite.createdAt} />
34 {invite.status === "pending" && (
35 <>
36 {" "}· works until {new Date(invite.expiresAt).toISOString().slice(0, 10)}
37 </>
38 )}
39 {invite.chargedTo === "workspace" && invite.workspace == null && " · from a workspace's invites"}
40 </p>
41 {invite.status === "pending" && invite.code && <CopyLine text={inviteLink(invite.code, origin)} />}
42 </li>
43 );
44}
45
46/**
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas47 * 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 look48 * link, or for one address), copying links, and revoking.
49 */
50export function InvitesSection({
51 overview,
52 created,
53 error,
54 origin = "https://g1t.sh",
55}: {
56 overview: InvitesOverview | null;
57 created?: Invite;
58 error?: string;
59 origin?: string;
60}) {
61 if (!overview) {
62 return (
63 <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 quotas64 <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 look65 </section>
66 );
67 }
68 const { allowance, workspaces, invites } = overview;
69 const out = allowance.remaining === 0 && workspaces.every((workspace) => workspace.allowance.remaining === 0);
70 return (
71 <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 quotas72 <p className="text-sm text-muted">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73 {overview.mode === "invite"
74 ? "g1t is invite-only for now. Each invite lets one person make an account. "
75 : "Anyone can make an account, but an invite still says who sent it. "}
76 A revoked or expired invite that was never used comes back to you.
77 </p>
78 <p className="mt-3 text-sm">
79 <span className="font-medium">{remainingLine(allowance)}</span>
80 {workspaces.map((workspace) => (
81 <span key={workspace.slug} className="text-muted">
82 {" "}· {workspace.slug} has {workspace.allowance.remaining ?? "unlimited"} to share
83 </span>
84 ))}
85 </p>
86
87 {created?.code && (
88 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
89 <p className="text-sm">
90 {created.email ? `Invite sent to ${created.email}. ` : ""}Share this link; it works once
91 {created.email ? ", for that address only" : ""}.
92 </p>
93 <div className="mt-2">
94 <CopyLine text={inviteLink(created.code, origin)} />
95 </div>
96 </div>
97 )}
98
99 {out ? (
100 <div className="mt-4 flex items-start gap-3 rounded-md border border-line bg-surface p-4 text-sm">
101 <Mail size={16} className="mt-0.5 shrink-0 text-muted" />
102 <p>
103 Need more invites?{" "}
104 <a href={moreInvitesMailto()} className="text-accent underline underline-offset-4">
105 Contact us
106 </a>{" "}
107 and say who you would like to bring, or ask on the{" "}
108 <Link to="/support" className="underline underline-offset-4">
109 support page
110 </Link>
111 .
112 </p>
113 </div>
114 ) : (
Merge branch 'worktree-agent-a985c01f43538af01'115 // Empty again once the invite is made; kept as filled in when it failed.
116 <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 look117 <input type="hidden" name="intent" value="create-invite" />
118 <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
119 <div className="grow">
120 <Field label="Email (optional)">
121 <Input name="email" type="email" maxLength={254} placeholder="Anyone with the link" />
122 </Field>
123 </div>
124 {workspaces.length > 0 && (
125 <label className="block">
126 <span className="mb-1.5 block text-sm font-medium text-muted">Use</span>
127 <select
128 name="charge"
129 className="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"
130 >
131 <option value="mine">Your invites</option>
132 {workspaces
133 .filter((workspace) => workspace.allowance.remaining !== 0)
134 .map((workspace) => (
135 <option key={workspace.slug} value={workspace.slug}>
136 {workspace.slug}'s invites
137 </option>
138 ))}
139 </select>
140 </label>
141 )}
Merge branch 'worktree-agent-a985c01f43538af01'142 <SubmitButton match={{ intent: "create-invite" }} pending="Creating…">
143 Create invite
144 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look145 </div>
146 <p className="text-xs text-faint">
147 With an email, the invite is sent there and only that address can use it.
148 </p>
149 </Form>
150 )}
151 <div className="mt-2">
152 <ErrorText>{error}</ErrorText>
153 </div>
154
155 {invites.length > 0 && (
156 <ul className="mt-4 divide-y divide-line rounded-md border border-line">
157 {invites.map((invite) => (
158 <InviteRow key={invite.id} invite={invite} origin={origin} />
159 ))}
160 </ul>
161 )}
162 </section>
163 );
164}

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