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

94 lines4,046 bytesCodeBlame
1import { Form, data, redirect } from "react-router";
2
3import type { Route } from "./+types/settings";
4import { page } from "../../lib/meta";
5import { AvatarField } from "../../components/avatar-field";
6import { Button, ErrorText, Field, Input } from "../../components/ui";
7import { readAvatarUpload } from "../../lib/avatar-upload";
8import { identity } from "../../lib/services.server";
9import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
10
11export function meta({ params, ...args }: Route.MetaArgs) {
12 return page(args, { title: `Settings · ${params.owner} · g1t` });
13}
14
15export async function loader({ params, context }: Route.LoaderArgs) {
16 // Owners only; to anyone else the page does not exist.
17 if (roleIn(getViewer(context), params.owner) !== "owner") {
18 throw data(null, { status: 404 });
19 }
20 const workspace = await identity.getWorkspace(params.owner);
21 if (!workspace) throw data(null, { status: 404 });
22 return { workspace };
23}
24
25export async function action({ request, params, context }: Route.ActionArgs) {
26 assertSameOrigin(request);
27 const user = requireUser(context, request);
28 const form = await request.formData();
29 const intent = form.get("intent");
30 // The icon: identity checks the owner and the image's bytes again.
31 if (intent === "avatar" || intent === "remove-avatar") {
32 let image: string | null = null;
33 if (intent === "avatar") {
34 const upload = await readAvatarUpload(form);
35 if ("error" in upload) return { avatarError: upload.error };
36 image = upload.image;
37 }
38 const result = await identity.setWorkspaceAvatar(user, params.owner, image);
39 if (!result.ok) return { avatarError: result.error.message };
40 return { saved: "avatar" as const };
41 }
42 const result = await identity.updateWorkspace(user, params.owner, {
43 name: String(form.get("displayName") ?? ""),
44 description: String(form.get("description") ?? ""),
45 });
46 if (!result.ok) return { error: result.error.message };
47 throw redirect(`/${result.value.slug}`);
48}
49
50export default function WorkspaceSettings({ loaderData, actionData }: Route.ComponentProps) {
51 const { workspace } = loaderData;
52 return (
53 <div className="max-w-lg space-y-10">
54 <section>
55 <h2 className="font-medium">Icon</h2>
56 <div className="mt-4">
57 <AvatarField
58 name={workspace.slug}
59 image={workspace.avatar}
60 square
61 error={actionData && "avatarError" in actionData ? actionData.avatarError : undefined}
62 about="Shown beside the workspace's name everywhere on g1t, and on its link previews. Without one, g1t draws its first letter."
63 />
64 </div>
65 </section>
66
67 <section>
68 <h2 className="font-medium">Workspace details</h2>
69 <Form method="post" className="mt-5 space-y-4">
70 <Field
71 label="Display name"
72 hint="How people see the workspace: in the sidebar, at the top of its page and in link previews. Up to 80 characters; spaces and capitals are fine."
73 >
74 <Input name="displayName" maxLength={80} defaultValue={workspace.name} placeholder={workspace.slug} />
75 </Field>
76 <Field label="Description" hint="One line, shown at the top of the workspace's page.">
77 <Input name="description" maxLength={160} defaultValue={workspace.description ?? ""} />
78 </Field>
79 <ErrorText>{actionData && "error" in actionData ? actionData.error : undefined}</ErrorText>
80 <Button type="submit">Save</Button>
81 </Form>
82 </section>
83
84 <section>
85 <h2 className="font-medium">Address</h2>
86 <p className="mt-3 font-mono text-sm text-fg">g1t.sh/{workspace.slug}</p>
87 <p className="mt-1.5 text-xs text-faint">
88 The workspace's slug: lowercase, in every repository's address and every clone URL. It cannot be
89 changed, so links and clones keep working. Change the display name instead.
90 </p>
91 </section>
92 </div>
93 );
94}