diff --git a/apps/web/src/app/(app)/error.tsx b/apps/web/src/app/(app)/error.tsx
new file mode 100644
index 0000000..a617ee1
--- /dev/null
+++ b/apps/web/src/app/(app)/error.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect } from "react";
+
+export default function AppError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ console.error("[AppError]", error);
+ }, [error]);
+
+ return (
+
+
+
+ Something went wrong
+
+
+ {error.message || "An unexpected error occurred. The team has been notified."}
+
+ {error.digest && (
+
+ Error ID: {error.digest}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(app)/not-found.tsx b/apps/web/src/app/(app)/not-found.tsx
new file mode 100644
index 0000000..79f9d02
--- /dev/null
+++ b/apps/web/src/app/(app)/not-found.tsx
@@ -0,0 +1,35 @@
+import Link from "next/link";
+
+export default function NotFound() {
+ return (
+
+
+
+ Page not found
+
+
+ The page you are looking for does not exist or has been moved.
+
+
+ Go to dashboard
+
+
+ );
+}
diff --git a/apps/web/src/app/(app)/staffing/page.tsx b/apps/web/src/app/(app)/staffing/page.tsx
index 2ad1a06..d91fedb 100644
--- a/apps/web/src/app/(app)/staffing/page.tsx
+++ b/apps/web/src/app/(app)/staffing/page.tsx
@@ -1,5 +1,10 @@
import { StaffingPanel } from "~/components/staffing/StaffingPanel.js";
+import { ErrorBoundary } from "~/components/ui/ErrorBoundary.js";
export default function StaffingPage() {
- return ;
+ return (
+
+
+
+ );
}
diff --git a/apps/web/src/app/(app)/timeline/page.tsx b/apps/web/src/app/(app)/timeline/page.tsx
index d16dc8c..e5d4d1d 100644
--- a/apps/web/src/app/(app)/timeline/page.tsx
+++ b/apps/web/src/app/(app)/timeline/page.tsx
@@ -1,4 +1,5 @@
import dynamic from "next/dynamic";
+import { ErrorBoundary } from "~/components/ui/ErrorBoundary.js";
const TimelineView = dynamic(
() => import("~/components/timeline/TimelineView.js").then((m) => m.TimelineView),
@@ -22,7 +23,9 @@ export default function TimelinePage() {
Interactive resource planning timeline
-
+
+
+
);
}
diff --git a/apps/web/src/components/ui/ErrorBoundary.tsx b/apps/web/src/components/ui/ErrorBoundary.tsx
new file mode 100644
index 0000000..c1e0314
--- /dev/null
+++ b/apps/web/src/components/ui/ErrorBoundary.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { Component, type ReactNode, type ErrorInfo } from "react";
+
+interface Props {
+ children: ReactNode;
+ fallback?: ReactNode;
+ onError?: (error: Error, info: ErrorInfo) => void;
+}
+
+interface State {
+ error: Error | null;
+}
+
+export class ErrorBoundary extends Component {
+ state: State = { error: null };
+
+ static getDerivedStateFromError(error: Error): State {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ console.error("[ErrorBoundary]", error, info.componentStack);
+ this.props.onError?.(error, info);
+ }
+
+ render() {
+ if (this.state.error) {
+ return (
+ this.props.fallback ?? (
+ this.setState({ error: null })}
+ />
+ )
+ );
+ }
+ return this.props.children;
+ }
+}
+
+export function DefaultErrorFallback({
+ error,
+ reset,
+}: {
+ error: Error;
+ reset: () => void;
+}) {
+ return (
+
+
+
+ Something went wrong
+
+
+ {error.message || "An unexpected error occurred. The team has been notified."}
+
+
+
+
+
+
+ );
+}