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:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
54
packages/web/src/components/ErrorBoundary.tsx
Normal file
54
packages/web/src/components/ErrorBoundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
13
packages/web/src/components/ui/sonner.tsx
Normal file
13
packages/web/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Toaster as SonnerToaster } from "sonner";
|
||||
|
||||
export function Toaster() {
|
||||
return (
|
||||
<SonnerToaster
|
||||
position="bottom-right"
|
||||
richColors
|
||||
toastOptions={{
|
||||
className: "font-sans",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user