Improve error boundary UI with status-specific error pages (#21)
* Improve error pages with styled, status-aware UI Replace the bare-bones error boundary with a proper error page that: - Renders a minimal Brackt header so users can navigate home - Shows distinct messages and icons for 401, 403, 404, 500+ errors - Displays a "Sign in from the home page" hint on 401 so logged-out users know how to recover - Hides internal details in production (stack traces dev-only) - Uses existing dark theme Tailwind classes for visual consistency https://claude.ai/code/session_011jv8desa5vhSkjZHSjWZiV * Address code review feedback on error pages - Move statusIcon out of ErrorBoundary render as a plain module-level function to avoid React re-creating a component type on every render - Collapse 401 action into a single descriptive button label ("Go to Home Page to Sign In") instead of a fragmented button + hint - Fix details/summary border radius collision by using overflow-hidden on the container and removing rounded-md from the summary element; separate the pre block with a border-t instead https://claude.ai/code/session_011jv8desa5vhSkjZHSjWZiV --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e9798263b5
commit
8a444a51a1
1 changed files with 86 additions and 20 deletions
106
app/root.tsx
106
app/root.tsx
|
|
@ -16,6 +16,7 @@ import { ClerkProvider } from "@clerk/react-router";
|
|||
import { dark } from "@clerk/themes";
|
||||
import { Toaster } from "~/components/ui/sonner";
|
||||
import { isUserAdminByClerkId } from "~/models/user";
|
||||
import { AlertCircle, FileQuestion, Lock, ShieldOff, ServerCrash } from "lucide-react";
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()];
|
||||
|
||||
|
|
@ -83,30 +84,95 @@ export default function App({ loaderData }: Route.ComponentProps) {
|
|||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "Oops!";
|
||||
let details = "An unexpected error occurred.";
|
||||
let status = 500;
|
||||
let title = "Something Went Wrong";
|
||||
let message = "An unexpected error occurred. Please try again later.";
|
||||
let stack: string | undefined;
|
||||
|
||||
let showSignInHint = false;
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details =
|
||||
error.status === 404
|
||||
? "The requested page could not be found."
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
status = error.status;
|
||||
switch (error.status) {
|
||||
case 401:
|
||||
title = "Sign In Required";
|
||||
message = "You need to be signed in to access this page.";
|
||||
showSignInHint = true;
|
||||
break;
|
||||
case 403:
|
||||
title = "Access Denied";
|
||||
message = "You don't have permission to view this page.";
|
||||
break;
|
||||
case 404:
|
||||
title = "Page Not Found";
|
||||
message = "The page you're looking for doesn't exist or has been moved.";
|
||||
break;
|
||||
default:
|
||||
title = `Error ${error.status}`;
|
||||
message =
|
||||
typeof error.data === "string" && error.data
|
||||
? error.data
|
||||
: error.statusText || "Something went wrong.";
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
title = "Unexpected Error";
|
||||
message = import.meta.env.DEV ? error.message : "An unexpected error occurred. Please try again later.";
|
||||
stack = import.meta.env.DEV ? error.stack : undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||
<header className="border-b border-border/40 bg-background/95 backdrop-blur">
|
||||
<div className="flex h-16 items-center px-4 md:px-6 lg:px-8">
|
||||
<a href="/" className="font-bold text-xl hover:text-primary transition-colors">
|
||||
Brackt
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 flex items-center justify-center p-6">
|
||||
<div className="max-w-md w-full text-center space-y-6">
|
||||
<div className="text-8xl font-bold text-muted-foreground/40 select-none">
|
||||
{status}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
{statusIcon(status)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
<p className="text-muted-foreground">{message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center pt-2">
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex items-center justify-center rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 py-2 transition-colors"
|
||||
>
|
||||
{showSignInHint ? "Go to Home Page to Sign In" : "Go Home"}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{stack && (
|
||||
<details className="text-left mt-8 border border-border rounded-md overflow-hidden">
|
||||
<summary className="cursor-pointer text-sm text-muted-foreground px-4 py-3 hover:bg-muted/50">
|
||||
Stack trace (development only)
|
||||
</summary>
|
||||
<pre className="p-4 overflow-x-auto text-xs text-muted-foreground border-t border-border">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusIcon(status: number) {
|
||||
const cls = "h-14 w-14 text-muted-foreground";
|
||||
if (status === 401) return <Lock className={cls} />;
|
||||
if (status === 403) return <ShieldOff className={cls} />;
|
||||
if (status === 404) return <FileQuestion className={cls} />;
|
||||
if (status >= 500) return <ServerCrash className={cls} />;
|
||||
return <AlertCircle className={cls} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue