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

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 <Form method="post" className="mt-8 space-y-4">
60 <Field
61 label="Name in URLs"
62 hint="Lowercase letters, digits and single hyphens. An owner can change it later; old addresses redirect for 90 days."
63 >
64 <div className="flex items-center gap-2 font-mono text-sm">
65 <span className="text-muted">g1t.sh/</span>
66 <Input
67 name="slug"
68 required
69 autoFocus
70 maxLength={39}
71 defaultValue={first ? user.username : ""}
72 pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*"
73 />
74 </div>
75 </Field>
76 <Field label="Display name (optional)">
77 <Input name="displayName" maxLength={80} />
78 </Field>
79 <p className="text-xs text-faint">
80 A new workspace is free. Each person can own one free workspace; more need the plan on the ones you have.
81 </p>
82 <ErrorText>{actionData?.error}</ErrorText>
83 <SubmitButton pending="Creating…">Create workspace</SubmitButton>
84 </Form>
85 </>
Agents as a team: lifecycle, merge queue, billing and a new shell86 )}
87 </main>
88 );
89}
Merge Stripe Tax, the card fee on card payments, and one free workspace per person90
91/** Why there is no form: the person owns a free workspace already. */
92function OneFreeWorkspace({ free }: { free: string[] }) {
93 const [first] = free;
94 return (
95 <section className="mt-8 rounded-xl border border-line bg-surface p-5">
96 <h2 className="font-medium">You already own a free workspace</h2>
97 <p className="mt-2 text-sm text-muted">
98 Each person can own one workspace that is not on the g1t plan.{" "}
99 {free.length === 1 ? (
100 <>
101 Yours is <span className="font-mono text-fg">{first}</span>.
102 </>
103 ) : (
104 <>
105 You own {free.length} from before (
106 {free.map((slug, i) => (
107 <span key={slug}>
108 {i > 0 && ", "}
109 <span className="font-mono text-fg">{slug}</span>
110 </span>
111 ))}
112 ), and keep them all.
113 </>
114 )}{" "}
115 A new workspace starts free, so to make one, start the plan on {free.length === 1 ? "it" : "each of them"}, or delete{" "}
116 {free.length === 1 ? "it" : "the ones"} you no longer use.
117 </p>
118 <div className="mt-4 flex flex-wrap items-center gap-3">
119 <ButtonLink to={planHref(first)}>Start the plan on {first}</ButtonLink>
120 <Link to={`/${first}/-/settings`} className="text-sm text-muted hover:text-fg">
121 Workspace settings
122 </Link>
123 </div>
124 <p className="mt-4 text-xs text-faint">
125 The plan is one price for everyone in the workspace, never per person.{" "}
126 <Link to="/pricing" className="underline underline-offset-2 hover:text-fg">
127 Pricing
128 </Link>
129 </p>
130 </section>
131 );
132}

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