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.

81 linesCodeBlameRaw
1import { Form } from "react-router";
2
3import type { WorkspaceInvitation } from "@g1t/contracts";
4
5import { ErrorText, SubmitButton } from "./ui";
6import { Avatar } from "./ui/avatar";
7import { Card } from "./ui/card";
8import { UserCard } from "./user-card";
9import { invitationLine } from "../lib/invitations";
10
11/**
12 * The workspace invitations waiting for the person's answer, each with
13 * Accept and Decline. Accepting joins with the role it names; declining
14 * tells whoever sent it. `next` is where accepting goes, when not to the
15 * workspace itself.
16 */
17export function InvitationList({
18 invitations,
19 error,
20 errorFor,
21 next,
22}: {
23 invitations: WorkspaceInvitation[];
24 error?: string | null;
25 errorFor?: string | null;
26 next?: string | null;
27}) {
28 return (
29 <Card asChild tone="plain" divided>
30 <ul>
31 {invitations.map((invitation) => (
32 <li key={invitation.id} className="px-4 py-4">
33 <div className="flex flex-wrap items-center gap-3">
34 <Avatar name={invitation.workspace.slug} image={invitation.workspace.avatar} size={32} square />
35 <div className="min-w-0 grow basis-48">
36 <p className="text-sm font-medium">
37 {invitation.workspace.name} <span className="font-mono text-xs font-normal text-faint">{invitation.workspace.slug}</span>
38 </p>
39 <p className="mt-0.5 text-xs text-muted">
40 {invitation.invitedBy ? (
41 <>
42 <UserCard username={invitation.invitedBy.username}>
43 <span className="font-mono text-fg/90">@{invitation.invitedBy.username}</span>
44 </UserCard>{" "}
45 </>
46 ) : (
47 "The g1t team "
48 )}
49 {invitationLine(invitation)}
50 </p>
51 </div>
52 <div className="flex items-center gap-2">
53 <Form method="post">
54 <input type="hidden" name="intent" value="decline-invitation" />
55 <input type="hidden" name="id" value={invitation.id} />
56 <input type="hidden" name="workspace" value={invitation.workspace.name} />
57 <SubmitButton variant="outline" match={{ intent: "decline-invitation", id: invitation.id }} pending="Declining…">
58 Decline
59 </SubmitButton>
60 </Form>
61 <Form method="post">
62 <input type="hidden" name="intent" value="accept-invitation" />
63 <input type="hidden" name="id" value={invitation.id} />
64 {next && <input type="hidden" name="next" value={next} />}
65 <SubmitButton match={{ intent: "accept-invitation", id: invitation.id }} pending="Joining…">
66 Accept
67 </SubmitButton>
68 </Form>
69 </div>
70 </div>
71 {errorFor === invitation.id && (
72 <div className="mt-2">
73 <ErrorText>{error}</ErrorText>
74 </div>
75 )}
76 </li>
77 ))}
78 </ul>
79 </Card>
80 );
81}