Files
OpenFrame/components/error-boundary.tsx
T
Yusuf İpek 373aab964c feat: Implement robust error/not-found pages & UI components
- Introduce dedicated error pages for dashboard and video routes
- Add specific not-found pages for dashboard, projects, videos, and settings
- Implement global `not-found.tsx` for general unhandled routes
- Integrate root and dashboard layouts with ErrorBoundary and Suspense
- Add new UI components: Accordion, Hover Card, Menubar, Navigation Menu, Select, Tabs
- Update Navbar to utilize the new Navigation Menu component
- Enhance `button` component with a `link` variant for better styling
- Refine existing UI components (dialog, dropdown, input, etc.)
- Update Tailwind config with new colors and animation extensions
2026-02-07 15:53:31 +03:00

140 lines
3.8 KiB
TypeScript

"use client";
import React, { Component, ErrorInfo, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { AlertTriangle, Film } from "lucide-react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
context?: string;
}
interface State {
hasError: boolean;
error: Error | null;
}
/**
* Global Error Boundary component for catching React component errors.
* Use this to wrap client components that might crash, especially the video player.
*
* @example
* ```tsx
* <ErrorBoundary context="VideoPlayer">
* <VideoPlayer {...props} />
* </ErrorBoundary>
* ```
*/
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error(`ErrorBoundary${this.props.context ? ` [${this.props.context}]` : ""} caught an error:`, error, errorInfo);
this.props.onError?.(error, errorInfo);
}
private handleReset = () => {
this.setState({ hasError: false, error: null });
};
private handleReload = () => {
window.location.reload();
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<ErrorFallback
error={this.state.error}
context={this.props.context}
onReset={this.handleReset}
onReload={this.handleReload}
/>
);
}
return this.props.children;
}
}
interface ErrorFallbackProps {
error: Error | null;
context?: string;
onReset: () => void;
onReload: () => void;
}
function ErrorFallback({ error, context, onReset, onReload }: ErrorFallbackProps) {
const isVideoContext = context?.toLowerCase().includes("video");
return (
<div className="flex min-h-[300px] flex-col items-center justify-center gap-4 rounded-lg border border-destructive/20 bg-destructive/5 p-6 text-center">
<div className="relative">
{isVideoContext ? (
<>
<Film className="h-12 w-12 text-muted-foreground" />
<AlertTriangle className="absolute -bottom-1 -right-1 h-6 w-6 text-destructive" />
</>
) : (
<AlertTriangle className="h-12 w-12 text-destructive" />
)}
</div>
<div className="space-y-1">
<h3 className="text-lg font-semibold">
{context ? `${context} crashed` : "Something went wrong"}
</h3>
<p className="text-muted-foreground text-sm max-w-sm">
{isVideoContext
? "The video player encountered an error. Try reloading or go back to the project."
: "An unexpected error occurred. Try resetting the component or reload the page."}
</p>
</div>
{process.env.NODE_ENV === "development" && error?.message && (
<div className="rounded bg-destructive/10 px-3 py-2 text-xs text-destructive font-mono max-w-sm overflow-auto">
{error.message}
</div>
)}
<div className="flex gap-2">
<Button onClick={onReset} variant="default" size="sm">
Try again
</Button>
<Button onClick={onReload} variant="outline" size="sm">
Reload page
</Button>
</div>
</div>
);
}
/**
* HOC to wrap a component with ErrorBoundary
*/
export function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
errorBoundaryProps?: Omit<Props, "children">
) {
return function WithErrorBoundaryWrapper(props: P) {
return (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
);
};
}