mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-15 05:45:20 +00:00
151 lines
No EOL
5.3 KiB
TypeScript
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>
|
|
)
|
|
} |