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.

g1t.sh
107 linesCodeBlameRaw
1import { Check, Hash, Lock, MessagesSquare, PlugZap } from "lucide-react";
2import { type ReactNode, useState } from "react";
3import { useParams } from "react-router";
4
5import { CreateChannelButton, NewMessageButton } from "./actions";
6import { Button } from "../ui/button";
7import { Card } from "../ui/card";
8import { Textarea } from "../ui/textarea";
9
10/** A calm full-height panel for a conversation that cannot be shown. */
11function Panel({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) {
12 return (
13 <div className="flex h-(--page-h) items-center justify-center px-6">
14 <div className="max-w-sm text-center">
15 <Card className="mx-auto flex size-12 items-center justify-center text-muted">{icon}</Card>
16 <h1 className="mt-4 text-base font-semibold">{title}</h1>
17 <div className="mt-1.5 text-sm text-muted">{children}</div>
18 </div>
19 </div>
20 );
21}
22
23/** Chat did not answer: it may be deploying a moment after the site. */
24export function ChatUnavailable({ title = "Chat" }: { title?: string }) {
25 return (
26 <Panel icon={<PlugZap size={20} />} title={`${title} isn't reachable right now`}>
27 Chat didn't answer. It's usually back within a minute; reload to try again. Nothing you sent is lost.
28 </Panel>
29 );
30}
31
32/** A workspace with no conversations yet. */
33export function NoChannels() {
34 const { owner = "" } = useParams();
35 return (
36 <Panel icon={<MessagesSquare size={20} />} title="Start the first conversation">
37 <p>Channels keep a topic in one place. Direct messages are for one person or a few.</p>
38 <div className="mt-5 flex justify-center gap-2">
39 <CreateChannelButton slug={owner.toLowerCase()} variant="button" />
40 <NewMessageButton slug={owner.toLowerCase()} variant="button" />
41 </div>
42 </Panel>
43 );
44}
45
46/** A channel's name with its mark, as the header and the sidebar show it. */
47export function ChannelGlyph({ className }: { className?: string }) {
48 return <Hash size={16} className={className} />;
49}
50
51/**
52 * A channel's link that does not open for the viewer: private, kept to a
53 * workspace they are not in, or not there at all, which reads the same so
54 * nothing about a channel they may not see is told. They may ask whoever
55 * could add them to let them in, with a note.
56 */
57export function RequestAccess({ name, workspace, member }: { name: string; workspace: { slug: string; name: string }; member: boolean }) {
58 const [note, setNote] = useState("");
59 const [state, setState] = useState<"idle" | "sending" | "sent" | "failed">("idle");
60 const ask = async () => {
61 setState("sending");
62 try {
63 const response = await fetch(`/${workspace.slug}/-/chat/api`, {
64 method: "POST",
65 headers: { "content-type": "application/json" },
66 body: JSON.stringify({ intent: "request_access", name, note }),
67 });
68 const result = (await response.json()) as { ok: boolean };
69 setState(result.ok ? "sent" : "failed");
70 } catch {
71 setState("failed");
72 }
73 };
74 return (
75 <Panel icon={<Lock size={20} />} title={`You can't open #${name}`}>
76 <p>
77 {member
78 ? `If #${name} is a private channel in ${workspace.name}, only the people in it can read it.`
79 : `You're not a member of ${workspace.name}. If #${name} is one of its channels, it's for its members or the people asked into it.`}{" "}
80 You can ask to be let in: whoever could add you will see your request.
81 </p>
82 {state === "sent" ? (
83 <p className="mt-5 inline-flex items-center gap-1.5 text-fg">
84 <Check size={15} className="text-accent" />
85 Asked. You'll find it in your sidebar once someone lets you in.
86 </p>
87 ) : (
88 <div className="mt-5 space-y-2 text-left">
89 <Textarea
90 value={note}
91 onChange={(event) => setNote(event.target.value)}
92 maxLength={300}
93 rows={3}
94 placeholder="Say who you are and why you'd like in (optional)"
95 aria-label="A note for whoever lets you in"
96 />
97 <div className="flex items-center justify-between gap-2">
98 <span className="text-xs text-danger">{state === "failed" ? "That didn't go through. Try again." : ""}</span>
99 <Button type="button" onClick={() => void ask()} disabled={state === "sending"}>
100 {state === "sending" ? "Asking…" : "Ask to join"}
101 </Button>
102 </div>
103 </div>
104 )}
105 </Panel>
106 );
107}