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"
|
"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": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -6725,6 +6735,7 @@
|
|||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 { 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({
|
export const Route = createRootRoute({
|
||||||
component: () => (
|
component: () => (
|
||||||
|
<>
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
|
<ErrorBoundary>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</ErrorBoundary>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
<Toaster />
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
notFoundComponent: () => (
|
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>
|
<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>
|
</div>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user