SupportFormPage.tsx282 lines · main
1// @ts-nocheck
2import * as Sentry from '@sentry/nextjs'
3import { Loader2, Wrench } from 'lucide-react'
4import Link from 'next/link'
5import { useCallback, useReducer, type Dispatch, type PropsWithChildren } from 'react'
6import type { UseFormReturn } from 'react-hook-form'
7import SVG from 'react-inlinesvg'
8import { toast } from 'sonner'
9import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
10import { Admonition } from 'ui-patterns/admonition'
11
12import { AIAssistantOption } from './AIAssistantOption'
13import { DiscordCTACard } from './DiscordCTACard'
14import { IncidentAdmonition } from './IncidentAdmonition'
15import { Success } from './Success'
16import type { ExtendedSupportCategories } from './Support.constants'
17import type { SupportFormValues } from './SupportForm.schema'
18import {
19 createInitialSupportFormState,
20 supportFormReducer,
21 type SupportFormActions,
22 type SupportFormState,
23} from './SupportForm.state'
24import { NO_PROJECT_MARKER } from './SupportForm.utils'
25import { SupportFormV2 } from './SupportFormV2'
26import { useSupportForm } from './useSupportForm'
27import CopyButton from '@/components/ui/CopyButton'
28import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
29import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
30import { useStateTransition } from '@/hooks/misc/useStateTransition'
31import { BASE_PATH, DOCS_URL } from '@/lib/constants'
32
33export { SupportForm, SupportFormStatusButton } from './SupportSidebarForm'
34
35function useSupportFormTelemetry() {
36 const { mutate: sendEvent } = useSendEventMutation()
37
38 return useCallback(
39 ({
40 projectRef,
41 orgSlug,
42 category,
43 }: {
44 projectRef: string | undefined
45 orgSlug: string | undefined
46 category: ExtendedSupportCategories
47 }) =>
48 sendEvent({
49 action: 'support_ticket_submitted',
50 properties: {
51 ticketCategory: category,
52 },
53 groups: {
54 project: projectRef,
55 organization: orgSlug,
56 },
57 }),
58 [sendEvent]
59 )
60}
61
62export function SupportFormPage() {
63 return <SupportFormPageContent />
64}
65
66function SupportFormPageContent() {
67 const [state, dispatch] = useReducer(supportFormReducer, undefined, createInitialSupportFormState)
68 const { form, initialError, projectRef, orgSlug } = useSupportForm(dispatch)
69
70 const {
71 data: allStatusPageEvents,
72 isPending: isIncidentsPending,
73 isError: isIncidentsError,
74 } = useIncidentStatusQuery()
75 const { incidents = [] } = allStatusPageEvents ?? {}
76 const hasActiveIncidents =
77 !isIncidentsPending && !isIncidentsError && incidents && incidents.length > 0
78
79 const sendTelemetry = useSupportFormTelemetry()
80 useStateTransition(state, 'submitting', 'success', (_, curr) => {
81 toast.success('Support request sent. Thank you!')
82 sendTelemetry({
83 projectRef: curr.sentProjectRef,
84 orgSlug: curr.sentOrgSlug,
85 category: curr.sentCategory,
86 })
87 })
88
89 useStateTransition(state, 'submitting', 'error', (_, curr) => {
90 toast.error(`Failed to submit support ticket: ${curr.message}`)
91 Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`)
92 dispatch({ type: 'RETURN_TO_EDITING' })
93 })
94
95 const isSuccess = state.type === 'success'
96
97 return (
98 <SupportFormWrapper>
99 <SupportFormHeader />
100
101 <IncidentAdmonition isActive={hasActiveIncidents} />
102
103 {!isSuccess && !hasActiveIncidents && (
104 <div className="flex flex-col gap-y-4">
105 <AIAssistantOption projectRef={projectRef} organizationSlug={orgSlug} />
106 <DiscordCTACard organizationSlug={orgSlug} />
107 </div>
108 )}
109
110 <SupportFormBody
111 form={form}
112 state={state}
113 dispatch={dispatch}
114 initialError={initialError}
115 selectedProjectRef={projectRef}
116 />
117 {!isSuccess && <SupportFormDirectEmailInfo projectRef={projectRef} />}
118 </SupportFormWrapper>
119 )
120}
121
122function SupportFormWrapper({ children }: PropsWithChildren) {
123 return (
124 <div className="relative overflow-y-auto overflow-x-hidden">
125 <div className="mx-auto my-16 max-w-2xl w-full px-4 lg:px-6">
126 <div className="flex flex-col gap-y-8">{children}</div>
127 </div>
128 </div>
129 )
130}
131
132function SupportFormHeader() {
133 const { data: allStatusPageEvents, isPending: isLoading, isError } = useIncidentStatusQuery()
134 const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {}
135 const isMaintenance = maintenanceEvents.length > 0
136 const isIncident = incidents.length > 0
137
138 return (
139 <div className="flex flex-col items-start justify-between gap-y-2 sm:flex-row sm:items-center">
140 <div className="flex items-center space-x-3">
141 <SVG src={`${BASE_PATH}/img/briven-logo.svg`} className="h-4 w-4" />
142 <h3 className="m-0 text-lg">Briven support</h3>
143 </div>
144
145 <div className="flex items-center gap-x-3">
146 <Button asChild type="default" icon={<Wrench />}>
147 <Link
148 href={`${DOCS_URL}/guides/troubleshooting?products=platform`}
149 target="_blank"
150 rel="noreferrer"
151 >
152 Troubleshooting
153 </Link>
154 </Button>
155 <Tooltip>
156 <TooltipTrigger asChild>
157 <Button
158 asChild
159 type="default"
160 icon={
161 isLoading ? (
162 <Loader2 className="animate-spin" />
163 ) : (
164 <div
165 className={cn('h-2 w-2 rounded-full', isIncident ? 'bg-warning' : 'bg-brand')}
166 />
167 )
168 }
169 >
170 <Link href="https://status.supabase.com/" target="_blank" rel="noreferrer">
171 {isLoading
172 ? 'Checking status'
173 : isError
174 ? 'Failed to check status'
175 : isIncident
176 ? 'Active incident ongoing'
177 : isMaintenance
178 ? 'Scheduled maintenance'
179 : 'All systems operational'}
180 </Link>
181 </Button>
182 </TooltipTrigger>
183 <TooltipContent side="bottom" align="center">
184 Check the Briven status page
185 </TooltipContent>
186 </Tooltip>
187 </div>
188 </div>
189 )
190}
191
192interface SupportFormDirectEmailInfoProps {
193 projectRef: string | null
194}
195
196function SupportFormDirectEmailInfo({ projectRef }: SupportFormDirectEmailInfoProps) {
197 const hasProjectRef = projectRef && projectRef !== NO_PROJECT_MARKER
198
199 return (
200 <Admonition
201 type="default"
202 title="Having trouble submitting the form?"
203 description={
204 <>
205 <p className="mb-2.5!">
206 Please email us directly. Include your project ID and as much information as possible.
207 </p>
208 <p className="flex items-center gap-x-1.5 flex-wrap">
209 Email:{' '}
210 <span className="inline-flex items-center gap-x-1">
211 <a
212 href={`mailto:support@supabase.com?subject=${encodeURIComponent('Support Request')}${hasProjectRef ? `${encodeURIComponent(' for Project ID: ')}${encodeURIComponent(projectRef)}` : ''}&body=${encodeURIComponent('Here is a detailed description of the problem I am experiencing and any other information that might be helpful...')}`}
213 className="hover:text-foreground transition-colors duration-100"
214 >
215 <code className="text-code-inline text-foreground-light! underline decoration-foreground-lighter/50 hover:decoration-foreground-lighter/80 transition-colors duration-100">
216 support@supabase.com
217 </code>
218 </a>
219 <CopyButton
220 type="text"
221 text="support@supabase.com"
222 iconOnly
223 onClick={() => toast.success('Copied email address to clipboard')}
224 />
225 </span>
226 </p>
227 {hasProjectRef && (
228 <p className="flex items-center gap-x-1.5 flex-wrap">
229 Project ID:{' '}
230 <span className="inline-flex items-center gap-x-1">
231 <code className="text-code-inline text-foreground-light!">{projectRef}</code>
232 <CopyButton
233 iconOnly
234 type="text"
235 text={projectRef}
236 onClick={() => toast.success('Copied project ID to clipboard')}
237 />
238 </span>
239 </p>
240 )}
241 </>
242 }
243 />
244 )
245}
246
247interface SupportFromBodyProps {
248 form: UseFormReturn<SupportFormValues>
249 state: SupportFormState
250 dispatch: Dispatch<SupportFormActions>
251 initialError: string | null
252 selectedProjectRef: string | null
253}
254
255function SupportFormBody({
256 form,
257 state,
258 dispatch,
259 initialError,
260 selectedProjectRef,
261}: SupportFromBodyProps) {
262 const isSuccess = state.type === 'success'
263
264 return (
265 <div
266 className={cn(
267 'min-w-full w-full space-y-12 rounded-sm border bg-panel-body-light shadow-md',
268 `${isSuccess ? 'pt-8' : 'py-8'}`,
269 'border-default'
270 )}
271 >
272 {isSuccess ? (
273 <Success
274 selectedProject={selectedProjectRef ?? undefined}
275 sentCategory={state.sentCategory}
276 />
277 ) : (
278 <SupportFormV2 form={form} initialError={initialError} state={state} dispatch={dispatch} />
279 )}
280 </div>
281 )
282}