1import { useState } from "react";
2
3import { Status } from "@/__generated__/graphql";
4import {
5 useBugAddCommentMutation,
6 useBugAddCommentAndCloseMutation,
7 useBugAddCommentAndReopenMutation,
8 useBugStatusCloseMutation,
9 useBugStatusOpenMutation,
10 BugDetailDocument,
11} from "@/__generated__/graphql";
12import { Markdown } from "@/components/content/Markdown";
13import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
14import { Button } from "@/components/ui/button";
15import { Textarea } from "@/components/ui/textarea";
16import { useAuth } from "@/lib/auth";
17
18interface CommentBoxProps {
19 bugPrefix: string;
20 bugStatus: Status;
21 /** Current repo slug, passed as `ref` in refetch query variables. */
22 ref_?: string | null;
23}
24
25// Write/preview comment form at the bottom of BugDetailPage. Also contains the
26// Close / Reopen button. Hidden entirely in read-only mode (no logged-in user).
27export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
28 const { user } = useAuth();
29 const [message, setMessage] = useState("");
30 const [preview, setPreview] = useState(false);
31
32 const refetchVars = { variables: { ref: ref_, prefix: bugPrefix } };
33 const refetch = { refetchQueries: [{ query: BugDetailDocument, ...refetchVars }] };
34
35 const [addComment, { loading: addingComment }] = useBugAddCommentMutation(refetch);
36 const [addAndClose, { loading: addingAndClosing }] = useBugAddCommentAndCloseMutation(refetch);
37 const [addAndReopen, { loading: addingAndReopening }] =
38 useBugAddCommentAndReopenMutation(refetch);
39 const [statusClose, { loading: closing }] = useBugStatusCloseMutation(refetch);
40 const [statusOpen, { loading: reopening }] = useBugStatusOpenMutation(refetch);
41
42 const isOpen = bugStatus === Status.Open;
43 const busy = addingComment || addingAndClosing || addingAndReopening || closing || reopening;
44 const hasMessage = message.trim().length > 0;
45
46 async function handleComment() {
47 await addComment({ variables: { input: { prefix: bugPrefix, message: message.trim() } } });
48 setMessage("");
49 setPreview(false);
50 }
51
52 async function handleToggleStatus() {
53 if (isOpen) {
54 if (hasMessage) {
55 await addAndClose({ variables: { input: { prefix: bugPrefix, message: message.trim() } } });
56 } else {
57 await statusClose({ variables: { input: { prefix: bugPrefix } } });
58 }
59 } else {
60 if (hasMessage) {
61 await addAndReopen({
62 variables: { input: { prefix: bugPrefix, message: message.trim() } },
63 });
64 } else {
65 await statusOpen({ variables: { input: { prefix: bugPrefix } } });
66 }
67 }
68 setMessage("");
69 setPreview(false);
70 }
71
72 if (!user) return null;
73
74 return (
75 <div className="flex gap-3">
76 <Avatar className="mt-1 size-8 shrink-0">
77 <AvatarImage src={user.avatarUrl ?? undefined} alt={user.displayName} />
78 <AvatarFallback className="text-xs">
79 {user.displayName.slice(0, 2).toUpperCase()}
80 </AvatarFallback>
81 </Avatar>
82
83 <div className="border-border min-w-0 flex-1 rounded-md border">
84 {/* Write / Preview tabs */}
85 <div className="border-border flex border-b">
86 <button
87 onClick={() => setPreview(false)}
88 className={`px-4 py-2 text-sm font-medium transition-colors ${
89 !preview
90 ? "border-primary text-foreground border-b-2"
91 : "text-muted-foreground hover:text-foreground"
92 }`}
93 >
94 Write
95 </button>
96 <button
97 onClick={() => setPreview(true)}
98 disabled={!hasMessage}
99 className={`px-4 py-2 text-sm font-medium transition-colors disabled:opacity-40 ${
100 preview
101 ? "border-primary text-foreground border-b-2"
102 : "text-muted-foreground hover:text-foreground"
103 }`}
104 >
105 Preview
106 </button>
107 </div>
108
109 {preview ? (
110 <div className="min-h-[120px] px-4 py-3">
111 <Markdown content={message} />
112 </div>
113 ) : (
114 <Textarea
115 placeholder="Leave a comment…"
116 className="min-h-[120px] rounded-none border-0 shadow-none focus-visible:ring-0"
117 value={message}
118 onChange={(e) => setMessage(e.target.value)}
119 disabled={busy}
120 />
121 )}
122
123 <div className="border-border flex items-center justify-end gap-2 border-t px-3 py-2">
124 <Button
125 variant="outline"
126 size="sm"
127 onClick={() => {
128 void handleToggleStatus();
129 }}
130 disabled={busy}
131 className="min-w-[7.5rem]"
132 >
133 {isOpen ? "Close issue" : "Reopen issue"}
134 </Button>
135 <Button
136 size="sm"
137 onClick={() => {
138 void handleComment();
139 }}
140 disabled={!hasMessage || busy}
141 >
142 Comment
143 </Button>
144 </div>
145 </div>
146 </div>
147 );
148}