Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

95 linesCodeBlameRaw
1import { Form, Link } from "react-router";
2
3import type { Route } from "./+types/github";
4import { page } from "../../lib/meta";
5import { ErrorText, SubmitButton, TimeAgo } from "../../components/ui";
6import { Hint } from "../../components/ui/hint";
7import { assertSameOrigin, requireUser } from "../../lib/session.server";
8import { GithubMark } from "../../components/github";
9import { githubSignIn } from "../../lib/github.server";
10import { Unavailable } from "../../components/boundary";
11import { isFailed, part } from "../../lib/partial";
12
13export function meta(args: Route.MetaArgs) {
14 return page(args, { title: "GitHub · Settings · g1t" });
15}
16
17export async function loader({ request, context }: Route.LoaderArgs) {
18 const user = requireUser(context, request);
19 // Not read is not "not set up": the page says it could not be loaded.
20 return { github: await part(githubSignIn.account(user), "Your GitHub link") };
21}
22
23export async function action({ request, context }: Route.ActionArgs) {
24 assertSameOrigin(request);
25 const user = requireUser(context, request);
26 const form = await request.formData();
27 if (form.get("intent") === "unlink-github") {
28 const result = await githubSignIn.unlink(user);
29 return result.ok ? null : { githubError: result.error.message };
30 }
31 return null;
32}
33
34export default function GithubSettings({ loaderData, actionData }: Route.ComponentProps) {
35 const { github } = loaderData;
36 if (isFailed(github)) return <Unavailable what="Your GitHub link" />;
37 if (!github?.enabled) {
38 return <p className="text-sm text-muted">Signing in with GitHub is not set up here.</p>;
39 }
40 return (
41 <section id="github" className="scroll-mt-20">
42 {github.account ? (
43 <div className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border border-line px-4 py-3">
44 <GithubMark className="size-5 shrink-0" />
45 <div className="min-w-0">
46 <p className="text-sm">
47 Linked to <span className="font-mono">@{github.account.login}</span>
48 </p>
49 <p className="text-xs text-faint">
50 Since <TimeAgo at={github.account.linkedAt} />
51 {!github.account.authorized && " · access ended: link it again to import repositories"}
52 </p>
53 </div>
54 <div className="ml-auto flex items-center gap-2">
55 {!github.account.authorized && (
56 <a href="/auth/github?link=1" className="text-sm text-muted hover:text-fg">
57 Link again
58 </a>
59 )}
60 <Form method="post">
61 <input type="hidden" name="intent" value="unlink-github" />
62 <Hint label={github.hasPassword ? undefined : "GitHub is how you sign in. Set a password first."} disabled={!github.hasPassword}>
63 <SubmitButton variant="outline" pending="Unlinking…" match={{ intent: "unlink-github" }} disabled={!github.hasPassword}>
64 Unlink
65 </SubmitButton>
66 </Hint>
67 </Form>
68 </div>
69 </div>
70 ) : (
71 <a
72 href="/auth/github?link=1"
73 className="inline-flex items-center gap-2 rounded-md border border-line px-3.5 py-2 text-sm font-medium text-fg/90 hover:border-line-strong hover:bg-surface hover:text-fg"
74 >
75 <GithubMark /> Link GitHub
76 </a>
77 )}
78 {github.account?.authorized && (
79 <p className="mt-3 text-sm text-muted">
80 To bring repositories into a workspace,{" "}
81 <Link to="/new/github" className="text-fg underline underline-offset-4">
82 import from GitHub
83 </Link>
84 .
85 </p>
86 )}
87 {github.account && !github.hasPassword && (
88 <p className="mt-2 text-xs text-faint">
89 GitHub is the only way you sign in, so it cannot be unlinked yet. Sign out and use Forgot your password to set one.
90 </p>
91 )}
92 <ErrorText>{actionData?.githubError}</ErrorText>
93 </section>
94 );
95}