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.

84 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">Workspaces you are invited to join. You join one only when you accept; declining tells whoever invited you.</p>
48 {actionData?.declined !== undefined && (
49 <p className="mt-4 text-sm text-muted" role="status">
50 {declinedLine(actionData.declined)}
51 </p>
52 )}
53 <div className="mt-6">
54 {isFailed(invitations) ? (
55 <Unavailable what="Your invitations" />
56 ) : invitations.length > 0 ? (
57 <InvitationList invitations={invitations} error={actionData?.invitationError} errorFor={actionData?.invitationId} next={next} />
58 ) : (
59 <Card tone="plain" className="border-dashed px-6 py-10 text-center">
60 <p className="text-sm font-medium">No invitations waiting</p>
61 <p className="mt-1 text-sm text-muted">
62 {hasWorkspace ? (
63 <>
64 When someone invites you to a workspace, it shows here and in your{" "}
65 <Link to="/notifications" className="underline underline-offset-4 hover:text-fg">
66 notifications
67 </Link>
68 .
69 </>
70 ) : (
71 <>
72 <Link to="/workspaces/new" className="underline underline-offset-4 hover:text-fg">
73 Create your workspace
74 </Link>
75 , or ask a workspace's owners to invite you by your username.
76 </>
77 )}
78 </p>
79 </Card>
80 )}
81 </div>
82 </main>
83 );
84}