flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

80 lines3,013 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.

Agents as a team: lifecycle, merge queue, billing and a new shell1import { Form, redirect } from "react-router";
2
3import type { Route } from "./+types/new";
Social cards for every page: og.g1t.sh4import { page } from "../../lib/meta";
Agents as a team: lifecycle, merge queue, billing and a new shell5import { Button, ErrorText, Field, Input } from "../../components/ui";
6import { identity } from "../../lib/services.server";
7import { assertSameOrigin, nextPath, requireUser } from "../../lib/session.server";
8
Social cards for every page: og.g1t.sh9export function meta(args: Route.MetaArgs) {
10 return page(args, { title: "New workspace · g1t" });
Agents as a team: lifecycle, merge queue, billing and a new shell11}
12
13export function loader({ request, context }: Route.LoaderArgs) {
14 const user = requireUser(context, request);
15 return { user, first: (user.workspaces ?? []).length === 0 };
16}
17
18export async function action({ request, context }: Route.ActionArgs) {
19 assertSameOrigin(request);
20 const user = requireUser(context, request);
21 const form = await request.formData();
22 const result = await identity.createWorkspace(
23 user,
24 String(form.get("slug") ?? ""),
25 String(form.get("displayName") ?? ""),
26 );
27 if (!result.ok) return { error: result.error.message };
28 // Someone sent here on their way elsewhere carries on to it.
29 const next = nextPath(request);
30 throw redirect(next === "/" ? `/${result.value.slug}` : next);
31}
32
33export default function NewWorkspace({
34 loaderData,
35 actionData,
36}: Route.ComponentProps) {
37 const { user, first } = loaderData;
38 return (
39 <main className="mx-auto max-w-lg px-4 py-12">
40 <h1 className="text-xl font-semibold">
41 {first ? "Create your workspace" : "New workspace"}
42 </h1>
43 <p className="mt-2 text-sm text-muted">
44 {first && "Everything on g1t lives in a workspace, so this comes first. "}
45 A workspace holds repositories, the people who work on them and the
46 access tokens that automate them, and is the first part of every
47 address: <span className="font-mono text-fg">g1t.sh/workspace/repo</span>.
48 Use one for yourself, and one for each team or company you work with.
49 </p>
50 {!user.verified && (
51 <p className="mt-4 rounded-md border border-warn/30 bg-warn/10 px-3 py-2 text-sm">
52 Confirm your email address first. We sent you a link.
53 </p>
54 )}
55 <Form method="post" className="mt-8 space-y-4">
56 <Field
57 label="Name in URLs"
58 hint="Lowercase letters, digits and single hyphens. It cannot be changed later."
59 >
60 <div className="flex items-center gap-2 font-mono text-sm">
61 <span className="text-muted">g1t.sh/</span>
62 <Input
63 name="slug"
64 required
65 autoFocus
66 maxLength={39}
67 defaultValue={first ? user.username : ""}
68 pattern="[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9]))*"
69 />
70 </div>
71 </Field>
72 <Field label="Display name (optional)">
73 <Input name="displayName" maxLength={80} />
74 </Field>
75 <ErrorText>{actionData?.error}</ErrorText>
76 <Button type="submit">Create workspace</Button>
77 </Form>
78 </main>
79 );
80}