SupportFormPage.tsx 9.2 KB

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