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/new.tsx

93 lines3,576 bytesCodeBlame
1import { Form, redirect } from "react-router";
2
3import type { Route } from "./+types/new";
4import { Button, ErrorText, Field, Input } from "../components/ui";
5import { repos } from "../lib/services.server";
6import { assertSameOrigin, requireUser } from "../lib/session.server";
7
8export function meta({}: Route.MetaArgs) {
9 return [{ title: "New repository · g1t" }];
10}
11
12export function loader({ request, context }: Route.LoaderArgs) {
13 const user = requireUser(context, request);
14 const workspaces = (user.workspaces ?? []).map((membership) => membership.slug);
15 // Repositories live in a workspace, so there has to be one first.
16 if (workspaces.length === 0) throw redirect("/workspaces/new");
17 const asked = new URL(request.url).searchParams.get("workspace");
18 return {
19 workspaces,
20 selected: asked && workspaces.includes(asked) ? asked : workspaces[0],
21 };
22}
23
24export async function action({ request, context }: Route.ActionArgs) {
25 assertSameOrigin(request);
26 const user = requireUser(context, request);
27 const form = await request.formData();
28 const result = await repos.create(user, {
29 namespace: String(form.get("workspace") ?? ""),
30 name: String(form.get("name") ?? ""),
31 description: String(form.get("description") ?? ""),
32 isPrivate: form.get("visibility") === "private",
33 importUrl: String(form.get("importUrl") ?? "").trim() || undefined,
34 });
35 if (!result.ok) return { error: result.error.message };
36 throw redirect(`/${result.value.namespace}/${result.value.name}`);
37}
38
39export default function NewRepo({
40 loaderData,
41 actionData,
42}: Route.ComponentProps) {
43 return (
44 <main className="mx-auto max-w-lg px-4 py-12">
45 <h1 className="text-xl font-semibold">New repository</h1>
46 <Form method="post" className="mt-8 space-y-4">
47 <Field label="Name">
48 <div className="flex items-center gap-2 font-mono text-sm">
49 <select
50 name="workspace"
51 defaultValue={loaderData.selected}
52 aria-label="Workspace"
53 className="rounded-md border border-line bg-bg px-2 py-2 text-sm"
54 >
55 {loaderData.workspaces.map((slug) => (
56 <option key={slug} value={slug}>
57 {slug}
58 </option>
59 ))}
60 </select>
61 <span className="text-muted">/</span>
62 <Input name="name" required autoFocus maxLength={100} />
63 </div>
64 </Field>
65 <Field label="Description (optional)">
66 <Input name="description" maxLength={200} />
67 </Field>
68 <Field
69 label="Import from (optional)"
70 hint="The address of a public repository on GitHub or any git host. Its default branch is copied, up to 40 MB. Leave empty to start with nothing."
71 >
72 <Input
73 name="importUrl"
74 type="url"
75 placeholder="https://github.com/owner/repo"
76 />
77 </Field>
78 <fieldset className="space-y-2 text-sm">
79 <label className="flex items-center gap-2">
80 <input type="radio" name="visibility" value="public" defaultChecked />
81 Public — anyone can see and clone it
82 </label>
83 <label className="flex items-center gap-2">
84 <input type="radio" name="visibility" value="private" />
85 Private — only members of the workspace
86 </label>
87 </fieldset>
88 <ErrorText>{actionData?.error}</ErrorText>
89 <Button type="submit">Create repository</Button>
90 </Form>
91 </main>
92 );
93}