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 { dark } from "@clerk/themes";
|
||||||
import { Toaster } from "~/components/ui/sonner";
|
import { Toaster } from "~/components/ui/sonner";
|
||||||
import { isUserAdminByClerkId } from "~/models/user";
|
import { isUserAdminByClerkId } from "~/models/user";
|
||||||
|
import { AlertCircle, FileQuestion, Lock, ShieldOff, ServerCrash } from "lucide-react";
|
||||||
|
|
||||||
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()];
|
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()];
|
||||||
|
|
||||||
|
|
@ -83,30 +84,95 @@ export default function App({ loaderData }: Route.ComponentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
let message = "Oops!";
|
let status = 500;
|
||||||
let details = "An unexpected error occurred.";
|
let title = "Something Went Wrong";
|
||||||
|
let message = "An unexpected error occurred. Please try again later.";
|
||||||
let stack: string | undefined;
|
let stack: string | undefined;
|
||||||
|
let showSignInHint = false;
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
message = error.status === 404 ? "404" : "Error";
|
status = error.status;
|
||||||
details =
|
switch (error.status) {
|
||||||
error.status === 404
|
case 401:
|
||||||
? "The requested page could not be found."
|
title = "Sign In Required";
|
||||||
: error.statusText || details;
|
message = "You need to be signed in to access this page.";
|
||||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
showSignInHint = true;
|
||||||
details = error.message;
|
break;
|
||||||
stack = error.stack;
|
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 (
|
return (
|
||||||
<main className="pt-16 p-4 container mx-auto">
|
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||||
<h1>{message}</h1>
|
<header className="border-b border-border/40 bg-background/95 backdrop-blur">
|
||||||
<p>{details}</p>
|
<div className="flex h-16 items-center px-4 md:px-6 lg:px-8">
|
||||||
{stack && (
|
<a href="/" className="font-bold text-xl hover:text-primary transition-colors">
|
||||||
<pre className="w-full p-4 overflow-x-auto">
|
Brackt
|
||||||
<code>{stack}</code>
|
</a>
|
||||||
</pre>
|
</div>
|
||||||
)}
|
</header>
|
||||||
</main>
|
|
||||||
|
<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