mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 15:11:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
187
src-old/components/error/AdminErrorBoundary.tsx
Normal file
187
src-old/components/error/AdminErrorBoundary.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
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 { handleError } from '@/lib/errorHandler';
|
||||
|
||||
interface AdminErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
section?: string; // e.g., "Moderation", "Users", "Settings"
|
||||
}
|
||||
|
||||
type ErrorWithId = Error & { errorId: string };
|
||||
|
||||
interface AdminErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: ErrorWithId | 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: error as ErrorWithId,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
// Log to database and get error ID for user reference
|
||||
const errorId = handleError(error, {
|
||||
action: `Admin panel error in ${this.props.section || 'unknown section'}`,
|
||||
metadata: {
|
||||
section: this.props.section,
|
||||
componentStack: errorInfo.componentStack,
|
||||
severity: 'high',
|
||||
},
|
||||
});
|
||||
|
||||
this.setState({ errorInfo, error: { ...error, errorId } as ErrorWithId });
|
||||
}
|
||||
|
||||
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>
|
||||
{(this.state.error as any)?.errorId && (
|
||||
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||
Reference ID: {((this.state.error as any).errorId as string).slice(0, 8)}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
186
src-old/components/error/EntityErrorBoundary.tsx
Normal file
186
src-old/components/error/EntityErrorBoundary.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
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 { handleError } from '@/lib/errorHandler';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
type ErrorWithId = Error & { errorId: string };
|
||||
|
||||
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) {
|
||||
// Log to database and get error ID for user reference
|
||||
const errorId = handleError(error, {
|
||||
action: `${this.props.entityType} page error`,
|
||||
metadata: {
|
||||
entityType: this.props.entityType,
|
||||
entitySlug: this.props.entitySlug,
|
||||
componentStack: errorInfo.componentStack,
|
||||
},
|
||||
});
|
||||
|
||||
this.setState({ errorInfo, error: { ...error, errorId } as ErrorWithId });
|
||||
}
|
||||
|
||||
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>
|
||||
{(this.state.error as ErrorWithId)?.errorId && (
|
||||
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||
Reference ID: {(this.state.error as ErrorWithId).errorId.slice(0, 8)}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
157
src-old/components/error/ErrorBoundary.tsx
Normal file
157
src-old/components/error/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
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 { handleError } from '@/lib/errorHandler';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
type ErrorWithId = Error & { errorId: string };
|
||||
|
||||
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 to database and get error ID for user reference
|
||||
const errorId = handleError(error, {
|
||||
action: `Component error in ${this.props.context || 'unknown context'}`,
|
||||
metadata: {
|
||||
context: this.props.context,
|
||||
componentStack: errorInfo.componentStack,
|
||||
},
|
||||
});
|
||||
|
||||
this.setState({ errorInfo, error: { ...error, errorId } as ErrorWithId });
|
||||
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>
|
||||
{(this.state.error as ErrorWithId)?.errorId && (
|
||||
<p className="text-xs mt-2 font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||
Reference ID: {(this.state.error as ErrorWithId).errorId.slice(0, 8)}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
167
src-old/components/error/ModerationErrorBoundary.tsx
Normal file
167
src-old/components/error/ModerationErrorBoundary.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
|
||||
interface ModerationErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
submissionId?: string;
|
||||
fallback?: ReactNode;
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface ModerationErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
type ErrorWithId = Error & { errorId: string };
|
||||
|
||||
/**
|
||||
* Error Boundary for Moderation Queue Components
|
||||
*
|
||||
* Prevents individual queue item render errors from crashing the entire queue.
|
||||
* Shows user-friendly error UI with retry functionality.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <ModerationErrorBoundary submissionId={item.id}>
|
||||
* <QueueItem item={item} />
|
||||
* </ModerationErrorBoundary>
|
||||
* ```
|
||||
*/
|
||||
export class ModerationErrorBoundary extends Component<
|
||||
ModerationErrorBoundaryProps,
|
||||
ModerationErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ModerationErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ModerationErrorBoundaryState> {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
// Log to database and get error ID for user reference
|
||||
const errorId = handleError(error, {
|
||||
action: 'Moderation queue item render error',
|
||||
metadata: {
|
||||
submissionId: this.props.submissionId,
|
||||
componentStack: errorInfo.componentStack,
|
||||
},
|
||||
});
|
||||
|
||||
// Update state with error info
|
||||
this.setState({
|
||||
error: { ...error, errorId } as ErrorWithId,
|
||||
errorInfo,
|
||||
});
|
||||
|
||||
// Call optional error handler
|
||||
this.props.onError?.(error, errorInfo);
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Custom fallback if provided
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
// Default error UI
|
||||
return (
|
||||
<Card className="border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/10">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-red-700 dark:text-red-300">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
Queue Item Error
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Failed to render submission</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-sm">
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
{(this.state.error as ErrorWithId)?.errorId && (
|
||||
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||
Reference ID: {(this.state.error as ErrorWithId).errorId.slice(0, 8)}
|
||||
</p>
|
||||
)}
|
||||
{this.props.submissionId && (
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
Submission ID: {this.props.submissionId}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={this.handleRetry}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Retry
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
error: this.state.error?.message,
|
||||
stack: this.state.error?.stack,
|
||||
submissionId: this.props.submissionId,
|
||||
}, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy Error Details
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
Show Component Stack
|
||||
</summary>
|
||||
<pre className="mt-2 overflow-auto p-2 bg-muted rounded text-xs">
|
||||
{this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
139
src-old/components/error/NetworkErrorBanner.tsx
Normal file
139
src-old/components/error/NetworkErrorBanner.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WifiOff, RefreshCw, X, Eye } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface NetworkErrorBannerProps {
|
||||
isOffline: boolean;
|
||||
pendingCount?: number;
|
||||
onRetryNow?: () => Promise<void>;
|
||||
onViewQueue?: () => void;
|
||||
estimatedRetryTime?: Date;
|
||||
}
|
||||
|
||||
export function NetworkErrorBanner({
|
||||
isOffline,
|
||||
pendingCount = 0,
|
||||
onRetryNow,
|
||||
onViewQueue,
|
||||
estimatedRetryTime,
|
||||
}: NetworkErrorBannerProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isRetrying, setIsRetrying] = useState(false);
|
||||
const [countdown, setCountdown] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(isOffline || pendingCount > 0);
|
||||
}, [isOffline, pendingCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!estimatedRetryTime) {
|
||||
setCountdown(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, estimatedRetryTime.getTime() - now);
|
||||
setCountdown(Math.ceil(remaining / 1000));
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval);
|
||||
setCountdown(null);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [estimatedRetryTime]);
|
||||
|
||||
const handleRetryNow = async () => {
|
||||
if (!onRetryNow) return;
|
||||
|
||||
setIsRetrying(true);
|
||||
try {
|
||||
await onRetryNow();
|
||||
} finally {
|
||||
setIsRetrying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-0 left-0 right-0 z-50 transition-transform duration-300",
|
||||
isVisible ? "translate-y-0" : "-translate-y-full"
|
||||
)}
|
||||
>
|
||||
<div className="bg-destructive/90 backdrop-blur-sm text-destructive-foreground shadow-lg">
|
||||
<div className="container mx-auto px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<WifiOff className="h-5 w-5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-sm">
|
||||
{isOffline ? 'You are offline' : 'Network Issue Detected'}
|
||||
</p>
|
||||
<p className="text-xs opacity-90 truncate">
|
||||
{pendingCount > 0 ? (
|
||||
<>
|
||||
{pendingCount} submission{pendingCount !== 1 ? 's' : ''} pending
|
||||
{countdown !== null && countdown > 0 && (
|
||||
<span className="ml-2">
|
||||
· Retrying in {countdown}s
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
'Changes will sync when connection is restored'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{pendingCount > 0 && onViewQueue && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={onViewQueue}
|
||||
className="h-8 text-xs bg-background/20 hover:bg-background/30"
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5 mr-1.5" />
|
||||
View Queue ({pendingCount})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onRetryNow && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={handleRetryNow}
|
||||
disabled={isRetrying}
|
||||
className="h-8 text-xs bg-background/20 hover:bg-background/30"
|
||||
>
|
||||
<RefreshCw className={cn(
|
||||
"h-3.5 w-3.5 mr-1.5",
|
||||
isRetrying && "animate-spin"
|
||||
)} />
|
||||
{isRetrying ? 'Retrying...' : 'Retry Now'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="h-8 w-8 p-0 hover:bg-background/20"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Dismiss</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
src-old/components/error/RouteErrorBoundary.tsx
Normal file
196
src-old/components/error/RouteErrorBoundary.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
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 { handleError } from '@/lib/errorHandler';
|
||||
|
||||
interface RouteErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface RouteErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
type ErrorWithId = Error & { errorId: string };
|
||||
|
||||
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) {
|
||||
// Detect chunk load failures (deployment cache issue)
|
||||
const isChunkLoadError =
|
||||
error.message.includes('Failed to fetch dynamically imported module') ||
|
||||
error.message.includes('Loading chunk') ||
|
||||
error.message.includes('ChunkLoadError');
|
||||
|
||||
if (isChunkLoadError) {
|
||||
// Check if we've already tried reloading
|
||||
const hasReloaded = sessionStorage.getItem('chunk-load-reload');
|
||||
|
||||
if (!hasReloaded) {
|
||||
// Mark as reloaded and reload once
|
||||
sessionStorage.setItem('chunk-load-reload', 'true');
|
||||
window.location.reload();
|
||||
return; // Don't log error yet
|
||||
} else {
|
||||
// Second failure - clear flag and show error
|
||||
sessionStorage.removeItem('chunk-load-reload');
|
||||
}
|
||||
}
|
||||
|
||||
// Log to database and get error ID for user reference
|
||||
const errorId = handleError(error, {
|
||||
action: 'Route-level component crash',
|
||||
metadata: {
|
||||
componentStack: errorInfo.componentStack,
|
||||
url: window.location.href,
|
||||
severity: isChunkLoadError ? 'medium' : 'critical',
|
||||
isChunkLoadError,
|
||||
},
|
||||
});
|
||||
|
||||
this.setState({ error: { ...error, errorId } as ErrorWithId });
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleClearCacheAndReload = async () => {
|
||||
try {
|
||||
// Clear all caches
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
||||
}
|
||||
|
||||
// Unregister service workers
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map(reg => reg.unregister()));
|
||||
}
|
||||
|
||||
// Clear session storage chunk reload flag
|
||||
sessionStorage.removeItem('chunk-load-reload');
|
||||
|
||||
// Force reload bypassing cache
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
// Fallback to regular reload if cache clearing fails
|
||||
console.error('Failed to clear cache:', error);
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
handleGoHome = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
const isChunkError =
|
||||
this.state.error?.message.includes('Failed to fetch dynamically imported module') ||
|
||||
this.state.error?.message.includes('Loading chunk') ||
|
||||
this.state.error?.message.includes('ChunkLoadError');
|
||||
|
||||
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">
|
||||
{isChunkError ? 'App Update Required' : 'Something Went Wrong'}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2 space-y-2">
|
||||
{isChunkError ? (
|
||||
<>
|
||||
<p>The app has been updated with new features and improvements.</p>
|
||||
<p className="text-sm font-medium">
|
||||
To continue, please clear your browser cache and reload:
|
||||
</p>
|
||||
<ul className="text-sm list-disc list-inside space-y-1 ml-2">
|
||||
<li>Click "Clear Cache & Reload" below, or</li>
|
||||
<li>Press <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Ctrl+Shift+R</kbd> (Windows/Linux) or <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">⌘+Shift+R</kbd> (Mac)</li>
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
"We encountered an unexpected error. This has been logged and we'll look into it."
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{this.state.error && (
|
||||
<div className="p-3 bg-muted rounded-lg space-y-2">
|
||||
{import.meta.env.DEV && (
|
||||
<p className="text-xs font-mono text-muted-foreground">
|
||||
{this.state.error.message}
|
||||
</p>
|
||||
)}
|
||||
{(this.state.error as ErrorWithId)?.errorId && (
|
||||
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
|
||||
Reference ID: {(this.state.error as ErrorWithId).errorId.slice(0, 8)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{isChunkError && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={this.handleClearCacheAndReload}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Clear Cache & Reload
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant={isChunkError ? "outline" : "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>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
43
src-old/components/error/SubmissionErrorBoundary.tsx
Normal file
43
src-old/components/error/SubmissionErrorBoundary.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { ModerationErrorBoundary } from './ModerationErrorBoundary';
|
||||
|
||||
interface SubmissionErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
submissionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight Error Boundary for Submission-Related Components
|
||||
*
|
||||
* Wraps ModerationErrorBoundary with a submission-specific fallback UI.
|
||||
* Use this for any component that displays submission data.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <SubmissionErrorBoundary submissionId={id}>
|
||||
* <SubmissionDetails />
|
||||
* </SubmissionErrorBoundary>
|
||||
* ```
|
||||
*/
|
||||
export function SubmissionErrorBoundary({
|
||||
children,
|
||||
submissionId
|
||||
}: SubmissionErrorBoundaryProps) {
|
||||
return (
|
||||
<ModerationErrorBoundary
|
||||
submissionId={submissionId}
|
||||
fallback={
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load submission data. Please try refreshing the page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ModerationErrorBoundary>
|
||||
);
|
||||
}
|
||||
13
src-old/components/error/index.ts
Normal file
13
src-old/components/error/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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';
|
||||
export { SubmissionErrorBoundary } from './SubmissionErrorBoundary';
|
||||
Reference in New Issue
Block a user