Skip to content

g1t/apps/web/app/routes/workspace/new.tsx

137 lines5,642 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.

Merge Stripe Tax, the card fee on card payments, and one free workspace per person1import { Form, Link, redirect } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell2
3import type { Route } from "./+types/new";
Social cards for every page: og.g1t.sh4import { page } from "../../lib/meta";
Merge Stripe Tax, the card fee on card payments, and one free workspace per person5import { planHref } from "../../components/start-plan";
6import { ButtonLink, ErrorText, Field, Input, SubmitButton } from "../../components/ui";
7import { billing, identity } from "../../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell8import { assertSameOrigin, nextPath, requireUser } from "../../lib/session.server";
9
Social cards for every page: og.g1t.sh10export function meta(args: Route.MetaArgs) {
11 return page(args, { title: "New workspace · g1t" });
Agents as a team: lifecycle, merge queue, billing and a new shell12}
13
Merge Stripe Tax, the card fee on card payments, and one free workspace per person14export async function loader({ request, context }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell15 const user = requireUser(context, request);
Merge Stripe Tax, the card fee on card payments, and one free workspace per person16 const owned = (user.workspaces ?? []).filter((membership) => membership.role === "owner").map((membership) => membership.slug);
17 // A person owns at most one free workspace; identity refuses a second
18 // either way, so a failure here only leaves the form up.
19 const free = owned.length > 0 ? await billing.freeWorkspaces(owned).catch(() => [] as string[]) : [];
20 return { user, first: (user.workspaces ?? []).length === 0, free };
Agents as a team: lifecycle, merge queue, billing and a new shell21}
22
23export async function action({ request, context }: Route.ActionArgs) {
24 assertSameOrigin(request);
25 const user = requireUser(context, request);
26 const form = await request.formData();
27 const result = await identity.createWorkspace(
28 user,
29 String(form.get("slug") ?? ""),
30 String(form.get("displayName") ?? ""),
31 );
32 if (!result.ok) return { error: result.error.message };
33 // Someone sent here on their way elsewhere carries on to it.
34 const next = nextPath(request);
35 throw redirect(next === "/" ? `/${result.value.slug}` : next);
36}
37
38export default function NewWorkspace({
39 loaderData,
40 actionData,
41}: Route.ComponentProps) {
Merge Stripe Tax, the card fee on card payments, and one free workspace per person42 const { user, first, free } = loaderData;
Agents as a team: lifecycle, merge queue, billing and a new shell43 return (
44 <main className="mx-auto max-w-lg px-4 py-12">
45 <h1 className="text-xl font-semibold">
46 {first ? "Create your workspace" : "New workspace"}
47 </h1>
48 <p className="mt-2 text-sm text-muted">
49 {first && "Everything on g1t lives in a workspace, so this comes first. "}
50 A workspace holds repositories, the people who work on them and the
51 access tokens that automate them, and is the first part of every
52 address: <span className="font-mono text-fg">g1t.sh/workspace/repo</span>.
53 Use one for yourself, and one for each team or company you work with.
54 </p>
Merge Stripe Tax, the card fee on card payments, and one free workspace per person55 {free.length > 0 ? (
56 <OneFreeWorkspace free={free} />
57 ) : (
58 <>
59 {!user.verified && (
60 <p className="mt-4 rounded-md border border-warn/30 bg-warn/10 px-3 py-2 text-sm">
61 Confirm your email address first. We sent you a link.
62 </p>
63 )}
64 <Form method="post" className="mt-8 space-y-4">
65 <Field
66 label="Name in URLs"
67 hint="Lowercase letters, digits and single hyphens. An owner can change it later; old addresses redirect for 90 days."
68 >
69 <div className="flex items-center gap-2 font-mono text-sm">
70 <span className="text-muted">g1t.sh/</span>
71 <Input
72 name="slug"
73 required
74 autoFocus
75 maxLength={39}
76 defaultValue={first ? user.username : ""}
77 pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*"
78 />
79 </div>
80 </Field>
81 <Field label="Display name (optional)">
82 <Input name="displayName" maxLength={80} />
83 </Field>
84 <p className="text-xs text-faint">
85 A new workspace is free. Each person can own one free workspace; more need the plan on the ones you have.
86 </p>
87 <ErrorText>{actionData?.error}</ErrorText>
88 <SubmitButton pending="Creating…">Create workspace</SubmitButton>
89 </Form>
90 </>
Agents as a team: lifecycle, merge queue, billing and a new shell91 )}
92 </main>
93 );
94}
Merge Stripe Tax, the card fee on card payments, and one free workspace per person95
96/** Why there is no form: the person owns a free workspace already. */
97function OneFreeWorkspace({ free }: { free: string[] }) {
98 const [first] = free;
99 return (
100 <section className="mt-8 rounded-xl border border-line bg-surface p-5">
101 <h2 className="font-medium">You already own a free workspace</h2>
102 <p className="mt-2 text-sm text-muted">
103 Each person can own one workspace that is not on the g1t plan.{" "}
104 {free.length === 1 ? (
105 <>
106 Yours is <span className="font-mono text-fg">{first}</span>.
107 </>
108 ) : (
109 <>
110 You own {free.length} from before (
111 {free.map((slug, i) => (
112 <span key={slug}>
113 {i > 0 && ", "}
114 <span className="font-mono text-fg">{slug}</span>
115 </span>
116 ))}
117 ), and keep them all.
118 </>
119 )}{" "}
120 A new workspace starts free, so to make one, start the plan on {free.length === 1 ? "it" : "each of them"}, or delete{" "}
121 {free.length === 1 ? "it" : "the ones"} you no longer use.
122 </p>
123 <div className="mt-4 flex flex-wrap items-center gap-3">
124 <ButtonLink to={planHref(first)}>Start the plan on {first}</ButtonLink>
125 <Link to={`/${first}/-/settings`} className="text-sm text-muted hover:text-fg">
126 Workspace settings
127 </Link>
128 </div>
129 <p className="mt-4 text-xs text-faint">
130 The plan is one price for everyone in the workspace, never per person.{" "}
131 <Link to="/pricing" className="underline underline-offset-2 hover:text-fg">
132 Pricing
133 </Link>
134 </p>
135 </section>
136 );
137}

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