Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Previously a JS rendering error produced a blank white screen with no feedback. PageErrorBoundary now catches the error, shows the error message + stack trace, and offers a reload button. This will surface the exact crash message for the inaccessible test page.
94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
import { Outlet } from "react-router-dom";
|
|
import { LogOut, AlertTriangle, RefreshCw } from "lucide-react";
|
|
import { useAuth } from "../context/AuthContext";
|
|
import Sidebar from "./Sidebar";
|
|
import NotificationBell from "./NotificationBell";
|
|
import React from "react";
|
|
|
|
/* ── Error Boundary ──────────────────────────────────────────────────
|
|
Catches any unhandled rendering error and shows a recoverable error
|
|
screen instead of a blank white page.
|
|
─────────────────────────────────────────────────────────────────── */
|
|
interface EBState { hasError: boolean; error: Error | null }
|
|
|
|
class PageErrorBoundary extends React.Component<{ children: React.ReactNode }, EBState> {
|
|
constructor(props: { children: React.ReactNode }) {
|
|
super(props);
|
|
this.state = { hasError: false, error: null };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): EBState {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
console.error("[PageErrorBoundary]", error, info.componentStack);
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
|
<AlertTriangle className="h-12 w-12 text-red-400" />
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-white">Something went wrong</h2>
|
|
<p className="mt-1 text-sm text-gray-400">
|
|
{this.state.error?.message ?? "An unexpected error occurred while rendering this page."}
|
|
</p>
|
|
{this.state.error?.stack && (
|
|
<pre className="mt-3 max-h-40 overflow-y-auto rounded-lg border border-gray-800 bg-gray-900 p-3 text-left text-xs text-gray-500">
|
|
{this.state.error.stack}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
this.setState({ hasError: false, error: null });
|
|
window.location.reload();
|
|
}}
|
|
className="flex items-center gap-2 rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-300 hover:bg-gray-800"
|
|
>
|
|
<RefreshCw className="h-4 w-4" />
|
|
Reload page
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
/* ── Layout ─────────────────────────────────────────────────────────── */
|
|
|
|
export default function Layout() {
|
|
const { user, logout } = useAuth();
|
|
|
|
return (
|
|
<div className="flex h-screen bg-gray-950 text-gray-100">
|
|
<Sidebar />
|
|
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<header className="flex h-16 items-center justify-end gap-4 border-b border-gray-800 bg-gray-900 px-6">
|
|
<NotificationBell />
|
|
<span className="text-sm text-gray-300">{user?.username}</span>
|
|
<button
|
|
onClick={logout}
|
|
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Logout
|
|
</button>
|
|
</header>
|
|
|
|
{/* Main content wrapped in error boundary */}
|
|
<main className="flex-1 overflow-y-auto p-6">
|
|
<PageErrorBoundary>
|
|
<Outlet />
|
|
</PageErrorBoundary>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|