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

206 lines8,402 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.

Workspace names and icons, and a component kit for every control1import { Box, Download, GitBranch, Globe, Lock, Sparkles } from "lucide-react";
Projects: what a workspace builds and runs, first on every page2import { 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";
Workspace names and icons, and a component kit for every control7import { Avatar, Button, ErrorText } from "../components/ui";
8import { Field, FieldDescription, FieldLabel, FieldLegend, FieldSet } from "../components/ui/field";
9import { Input, InputAddon, InputGroup } from "../components/ui/input";
10import { RadioCard, RadioGroup } from "../components/ui/radio-group";
11import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "../components/ui/select";
Rust repos service with shipping; pull requests kept in the model12import { repos } from "../lib/services.server";
Initial g1t: services, event bus, intents and attempts13import { assertSameOrigin, requireUser } from "../lib/session.server";
14
Social cards for every page: og.g1t.sh15export function meta(args: Route.MetaArgs) {
16 return page(args, { title: "New project · g1t" });
Initial g1t: services, event bus, intents and attempts17}
18
19export function loader({ request, context }: Route.LoaderArgs) {
Workspaces own repositories20 const user = requireUser(context, request);
Workspace names and icons, and a component kit for every control21 const workspaces = (user.workspaces ?? []).map((membership) => ({
22 slug: membership.slug,
23 role: membership.role,
24 name: membership.name ?? membership.slug,
25 avatar: membership.avatar ?? null,
26 }));
Projects: what a workspace builds and runs, first on every page27 // Projects live in a workspace, so there has to be one first.
Workspaces own repositories28 if (workspaces.length === 0) throw redirect("/workspaces/new");
29 const asked = new URL(request.url).searchParams.get("workspace");
30 return {
31 workspaces,
Workspace names and icons, and a component kit for every control32 selected: asked && workspaces.some((workspace) => workspace.slug === asked) ? asked : workspaces[0].slug,
Workspaces own repositories33 };
Initial g1t: services, event bus, intents and attempts34}
35
36export async function action({ request, context }: Route.ActionArgs) {
37 assertSameOrigin(request);
38 const user = requireUser(context, request);
39 const form = await request.formData();
Projects: what a workspace builds and runs, first on every page40 // A project hosted on g1t: its repository is made with it, and the
41 // project takes the repository's name.
Rust repos service with shipping; pull requests kept in the model42 const result = await repos.create(user, {
Workspaces own repositories43 namespace: String(form.get("workspace") ?? ""),
Initial g1t: services, event bus, intents and attempts44 name: String(form.get("name") ?? ""),
45 description: String(form.get("description") ?? ""),
46 isPrivate: form.get("visibility") === "private",
Projects: what a workspace builds and runs, first on every page47 importUrl: form.get("source") === "import" ? String(form.get("importUrl") ?? "").trim() || undefined : undefined,
Initial g1t: services, event bus, intents and attempts48 });
49 if (!result.ok) return { error: result.error.message };
50 throw redirect(`/${result.value.namespace}/${result.value.name}`);
51}
52
Projects: what a workspace builds and runs, first on every page53type Source = "empty" | "import";
54
55const SOURCES: { id: Source | "mirror"; title: string; text: string; icon: React.ReactNode; soon?: boolean }[] = [
56 {
57 id: "empty",
58 title: "Start empty",
59 text: "A new repository on g1t. Push to it, or hand an agent the first issue.",
Workspace names and icons, and a component kit for every control60 icon: <Sparkles />,
Projects: what a workspace builds and runs, first on every page61 },
62 {
63 id: "import",
64 title: "Import code",
65 text: "Copy a public repository from GitHub or any git host into a new one on g1t.",
Workspace names and icons, and a component kit for every control66 icon: <Download />,
Projects: what a workspace builds and runs, first on every page67 },
68 {
69 id: "mirror",
70 title: "Mirror GitHub, GitLab or Bitbucket",
71 text: "Keep the code where it is and get g1t's deployments and agents on it.",
Workspace names and icons, and a component kit for every control72 icon: <GitBranch />,
Projects: what a workspace builds and runs, first on every page73 soon: true,
74 },
75];
76
Workspace names and icons, and a component kit for every control77const ROLE_LABELS: Record<string, string> = { owner: "Owner", admin: "Admin", member: "Member" };
78
Projects: what a workspace builds and runs, first on every page79export default function NewProject({ loaderData, actionData }: Route.ComponentProps) {
80 const [source, setSource] = useState<Source>("empty");
Workspace names and icons, and a component kit for every control81 const [workspace, setWorkspace] = useState(loaderData.selected);
82 const [name, setName] = useState("");
Projects: what a workspace builds and runs, first on every page83 const busy = useNavigation().state === "submitting";
Initial g1t: services, event bus, intents and attempts84 return (
Projects: what a workspace builds and runs, first on every page85 <main className="mx-auto max-w-2xl px-4 py-12">
86 <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
87 <Box size={18} />
88 </span>
89 <h1 className="mt-4 text-2xl font-semibold tracking-tight">New project</h1>
90 <p className="mt-1.5 text-sm text-muted">
91 A project is what you build and run. Its code lives in its source; its deployments, environments, secrets and
92 variables belong to the project.
93 </p>
94
95 <Form method="post" className="mt-8 space-y-8">
Workspace names and icons, and a component kit for every control96 <FieldSet>
97 <FieldLegend>Where its code comes from</FieldLegend>
98 <RadioGroup
99 name="source"
100 value={source}
101 onValueChange={(value) => setSource(value as Source)}
102 aria-label="Where its code comes from"
103 className="gap-3 sm:grid-cols-3"
104 >
105 {SOURCES.map((option) => (
106 <RadioCard
107 key={option.id}
108 value={option.id}
109 title={option.title}
110 description={option.text}
111 icon={option.icon}
112 disabled={option.soon}
113 badge={option.soon ? "Soon" : undefined}
114 />
115 ))}
116 </RadioGroup>
117 </FieldSet>
Projects: what a workspace builds and runs, first on every page118
119 {source === "import" && (
Workspace names and icons, and a component kit for every control120 <Field className="animate-fade-in">
121 <FieldLabel htmlFor="importUrl">Repository to import</FieldLabel>
122 <Input id="importUrl" name="importUrl" type="url" required placeholder="https://github.com/owner/repo" />
123 <FieldDescription>The address of a public repository. Its default branch is copied, up to 40 MB.</FieldDescription>
Projects: what a workspace builds and runs, first on every page124 </Field>
125 )}
126
Workspace names and icons, and a component kit for every control127 <Field>
128 <FieldLabel htmlFor="name">Name</FieldLabel>
129 <InputGroup className="h-10">
130 <Select name="workspace" value={workspace} onValueChange={setWorkspace}>
131 <SelectTrigger
132 aria-label="Workspace"
133 className="h-full w-auto max-w-[55%] shrink-0 rounded-none border-0 bg-surface px-2.5 font-medium hover:bg-raised focus-visible:ring-0 data-[state=open]:bg-raised data-[state=open]:ring-0 sm:max-w-[45%]"
134 >
135 <SelectValue />
136 </SelectTrigger>
137 <SelectContent align="start" className="min-w-64">
138 <SelectGroup>
139 <SelectLabel>Workspaces</SelectLabel>
140 {loaderData.workspaces.map((option) => (
141 <SelectItem
142 key={option.slug}
143 value={option.slug}
144 icon={<Avatar name={option.name} image={option.avatar} size={18} square />}
145 description={`g1t.sh/${option.slug} · ${ROLE_LABELS[option.role] ?? option.role}`}
146 >
147 {option.name}
148 </SelectItem>
149 ))}
150 </SelectGroup>
151 </SelectContent>
152 </Select>
153 <InputAddon className="border-l border-line px-2 font-mono text-base text-faint">/</InputAddon>
154 <Input
155 id="name"
156 name="name"
157 required
158 autoFocus
159 maxLength={100}
160 placeholder="my-app"
161 value={name}
162 onChange={(event) => setName(event.target.value)}
163 className="pl-1 font-mono"
164 />
165 </InputGroup>
166 <FieldDescription>
167 The project and its repository share it:{" "}
168 <span className="font-mono text-muted">
169 g1t.sh/{workspace}/<span className={name ? "text-fg" : undefined}>{name || "name"}</span>
170 </span>
171 </FieldDescription>
Initial g1t: services, event bus, intents and attempts172 </Field>
Workspace names and icons, and a component kit for every control173
174 <Field>
175 <FieldLabel htmlFor="description">
176 Description <span className="font-normal text-faint">(optional)</span>
177 </FieldLabel>
178 <Input id="description" name="description" maxLength={200} placeholder="What it is, in a line" />
Agents as a team: lifecycle, merge queue, billing and a new shell179 </Field>
Workspace names and icons, and a component kit for every control180
181 <FieldSet>
182 <FieldLegend>Who can see it</FieldLegend>
183 <RadioGroup name="visibility" defaultValue="public" aria-label="Who can see it" className="gap-3 sm:grid-cols-2">
184 <RadioCard
185 value="public"
186 icon={<Globe />}
187 title="Public"
188 description="Anyone can see its code and open issues."
189 />
190 <RadioCard
191 value="private"
192 icon={<Lock />}
193 title="Private"
194 description="Only members of the workspace."
195 />
196 </RadioGroup>
197 </FieldSet>
198
Initial g1t: services, event bus, intents and attempts199 <ErrorText>{actionData?.error}</ErrorText>
Projects: what a workspace builds and runs, first on every page200 <Button type="submit" variant="accent" disabled={busy}>
201 Create project
202 </Button>
Initial g1t: services, event bus, intents and attempts203 </Form>
204 </main>
205 );
206}