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

170 lines7,129 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.

Projects: what a workspace builds and runs, first on every page1import { Box, Download, GitBranch, Sparkles } from "lucide-react";
2import { useState } from "react";
3import { Form, redirect, useNavigation } from "react-router";
Initial g1t: services, event bus, intents and attempts4
5import type { Route } from "./+types/new";
Social cards for every page: og.g1t.sh6import { page } from "../lib/meta";
Initial g1t: services, event bus, intents and attempts7import { Button, ErrorText, Field, Input } from "../components/ui";
Rust repos service with shipping; pull requests kept in the model8import { repos } from "../lib/services.server";
Initial g1t: services, event bus, intents and attempts9import { assertSameOrigin, requireUser } from "../lib/session.server";
10
Social cards for every page: og.g1t.sh11export function meta(args: Route.MetaArgs) {
12 return page(args, { title: "New project · g1t" });
Initial g1t: services, event bus, intents and attempts13}
14
15export function loader({ request, context }: Route.LoaderArgs) {
Workspaces own repositories16 const user = requireUser(context, request);
17 const workspaces = (user.workspaces ?? []).map((membership) => membership.slug);
Projects: what a workspace builds and runs, first on every page18 // Projects live in a workspace, so there has to be one first.
Workspaces own repositories19 if (workspaces.length === 0) throw redirect("/workspaces/new");
20 const asked = new URL(request.url).searchParams.get("workspace");
21 return {
22 workspaces,
23 selected: asked && workspaces.includes(asked) ? asked : workspaces[0],
24 };
Initial g1t: services, event bus, intents and attempts25}
26
27export async function action({ request, context }: Route.ActionArgs) {
28 assertSameOrigin(request);
29 const user = requireUser(context, request);
30 const form = await request.formData();
Projects: what a workspace builds and runs, first on every page31 // A project hosted on g1t: its repository is made with it, and the
32 // project takes the repository's name.
Rust repos service with shipping; pull requests kept in the model33 const result = await repos.create(user, {
Workspaces own repositories34 namespace: String(form.get("workspace") ?? ""),
Initial g1t: services, event bus, intents and attempts35 name: String(form.get("name") ?? ""),
36 description: String(form.get("description") ?? ""),
37 isPrivate: form.get("visibility") === "private",
Projects: what a workspace builds and runs, first on every page38 importUrl: form.get("source") === "import" ? String(form.get("importUrl") ?? "").trim() || undefined : undefined,
Initial g1t: services, event bus, intents and attempts39 });
40 if (!result.ok) return { error: result.error.message };
41 throw redirect(`/${result.value.namespace}/${result.value.name}`);
42}
43
Projects: what a workspace builds and runs, first on every page44type Source = "empty" | "import";
45
46const SOURCES: { id: Source | "mirror"; title: string; text: string; icon: React.ReactNode; soon?: boolean }[] = [
47 {
48 id: "empty",
49 title: "Start empty",
50 text: "A new repository on g1t. Push to it, or hand an agent the first issue.",
51 icon: <Sparkles size={16} />,
52 },
53 {
54 id: "import",
55 title: "Import code",
56 text: "Copy a public repository from GitHub or any git host into a new one on g1t.",
57 icon: <Download size={16} />,
58 },
59 {
60 id: "mirror",
61 title: "Mirror GitHub, GitLab or Bitbucket",
62 text: "Keep the code where it is and get g1t's deployments and agents on it.",
63 icon: <GitBranch size={16} />,
64 soon: true,
65 },
66];
67
68export default function NewProject({ loaderData, actionData }: Route.ComponentProps) {
69 const [source, setSource] = useState<Source>("empty");
70 const busy = useNavigation().state === "submitting";
Initial g1t: services, event bus, intents and attempts71 return (
Projects: what a workspace builds and runs, first on every page72 <main className="mx-auto max-w-2xl px-4 py-12">
73 <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
74 <Box size={18} />
75 </span>
76 <h1 className="mt-4 text-2xl font-semibold tracking-tight">New project</h1>
77 <p className="mt-1.5 text-sm text-muted">
78 A project is what you build and run. Its code lives in its source; its deployments, environments, secrets and
79 variables belong to the project.
80 </p>
81
82 <Form method="post" className="mt-8 space-y-8">
83 <fieldset>
84 <legend className="mb-3 text-sm font-medium text-muted">Where its code comes from</legend>
85 <div className="grid gap-3 sm:grid-cols-3">
86 {SOURCES.map((option) => {
87 const chosen = source === option.id;
88 return (
89 <label
90 key={option.id}
91 className={`relative rounded-xl border p-4 transition-colors ${
92 option.soon
93 ? "cursor-not-allowed border-dashed border-line opacity-60"
94 : chosen
95 ? "cursor-pointer border-accent bg-accent/5"
96 : "cursor-pointer border-line hover:border-line-strong"
97 }`}
98 >
99 <input
100 type="radio"
101 name="source"
102 value={option.id}
103 checked={chosen}
104 disabled={option.soon}
105 onChange={() => !option.soon && setSource(option.id as Source)}
106 className="sr-only"
107 />
108 <span className={chosen ? "text-accent" : "text-muted"}>{option.icon}</span>
109 <span className="mt-2 block text-sm font-medium">{option.title}</span>
110 <span className="mt-1 block text-xs text-muted">{option.text}</span>
111 {option.soon && (
112 <span className="absolute top-3 right-3 rounded-full bg-raised px-2 py-0.5 text-[0.6875rem] text-muted ring-1 ring-line">
113 Soon
114 </span>
115 )}
116 </label>
117 );
118 })}
119 </div>
120 </fieldset>
121
122 {source === "import" && (
123 <Field
124 label="Repository to import"
125 hint="The address of a public repository. Its default branch is copied, up to 40 MB."
126 >
127 <Input name="importUrl" type="url" required placeholder="https://github.com/owner/repo" />
128 </Field>
129 )}
130
131 <Field label="Name" hint="The project and its repository share it: g1t.sh/<workspace>/<name>.">
Initial g1t: services, event bus, intents and attempts132 <div className="flex items-center gap-2 font-mono text-sm">
Workspaces own repositories133 <select
134 name="workspace"
135 defaultValue={loaderData.selected}
136 aria-label="Workspace"
137 className="rounded-md border border-line bg-bg px-2 py-2 text-sm"
138 >
139 {loaderData.workspaces.map((slug) => (
140 <option key={slug} value={slug}>
141 {slug}
142 </option>
143 ))}
144 </select>
145 <span className="text-muted">/</span>
Projects: what a workspace builds and runs, first on every page146 <Input name="name" required autoFocus maxLength={100} placeholder="my-app" />
Initial g1t: services, event bus, intents and attempts147 </div>
148 </Field>
149 <Field label="Description (optional)">
Projects: what a workspace builds and runs, first on every page150 <Input name="description" maxLength={200} placeholder="What it is, in a line" />
Agents as a team: lifecycle, merge queue, billing and a new shell151 </Field>
Initial g1t: services, event bus, intents and attempts152 <fieldset className="space-y-2 text-sm">
Projects: what a workspace builds and runs, first on every page153 <legend className="mb-2 font-medium text-muted">Who can see it</legend>
Initial g1t: services, event bus, intents and attempts154 <label className="flex items-center gap-2">
Projects: what a workspace builds and runs, first on every page155 <input type="radio" name="visibility" value="public" defaultChecked className="accent-accent" />
156 Public: anyone can see its code and open issues
Initial g1t: services, event bus, intents and attempts157 </label>
158 <label className="flex items-center gap-2">
Projects: what a workspace builds and runs, first on every page159 <input type="radio" name="visibility" value="private" className="accent-accent" />
160 Private: only members of the workspace
Initial g1t: services, event bus, intents and attempts161 </label>
162 </fieldset>
163 <ErrorText>{actionData?.error}</ErrorText>
Projects: what a workspace builds and runs, first on every page164 <Button type="submit" variant="accent" disabled={busy}>
165 Create project
166 </Button>
Initial g1t: services, event bus, intents and attempts167 </Form>
168 </main>
169 );
170}