pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/routes/new.tsx

82 lines3,105 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 });
34 if (!result.ok) return { error: result.error.message };
35 throw redirect(`/${result.value.namespace}/${result.value.name}`);
36}
37
38export default function NewRepo({
39 loaderData,
40 actionData,
41}: Route.ComponentProps) {
42 return (
43 <main className="mx-auto max-w-lg px-4 py-12">
44 <h1 className="text-xl font-semibold">New repository</h1>
45 <Form method="post" className="mt-8 space-y-4">
46 <Field label="Name">
47 <div className="flex items-center gap-2 font-mono text-sm">
48 <select
49 name="workspace"
50 defaultValue={loaderData.selected}
51 aria-label="Workspace"
52 className="rounded-md border border-line bg-bg px-2 py-2 text-sm"
53 >
54 {loaderData.workspaces.map((slug) => (
55 <option key={slug} value={slug}>
56 {slug}
57 </option>
58 ))}
59 </select>
60 <span className="text-muted">/</span>
61 <Input name="name" required autoFocus maxLength={100} />
62 </div>
63 </Field>
64 <Field label="Description (optional)">
65 <Input name="description" maxLength={200} />
66 </Field>
67 <fieldset className="space-y-2 text-sm">
68 <label className="flex items-center gap-2">
69 <input type="radio" name="visibility" value="public" defaultChecked />
70 Public — anyone can see and clone it
71 </label>
72 <label className="flex items-center gap-2">
73 <input type="radio" name="visibility" value="private" />
74 Private — only you
75 </label>
76 </fieldset>
77 <ErrorText>{actionData?.error}</ErrorText>
78 <Button type="submit">Create repository</Button>
79 </Form>
80 </main>
81 );
82}