ErrorDisplay.tsx90 lines · main
1'use client'
2
3import { HelpCircle } from 'lucide-react'
4import { forwardRef, useEffect, useRef } from 'react'
5import { Card, CardHeader, cn } from 'ui'
6
7import { WarningIcon } from '../admonition'
8import type { ErrorDisplayProps, SupportFormParams } from './ErrorDisplay.types'
9
10export type { SupportFormParams } from './ErrorDisplay.types'
11
12function buildSupportUrl(params?: SupportFormParams) {
13 if (!params) return '/support/new'
14 const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== '')
15 if (entries.length === 0) return '/support/new'
16 return `/support/new?${new URLSearchParams(entries as [string, string][]).toString()}`
17}
18
19export const ErrorDisplay = forwardRef<HTMLDivElement, ErrorDisplayProps>(
20 (
21 {
22 title,
23 errorMessage,
24 supportFormParams,
25 supportLabel = 'Contact support',
26 children,
27 className,
28 icon,
29 onRender,
30 onSupportClick,
31 ...props
32 },
33 ref
34 ) => {
35 const hasFired = useRef(false)
36 useEffect(() => {
37 if (hasFired.current) return
38 hasFired.current = true
39 onRender?.()
40 // eslint-disable-next-line react-hooks/exhaustive-deps
41 }, [])
42
43 const supportUrl = buildSupportUrl(supportFormParams)
44
45 return (
46 <Card
47 ref={ref}
48 className={cn('rounded-lg border border-default', className)}
49 role="alert"
50 aria-labelledby="error-display-title"
51 {...props}
52 >
53 <CardHeader className="flex-row items-center gap-2.5 space-y-0 p-3">
54 <div className="bg-warning p-1 text-background rounded-md">
55 {icon ?? <WarningIcon className="w-3 h-3" />}
56 </div>
57 <h3 id="error-display-title" className="text-sm text-foreground mt-0">
58 {title}
59 </h3>
60 </CardHeader>
61
62 <div className="px-4 py-3 bg-warning-200 border-y border-warning-500">
63 <pre className="text-xs font-mono text-warning-600 whitespace-pre-wrap wrap-break-word overflow-auto max-h-32">
64 {errorMessage}
65 </pre>
66 </div>
67
68 {children && <div>{children}</div>}
69
70 <div className="px-3 py-2 border-t border-default flex items-center gap-2">
71 <div className="shrink-0">
72 <HelpCircle className="h-4 w-4 text-foreground-muted" />
73 </div>
74 <span className="text-sm text-foreground-light">Need help?</span>
75 <a
76 href={supportUrl}
77 target="_blank"
78 rel="noopener noreferrer"
79 className="text-sm text-foreground shrink-0 underline hover:text-foreground-light transition-colors"
80 onClick={onSupportClick}
81 >
82 {supportLabel}
83 </a>
84 </div>
85 </Card>
86 )
87 }
88)
89
90ErrorDisplay.displayName = 'ErrorDisplay'