sourcebot/packages/web/src/app/error.tsx
2025-04-24 22:28:13 -07:00

151 lines
No EOL
5.3 KiB
TypeScript

"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect, useMemo } from 'react'
import { useState } from "react"
import { Copy, CheckCircle2, TriangleAlert } from "lucide-react"
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { serviceErrorSchema } from '@/lib/serviceError';
import { SourcebotLogo } from './components/sourcebotLogo';
import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
export default function Error({ error, reset }: { error: Error & { digest?: string }, reset: () => void }) {
useEffect(() => {
Sentry.captureException(error);
console.error(error);
}, [error]);
const { message, errorCode, statusCode } = useMemo(() => {
try {
const body = JSON.parse(error.message);
const { success, data: serviceError } = serviceErrorSchema.safeParse(body);
if (success) {
return {
message: serviceError.message,
errorCode: serviceError.errorCode,
statusCode: serviceError.statusCode,
}
}
/* eslint-disable no-empty */
} catch {}
return {
message: error.message,
}
}, [error]);
return (
<div className="flex flex-col min-h-screen justify-center items-center bg-backgroundSecondary">
<SourcebotLogo
className="mb-4"
size='large'
/>
<ErrorCard
message={message}
errorCode={errorCode}
statusCode={statusCode}
onReloadButtonClicked={reset}
/>
</div>
)
}
interface ErrorCardProps {
message: string
errorCode?: string | number
statusCode?: string | number
onReloadButtonClicked: () => void
}
function ErrorCard({ message, errorCode, statusCode, onReloadButtonClicked }: ErrorCardProps) {
const [copied, setCopied] = useState<string | null>(null)
const copyToClipboard = (text: string, field: string) => {
navigator.clipboard.writeText(text)
setCopied(field)
setTimeout(() => setCopied(null), 2000)
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader className="space-y-1 flex">
<CardTitle className="text-2xl font-bold flex items-center gap-2 text-destructive">
<TriangleAlert className="h-5 w-5 mt-0.5" />
Unexpected Error
</CardTitle>
<CardDescription className="text-sm">
An unexpected error occurred. Please reload the page and try again. If the issue persists, <Link href={`mailto:${SOURCEBOT_SUPPORT_EMAIL}?subject=Sourcebot%20Error%20Report${errorCode ? `%20|%20Code:%20${errorCode}` : ''}`} className='underline'>please contact us</Link>.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<ErrorField
label="Error Message"
value={message}
onCopy={() => copyToClipboard(message, "message")}
copied={copied === "message"}
/>
{errorCode && (
<ErrorField
label="Error Code"
value={errorCode}
onCopy={() => copyToClipboard(errorCode.toString(), "errorCode")}
copied={copied === "errorCode"}
/>
)}
{statusCode && (
<ErrorField
label="Status Code"
value={statusCode}
onCopy={() => copyToClipboard(statusCode.toString(), "statusCode")}
copied={copied === "statusCode"}
/>
)}
</div>
<Button
onClick={onReloadButtonClicked}
variant='outline'
className='w-full'
>
Reload Page
</Button>
</CardContent>
</Card>
)
}
interface ErrorFieldProps {
label: string
value: string | number
onCopy: () => void
copied: boolean
}
function ErrorField({ label, value, onCopy, copied }: ErrorFieldProps) {
return (
<div className="space-y-2">
<div className="text-sm font-medium">{label}</div>
<div className="flex items-center gap-2">
<div className="bg-muted p-2 rounded text-sm flex-1 break-words">{value}</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0"
onClick={onCopy}
aria-label={`Copy ${label.toLowerCase()}`}
>
{copied ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
)
}