feat(21-01): add ErrorBoundary, Sonner toast provider, and 404 navigation

- Create ErrorBoundary class component with recovery UI (reload button)
- Create Sonner Toaster wrapper (bottom-right, richColors)
- Wire ErrorBoundary around Outlet in root route to catch render errors
- Add Toaster as sibling to AppLayout in root route
- Update notFoundComponent with Back to Dashboard link button
This commit is contained in:
Lukas May
2026-02-05 08:57:30 +01:00
parent c52c6f170f
commit d323e1ea8e
5 changed files with 96 additions and 6 deletions

11
package-lock.json generated
View File

@@ -5984,6 +5984,16 @@
"url": "https://github.com/steveukx/git-js?sponsor=1"
}
},
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -6725,6 +6735,7 @@
"lucide-react": "^0.563.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {

View File

@@ -22,6 +22,7 @@
"lucide-react": "^0.563.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {

View File

@@ -0,0 +1,54 @@
import { Component } from "react";
import type { ErrorInfo, ReactNode } from "react";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
interface ErrorBoundaryProps {
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
handleReload = () => {
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div className="flex flex-col items-center justify-center gap-4 py-12">
<AlertCircle className="h-8 w-8 text-destructive" />
<h2 className="text-lg font-semibold">Something went wrong</h2>
<p className="max-w-md text-center text-sm text-muted-foreground">
{this.state.error?.message ?? "An unexpected error occurred."}
</p>
<Button variant="outline" size="sm" onClick={this.handleReload}>
Reload
</Button>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,13 @@
import { Toaster as SonnerToaster } from "sonner";
export function Toaster() {
return (
<SonnerToaster
position="bottom-right"
richColors
toastOptions={{
className: "font-sans",
}}
/>
);
}

View File

@@ -1,16 +1,27 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { AppLayout } from '../layouts/AppLayout'
import { ErrorBoundary } from '../components/ErrorBoundary'
import { Toaster } from '../components/ui/sonner'
import { Button } from '../components/ui/button'
export const Route = createRootRoute({
component: () => (
<AppLayout>
<Outlet />
</AppLayout>
<>
<AppLayout>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</AppLayout>
<Toaster />
</>
),
notFoundComponent: () => (
<div className="p-8 text-center">
<div className="flex flex-col items-center justify-center gap-4 py-12">
<h1 className="text-2xl font-bold">Page not found</h1>
<p className="text-muted-foreground mt-2">The page you are looking for does not exist.</p>
<p className="text-muted-foreground">The page you are looking for does not exist.</p>
<Button variant="outline" asChild>
<Link to="/initiatives">Back to Dashboard</Link>
</Button>
</div>
),
})