SupportFormPage.tsx282 lines · main
| 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 | |
| 12 | import { AIAssistantOption } from './AIAssistantOption' |
| 13 | import { DiscordCTACard } from './DiscordCTACard' |
| 14 | import { IncidentAdmonition } from './IncidentAdmonition' |
| 15 | import { Success } from './Success' |
| 16 | import type { ExtendedSupportCategories } from './Support.constants' |
| 17 | import type { SupportFormValues } from './SupportForm.schema' |
| 18 | import { |
| 19 | createInitialSupportFormState, |
| 20 | supportFormReducer, |
| 21 | type SupportFormActions, |
| 22 | type SupportFormState, |
| 23 | } from './SupportForm.state' |
| 24 | import { NO_PROJECT_MARKER } from './SupportForm.utils' |
| 25 | import { SupportFormV2 } from './SupportFormV2' |
| 26 | import { useSupportForm } from './useSupportForm' |
| 27 | import CopyButton from '@/components/ui/CopyButton' |
| 28 | import { useIncidentStatusQuery } from '@/data/platform/incident-status-query' |
| 29 | import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' |
| 30 | import { useStateTransition } from '@/hooks/misc/useStateTransition' |
| 31 | import { BASE_PATH, DOCS_URL } from '@/lib/constants' |
| 32 | |
| 33 | export { SupportForm, SupportFormStatusButton } from './SupportSidebarForm' |
| 34 | |
| 35 | function 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 | |
| 62 | export function SupportFormPage() { |
| 63 | return <SupportFormPageContent /> |
| 64 | } |
| 65 | |
| 66 | function 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 | |
| 122 | function 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 | |
| 132 | function 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 | |
| 192 | interface SupportFormDirectEmailInfoProps { |
| 193 | projectRef: string | null |
| 194 | } |
| 195 | |
| 196 | function 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 | |
| 247 | interface SupportFromBodyProps { |
| 248 | form: UseFormReturn<SupportFormValues> |
| 249 | state: SupportFormState |
| 250 | dispatch: Dispatch<SupportFormActions> |
| 251 | initialError: string | null |
| 252 | selectedProjectRef: string | null |
| 253 | } |
| 254 | |
| 255 | function 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 | } |