|
1 | 1 | import { AppShell } from "@/components/layout/app-shell"; |
2 | 2 | import { useAppStore } from "@/stores/app-store"; |
3 | | -import { lazy, Suspense } from "react"; |
| 3 | +import { Component, lazy, Suspense, type ReactNode, type ErrorInfo } from "react"; |
4 | 4 |
|
5 | 5 | const DashboardView = lazy( |
6 | 6 | () => import("@/components/views/dashboard/dashboard-view"), |
@@ -28,21 +28,63 @@ const views = { |
28 | 28 |
|
29 | 29 | function Loading() { |
30 | 30 | return ( |
31 | | - <div className="flex h-full items-center justify-center"> |
| 31 | + <div className="flex h-full items-center justify-center" role="status"> |
32 | 32 | <div className="h-6 w-6 animate-spin rounded-full border-2 border-border border-t-primary" /> |
| 33 | + <span className="sr-only">Loading...</span> |
33 | 34 | </div> |
34 | 35 | ); |
35 | 36 | } |
36 | 37 |
|
| 38 | +interface ErrorBoundaryState { |
| 39 | + error: Error | null; |
| 40 | +} |
| 41 | + |
| 42 | +class ErrorBoundary extends Component<{ children: ReactNode }, ErrorBoundaryState> { |
| 43 | + state: ErrorBoundaryState = { error: null }; |
| 44 | + |
| 45 | + static getDerivedStateFromError(error: Error) { |
| 46 | + return { error }; |
| 47 | + } |
| 48 | + |
| 49 | + componentDidCatch(error: Error, info: ErrorInfo) { |
| 50 | + console.error("View crashed:", error, info.componentStack); |
| 51 | + } |
| 52 | + |
| 53 | + render() { |
| 54 | + if (this.state.error) { |
| 55 | + return ( |
| 56 | + <div className="flex h-full flex-col items-center justify-center gap-3 p-8 text-center"> |
| 57 | + <h2 className="text-lg font-semibold text-text-1">Something went wrong</h2> |
| 58 | + <p className="max-w-sm text-sm text-text-3"> |
| 59 | + The view encountered an error. Try refreshing or switching to a different view. |
| 60 | + </p> |
| 61 | + <button |
| 62 | + onClick={() => { |
| 63 | + this.setState({ error: null }); |
| 64 | + useAppStore.getState().setView("dashboard"); |
| 65 | + }} |
| 66 | + className="mt-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary/90" |
| 67 | + > |
| 68 | + Back to Dashboard |
| 69 | + </button> |
| 70 | + </div> |
| 71 | + ); |
| 72 | + } |
| 73 | + return this.props.children; |
| 74 | + } |
| 75 | +} |
| 76 | + |
37 | 77 | export function App() { |
38 | 78 | const view = useAppStore((s) => s.view); |
39 | 79 | const View = views[view]; |
40 | 80 |
|
41 | 81 | return ( |
42 | 82 | <AppShell> |
43 | | - <Suspense fallback={<Loading />}> |
44 | | - <View /> |
45 | | - </Suspense> |
| 83 | + <ErrorBoundary> |
| 84 | + <Suspense fallback={<Loading />}> |
| 85 | + <View /> |
| 86 | + </Suspense> |
| 87 | + </ErrorBoundary> |
46 | 88 | </AppShell> |
47 | 89 | ); |
48 | 90 | } |
0 commit comments