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