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

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