2026-01-15 11:31:09 +01:00

167 lines
4.8 KiB
TypeScript

import type { ReactNode } from 'react'
import { Card } from './Card'
import { Button } from './Button'
import { t } from '@/lib/i18n'
import { classifyError, type ErrorClassification } from '@/lib/errorClassification'
interface ErrorStateProps {
message: string
action?: ReactNode
className?: string
onRetry?: () => void
onCheckConnection?: () => void
showDocumentationLink?: boolean
error?: unknown
}
function ErrorIcon(): React.ReactElement {
return (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)
}
function getUserFriendlyMessage(classification: ErrorClassification): string {
return classification.message
}
function getErrorClassification(error: unknown | undefined): ErrorClassification | null {
if (error === undefined) {
return null
}
return classifyError(error)
}
function RetryButton({ onRetry }: { onRetry: () => void }): React.ReactElement {
return (
<Button type="button" variant="primary" size="small" onClick={onRetry}>
{t('errors.actions.retry')}
</Button>
)
}
function CheckConnectionButton({ onCheckConnection }: { onCheckConnection: () => void }): React.ReactElement {
return (
<Button type="button" variant="secondary" size="small" onClick={onCheckConnection}>
{t('errors.actions.checkConnection')}
</Button>
)
}
function DocumentationButton(): React.ReactElement {
return (
<Button
type="button"
variant="ghost"
size="small"
onClick={() => {
window.open('/docs', '_blank')
}}
>
{t('errors.actions.viewDocumentation')}
</Button>
)
}
function computeActionFlags(
classification: ErrorClassification | null,
onRetry: (() => void) | undefined,
onCheckConnection: (() => void) | undefined,
showDocumentationLink: boolean
): { showRetry: boolean; showCheck: boolean; showDoc: boolean } {
const canRetry = classification?.canRetry ?? false
const canCheckConnection = classification?.canCheckConnection ?? false
const needsDocumentation = classification?.needsDocumentation ?? false
return {
showRetry: canRetry && onRetry !== undefined,
showCheck: canCheckConnection && onCheckConnection !== undefined,
showDoc: needsDocumentation && showDocumentationLink,
}
}
function hasAnyAction(
action: ReactNode | undefined,
showRetry: boolean,
showCheck: boolean,
showDoc: boolean
): boolean {
return action !== undefined || showRetry || showCheck || showDoc
}
function ErrorActions({
action,
classification,
onRetry,
onCheckConnection,
showDocumentationLink,
}: {
action?: ReactNode
classification: ErrorClassification | null
onRetry?: () => void
onCheckConnection?: () => void
showDocumentationLink?: boolean
}): React.ReactElement | null {
const flags = computeActionFlags(classification, onRetry, onCheckConnection, showDocumentationLink ?? false)
if (!hasAnyAction(action, flags.showRetry, flags.showCheck, flags.showDoc)) {
return null
}
return (
<div className="mt-3 flex flex-wrap gap-2">
{action !== undefined && action}
{flags.showRetry && onRetry !== undefined && <RetryButton onRetry={onRetry} />}
{flags.showCheck && onCheckConnection !== undefined && <CheckConnectionButton onCheckConnection={onCheckConnection} />}
{flags.showDoc && <DocumentationButton />}
</div>
)
}
export function ErrorState({
message,
action,
className = '',
onRetry,
onCheckConnection,
showDocumentationLink = false,
error,
}: ErrorStateProps): React.ReactElement {
const classification = getErrorClassification(error)
const displayMessage = classification !== null ? getUserFriendlyMessage(classification) : message
const suggestion = classification?.suggestion !== undefined ? t(classification.suggestion) : undefined
return (
<Card variant="default" className={`bg-red-900/20 border-red-500/50 ${className}`} role="alert">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 text-red-400">
<ErrorIcon />
</div>
<div className="flex-1">
<p className="text-sm text-red-400 font-medium mb-2">{displayMessage}</p>
{suggestion !== undefined && <p className="text-xs text-red-400/70 mb-3">{suggestion}</p>}
<ErrorActions
action={action}
classification={classification}
{...(onRetry !== undefined ? { onRetry } : {})}
{...(onCheckConnection !== undefined ? { onCheckConnection } : {})}
showDocumentationLink={showDocumentationLink}
/>
</div>
</div>
</Card>
)
}