feat: Add error boundaries

This commit is contained in:
gpt-engineer-app[bot]
2025-11-03 14:51:39 +00:00
parent 3ee65403ea
commit ee09e3652c
8 changed files with 1682 additions and 56 deletions

View File

@@ -12,6 +12,9 @@ import { LocationAutoDetectProvider } from "@/components/providers/LocationAutoD
import { AnalyticsWrapper } from "@/components/analytics/AnalyticsWrapper";
import { Footer } from "@/components/layout/Footer";
import { PageLoader } from "@/components/loading/PageSkeletons";
import { RouteErrorBoundary } from "@/components/error/RouteErrorBoundary";
import { AdminErrorBoundary } from "@/components/error/AdminErrorBoundary";
import { EntityErrorBoundary } from "@/components/error/EntityErrorBoundary";
// Core routes (eager-loaded for best UX)
import Index from "./pages/Index";
@@ -89,62 +92,225 @@ function AppContent(): React.JSX.Element {
<div className="min-h-screen flex flex-col">
<div className="flex-1">
<Suspense fallback={<PageLoader />}>
<Routes>
{/* Core routes - eager loaded */}
<Route path="/" element={<Index />} />
<Route path="/parks" element={<Parks />} />
<Route path="/rides" element={<Rides />} />
<Route path="/search" element={<Search />} />
<Route path="/auth" element={<Auth />} />
{/* Detail routes - lazy loaded */}
<Route path="/parks/:slug" element={<ParkDetail />} />
<Route path="/parks/:parkSlug/rides" element={<ParkRides />} />
<Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
<Route path="/manufacturers" element={<Manufacturers />} />
<Route path="/manufacturers/:slug" element={<ManufacturerDetail />} />
<Route path="/manufacturers/:manufacturerSlug/rides" element={<ManufacturerRides />} />
<Route path="/manufacturers/:manufacturerSlug/models" element={<ManufacturerModels />} />
<Route path="/manufacturers/:manufacturerSlug/models/:modelSlug" element={<RideModelDetail />} />
<Route path="/manufacturers/:manufacturerSlug/models/:modelSlug/rides" element={<RideModelRides />} />
<Route path="/designers" element={<Designers />} />
<Route path="/designers/:slug" element={<DesignerDetail />} />
<Route path="/designers/:designerSlug/rides" element={<DesignerRides />} />
<Route path="/owners" element={<ParkOwners />} />
<Route path="/owners/:slug" element={<PropertyOwnerDetail />} />
<Route path="/owners/:ownerSlug/parks" element={<OwnerParks />} />
<Route path="/operators" element={<Operators />} />
<Route path="/operators/:slug" element={<OperatorDetail />} />
<Route path="/operators/:operatorSlug/parks" element={<OperatorParks />} />
<Route path="/blog" element={<BlogIndex />} />
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/terms" element={<Terms />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/submission-guidelines" element={<SubmissionGuidelines />} />
<Route path="/contact" element={<Contact />} />
{/* User routes - lazy loaded */}
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/profile" element={<Profile />} />
<Route path="/profile/:username" element={<Profile />} />
<Route path="/settings" element={<UserSettings />} />
{/* Admin routes - lazy loaded */}
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/admin/moderation" element={<AdminModeration />} />
<Route path="/admin/reports" element={<AdminReports />} />
<Route path="/admin/system-log" element={<AdminSystemLog />} />
<Route path="/admin/users" element={<AdminUsers />} />
<Route path="/admin/blog" element={<AdminBlog />} />
<Route path="/admin/settings" element={<AdminSettings />} />
<Route path="/admin/contact" element={<AdminContact />} />
<Route path="/admin/email-settings" element={<AdminEmailSettings />} />
{/* Utility routes - lazy loaded */}
<Route path="/force-logout" element={<ForceLogout />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
<RouteErrorBoundary>
<Routes>
{/* Core routes - eager loaded */}
<Route path="/" element={<Index />} />
<Route path="/parks" element={<Parks />} />
<Route path="/rides" element={<Rides />} />
<Route path="/search" element={<Search />} />
<Route path="/auth" element={<Auth />} />
{/* Detail routes with entity error boundaries */}
<Route
path="/parks/:slug"
element={
<EntityErrorBoundary entityType="park">
<ParkDetail />
</EntityErrorBoundary>
}
/>
<Route
path="/parks/:parkSlug/rides"
element={
<EntityErrorBoundary entityType="park">
<ParkRides />
</EntityErrorBoundary>
}
/>
<Route
path="/parks/:parkSlug/rides/:rideSlug"
element={
<EntityErrorBoundary entityType="ride">
<RideDetail />
</EntityErrorBoundary>
}
/>
<Route path="/manufacturers" element={<Manufacturers />} />
<Route
path="/manufacturers/:slug"
element={
<EntityErrorBoundary entityType="manufacturer">
<ManufacturerDetail />
</EntityErrorBoundary>
}
/>
<Route
path="/manufacturers/:manufacturerSlug/rides"
element={
<EntityErrorBoundary entityType="manufacturer">
<ManufacturerRides />
</EntityErrorBoundary>
}
/>
<Route
path="/manufacturers/:manufacturerSlug/models"
element={
<EntityErrorBoundary entityType="manufacturer">
<ManufacturerModels />
</EntityErrorBoundary>
}
/>
<Route
path="/manufacturers/:manufacturerSlug/models/:modelSlug"
element={
<EntityErrorBoundary entityType="manufacturer">
<RideModelDetail />
</EntityErrorBoundary>
}
/>
<Route
path="/manufacturers/:manufacturerSlug/models/:modelSlug/rides"
element={
<EntityErrorBoundary entityType="manufacturer">
<RideModelRides />
</EntityErrorBoundary>
}
/>
<Route path="/designers" element={<Designers />} />
<Route
path="/designers/:slug"
element={
<EntityErrorBoundary entityType="designer">
<DesignerDetail />
</EntityErrorBoundary>
}
/>
<Route
path="/designers/:designerSlug/rides"
element={
<EntityErrorBoundary entityType="designer">
<DesignerRides />
</EntityErrorBoundary>
}
/>
<Route path="/owners" element={<ParkOwners />} />
<Route
path="/owners/:slug"
element={
<EntityErrorBoundary entityType="owner">
<PropertyOwnerDetail />
</EntityErrorBoundary>
}
/>
<Route
path="/owners/:ownerSlug/parks"
element={
<EntityErrorBoundary entityType="owner">
<OwnerParks />
</EntityErrorBoundary>
}
/>
<Route path="/operators" element={<Operators />} />
<Route
path="/operators/:slug"
element={
<EntityErrorBoundary entityType="operator">
<OperatorDetail />
</EntityErrorBoundary>
}
/>
<Route
path="/operators/:operatorSlug/parks"
element={
<EntityErrorBoundary entityType="operator">
<OperatorParks />
</EntityErrorBoundary>
}
/>
<Route path="/blog" element={<BlogIndex />} />
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/terms" element={<Terms />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/submission-guidelines" element={<SubmissionGuidelines />} />
<Route path="/contact" element={<Contact />} />
{/* User routes - lazy loaded */}
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/profile" element={<Profile />} />
<Route path="/profile/:username" element={<Profile />} />
<Route path="/settings" element={<UserSettings />} />
{/* Admin routes with admin error boundaries */}
<Route
path="/admin"
element={
<AdminErrorBoundary section="Dashboard">
<AdminDashboard />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/moderation"
element={
<AdminErrorBoundary section="Moderation Queue">
<AdminModeration />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/reports"
element={
<AdminErrorBoundary section="Reports">
<AdminReports />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/system-log"
element={
<AdminErrorBoundary section="System Log">
<AdminSystemLog />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/users"
element={
<AdminErrorBoundary section="User Management">
<AdminUsers />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/blog"
element={
<AdminErrorBoundary section="Blog Management">
<AdminBlog />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/settings"
element={
<AdminErrorBoundary section="Settings">
<AdminSettings />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/contact"
element={
<AdminErrorBoundary section="Contact Management">
<AdminContact />
</AdminErrorBoundary>
}
/>
<Route
path="/admin/email-settings"
element={
<AdminErrorBoundary section="Email Settings">
<AdminEmailSettings />
</AdminErrorBoundary>
}
/>
{/* Utility routes - lazy loaded */}
<Route path="/force-logout" element={<ForceLogout />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</RouteErrorBoundary>
</Suspense>
</div>
<Footer />

View File

@@ -0,0 +1,178 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, ArrowLeft, RefreshCw, Shield } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { logger } from '@/lib/logger';
interface AdminErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
section?: string; // e.g., "Moderation", "Users", "Settings"
}
interface AdminErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* Admin Error Boundary Component (P0 #5)
*
* Specialized error boundary for admin sections.
* Prevents admin panel errors from affecting the entire app.
*
* Usage:
* ```tsx
* <AdminErrorBoundary section="User Management">
* <UserManagement />
* </AdminErrorBoundary>
* ```
*/
export class AdminErrorBoundary extends Component<AdminErrorBoundaryProps, AdminErrorBoundaryState> {
constructor(props: AdminErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<AdminErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
logger.error('Admin panel error caught by boundary', {
section: this.props.section || 'unknown',
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
severity: 'high', // Admin errors are high priority
});
this.setState({ errorInfo });
}
handleRetry = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
handleBackToDashboard = () => {
window.location.href = '/admin';
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[500px] flex items-center justify-center p-6">
<Card className="max-w-3xl w-full border-destructive/50 bg-destructive/5">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-destructive/10">
<Shield className="w-6 h-6 text-destructive" />
</div>
<div>
<CardTitle className="text-destructive flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Admin Panel Error
</CardTitle>
<CardDescription className="mt-1">
{this.props.section
? `An error occurred in ${this.props.section}`
: 'An error occurred in the admin panel'}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error Details</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-2">
<p className="text-sm">
{this.state.error?.message || 'An unexpected error occurred in the admin panel'}
</p>
<p className="text-xs text-muted-foreground">
This error has been logged. If the problem persists, please contact support.
</p>
</div>
</AlertDescription>
</Alert>
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="default"
size="sm"
onClick={this.handleRetry}
className="gap-2"
>
<RefreshCw className="w-4 h-4" />
Retry
</Button>
<Button
variant="outline"
size="sm"
onClick={this.handleBackToDashboard}
className="gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back to Dashboard
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
navigator.clipboard.writeText(
JSON.stringify({
section: this.props.section,
error: this.state.error?.message,
stack: this.state.error?.stack,
timestamp: new Date().toISOString(),
url: window.location.href,
}, null, 2)
);
}}
>
Copy Error Report
</Button>
</div>
{import.meta.env.DEV && this.state.errorInfo && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground font-medium">
Show Stack Trace (Development Only)
</summary>
<div className="mt-2 space-y-2">
<pre className="overflow-auto p-3 bg-muted rounded text-xs max-h-[200px]">
{this.state.error?.stack}
</pre>
<pre className="overflow-auto p-3 bg-muted rounded text-xs max-h-[200px]">
{this.state.errorInfo.componentStack}
</pre>
</div>
</details>
)}
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,190 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, ArrowLeft, Home, RefreshCw } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { logger } from '@/lib/logger';
interface EntityErrorBoundaryProps {
children: ReactNode;
entityType: 'park' | 'ride' | 'manufacturer' | 'designer' | 'operator' | 'owner';
entitySlug?: string;
fallback?: ReactNode;
}
interface EntityErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* Entity Error Boundary Component (P0 #5)
*
* Specialized error boundary for entity detail pages.
* Prevents entity rendering errors from crashing the app.
*
* Usage:
* ```tsx
* <EntityErrorBoundary entityType="park" entitySlug={slug}>
* <ParkDetail />
* </EntityErrorBoundary>
* ```
*/
export class EntityErrorBoundary extends Component<EntityErrorBoundaryProps, EntityErrorBoundaryState> {
constructor(props: EntityErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<EntityErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
logger.error('Entity page error caught by boundary', {
entityType: this.props.entityType,
entitySlug: this.props.entitySlug,
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
this.setState({ errorInfo });
}
handleRetry = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
handleBackToList = () => {
const listPages: Record<string, string> = {
park: '/parks',
ride: '/rides',
manufacturer: '/manufacturers',
designer: '/designers',
operator: '/operators',
owner: '/owners',
};
window.location.href = listPages[this.props.entityType] || '/';
};
handleGoHome = () => {
window.location.href = '/';
};
getEntityLabel() {
const labels: Record<string, string> = {
park: 'Park',
ride: 'Ride',
manufacturer: 'Manufacturer',
designer: 'Designer',
operator: 'Operator',
owner: 'Property Owner',
};
return labels[this.props.entityType] || 'Entity';
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
const entityLabel = this.getEntityLabel();
return (
<div className="container mx-auto px-4 py-12">
<Card className="max-w-2xl mx-auto border-destructive/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="w-5 h-5" />
Failed to Load {entityLabel}
</CardTitle>
<CardDescription>
{this.props.entitySlug
? `Unable to display ${entityLabel.toLowerCase()}: ${this.props.entitySlug}`
: `Unable to display this ${entityLabel.toLowerCase()}`}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error Details</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-2">
<p className="text-sm">
{this.state.error?.message || `An unexpected error occurred while loading this ${entityLabel.toLowerCase()}`}
</p>
<p className="text-xs text-muted-foreground">
This might be due to:
</p>
<ul className="text-xs text-muted-foreground list-disc list-inside space-y-1">
<li>The {entityLabel.toLowerCase()} no longer exists</li>
<li>Temporary data loading issues</li>
<li>Network connectivity problems</li>
</ul>
</div>
</AlertDescription>
</Alert>
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="default"
size="sm"
onClick={this.handleRetry}
className="gap-2"
>
<RefreshCw className="w-4 h-4" />
Try Again
</Button>
<Button
variant="outline"
size="sm"
onClick={this.handleBackToList}
className="gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back to {entityLabel}s
</Button>
<Button
variant="ghost"
size="sm"
onClick={this.handleGoHome}
className="gap-2"
>
<Home className="w-4 h-4" />
Home
</Button>
</div>
{import.meta.env.DEV && this.state.errorInfo && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Show Debug Info (Development Only)
</summary>
<pre className="mt-2 overflow-auto p-3 bg-muted rounded text-xs max-h-[300px]">
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,162 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, Home, RefreshCw } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { logger } from '@/lib/logger';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
context?: string; // e.g., "PhotoUpload", "ParkDetail"
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* Generic Error Boundary Component (P0 #5)
*
* Prevents component errors from crashing the entire application.
* Shows user-friendly error UI with recovery options.
*
* Usage:
* ```tsx
* <ErrorBoundary context="PhotoUpload">
* <PhotoUploadForm />
* </ErrorBoundary>
* ```
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Log error with context
logger.error('Component error caught by boundary', {
context: this.props.context || 'unknown',
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo);
}
handleRetry = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[400px] flex items-center justify-center p-4">
<Card className="max-w-2xl w-full border-destructive/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="w-5 h-5" />
Something Went Wrong
</CardTitle>
<CardDescription>
{this.props.context
? `An error occurred in ${this.props.context}`
: 'An unexpected error occurred'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error Details</AlertTitle>
<AlertDescription>
<p className="text-sm mt-2">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
</AlertDescription>
</Alert>
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="default"
size="sm"
onClick={this.handleRetry}
className="gap-2"
>
<RefreshCw className="w-4 h-4" />
Try Again
</Button>
<Button
variant="outline"
size="sm"
onClick={this.handleGoHome}
className="gap-2"
>
<Home className="w-4 h-4" />
Go Home
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
navigator.clipboard.writeText(
JSON.stringify({
context: this.props.context,
error: this.state.error?.message,
stack: this.state.error?.stack,
timestamp: new Date().toISOString(),
}, null, 2)
);
}}
>
Copy Error Details
</Button>
</div>
{import.meta.env.DEV && this.state.errorInfo && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Show Component Stack (Development Only)
</summary>
<pre className="mt-2 overflow-auto p-3 bg-muted rounded text-xs max-h-[300px]">
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,119 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, Home, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { logger } from '@/lib/logger';
interface RouteErrorBoundaryProps {
children: ReactNode;
}
interface RouteErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
/**
* Route Error Boundary Component (P0 #5)
*
* Top-level error boundary that wraps all routes.
* Last line of defense to prevent complete app crashes.
*
* Usage: Wrap Routes component in App.tsx
* ```tsx
* <RouteErrorBoundary>
* <Routes>...</Routes>
* </RouteErrorBoundary>
* ```
*/
export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, RouteErrorBoundaryState> {
constructor(props: RouteErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error: Error): Partial<RouteErrorBoundaryState> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Critical: Route-level error - highest priority logging
logger.error('Route-level error caught by boundary', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
url: window.location.href,
severity: 'critical',
});
}
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
<Card className="max-w-lg w-full shadow-lg">
<CardHeader className="text-center pb-4">
<div className="mx-auto w-16 h-16 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
<AlertTriangle className="w-8 h-8 text-destructive" />
</div>
<CardTitle className="text-2xl">
Something Went Wrong
</CardTitle>
<CardDescription className="mt-2">
We encountered an unexpected error. This has been logged and we'll look into it.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{import.meta.env.DEV && this.state.error && (
<div className="p-3 bg-muted rounded-lg">
<p className="text-xs font-mono text-muted-foreground">
{this.state.error.message}
</p>
</div>
)}
<div className="flex flex-col sm:flex-row gap-2">
<Button
variant="default"
onClick={this.handleReload}
className="flex-1 gap-2"
>
<RefreshCw className="w-4 h-4" />
Reload Page
</Button>
<Button
variant="outline"
onClick={this.handleGoHome}
className="flex-1 gap-2"
>
<Home className="w-4 h-4" />
Go Home
</Button>
</div>
<p className="text-xs text-center text-muted-foreground">
If this problem persists, please contact support
</p>
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,12 @@
/**
* Error Boundary Components (P0 #5 - Critical)
*
* Prevents component errors from crashing the entire application.
* Provides user-friendly error UIs with recovery options.
*/
export { ErrorBoundary } from './ErrorBoundary';
export { AdminErrorBoundary } from './AdminErrorBoundary';
export { EntityErrorBoundary } from './EntityErrorBoundary';
export { RouteErrorBoundary } from './RouteErrorBoundary';
export { ModerationErrorBoundary } from './ModerationErrorBoundary';