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.

91 linesCodeBlameRaw
1import { Link } from "react-router";
2
3import type { Route } from "./+types/invitations";
4import { InvitationList } from "../components/invitation-list";
5import { answerInvitation } from "../lib/invitations.server";
6import { declinedLine } from "../lib/invitations";
7import { page } from "../lib/meta";
8import { assertSameOrigin, nextPath, requireUser } from "../lib/session.server";
9import { Card } from "../components/ui/card";
10import { identity } from "../lib/services.server";
11import { Unavailable } from "../components/boundary";
12import { isFailed, part } from "../lib/partial";
13
14export function meta(args: Route.MetaArgs) {
15 return page(args, { title: "Invitations · g1t" });
16}
17
18/**
19 * The workspace invitations waiting for the signed-in person's answer.
20 * Nobody joins a workspace without saying yes here (or on the invite's own
21 * page). Linked from the inbox item and the invitation email.
22 */
23export async function loader({ request, context }: Route.LoaderArgs) {
24 const user = requireUser(context, request);
25 const next = nextPath(request);
26 return {
27 // Read here rather than with loadInvitations, which reads a failure as
28 // none: on this page that would say "No invitations waiting" when some
29 // may be. An account still to confirm its address is shown none, as there.
30 invitations: user.verified ? await part(identity.listInvitations(user), "Your invitations") : [],
31 hasWorkspace: (user.workspaces ?? []).length > 0,
32 next: next === "/" ? null : next,
33 };
34}
35
36export async function action({ request, context }: Route.ActionArgs) {
37 assertSameOrigin(request);
38 const user = requireUser(context, request);
39 return (await answerInvitation(user, await request.formData(), request)) ?? null;
40}
41
42export default function Invitations({ loaderData, actionData }: Route.ComponentProps) {
43 const { invitations, hasWorkspace, next } = loaderData;
44 return (
45 <main className="mx-auto max-w-2xl px-4 py-12">
46 <h1 className="text-xl font-semibold">Invitations</h1>
47 <p className="mt-2 text-sm text-muted">
48 Workspaces you are invited to join. You join one only when you accept; declining tells whoever invited you.
49 </p>
50 {actionData?.declined !== undefined && (
51 <p className="mt-4 text-sm text-muted" role="status">
52 {declinedLine(actionData.declined)}
53 </p>
54 )}
55 <div className="mt-6">
56 {isFailed(invitations) ? (
57 <Unavailable what="Your invitations" />
58 ) : invitations.length > 0 ? (
59 <InvitationList
60 invitations={invitations}
61 error={actionData?.invitationError}
62 errorFor={actionData?.invitationId}
63 next={next}
64 />
65 ) : (
66 <Card tone="plain" className="border-dashed px-6 py-10 text-center">
67 <p className="text-sm font-medium">No invitations waiting</p>
68 <p className="mt-1 text-sm text-muted">
69 {hasWorkspace ? (
70 <>
71 When someone invites you to a workspace, it shows here and in your{" "}
72 <Link to="/notifications" className="underline underline-offset-4 hover:text-fg">
73 notifications
74 </Link>
75 .
76 </>
77 ) : (
78 <>
79 <Link to="/workspaces/new" className="underline underline-offset-4 hover:text-fg">
80 Create your workspace
81 </Link>
82 , or ask a workspace's owners to invite you by your username.
83 </>
84 )}
85 </p>
86 </Card>
87 )}
88 </div>
89 </main>
90 );
91}