Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 109 additions & 79 deletions wata-board-frontend/src/components/OfflineErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,142 @@
import React, { Component, ReactNode } from 'react';
import { shouldShowOfflineUI, getOfflineErrorMessage } from '../utils/offlineApi';
import { Component, type ErrorInfo, type ReactNode } from 'react';
import { getOfflineErrorMessage, shouldShowOfflineUI } from '../utils/offlineApi';

interface OfflineErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: any) => void;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
onRetry?: () => void;
}

interface OfflineErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: any;
errorInfo?: ErrorInfo;
isOnline: boolean;
}

export class OfflineErrorBoundary extends Component<OfflineErrorBoundaryProps, OfflineErrorBoundaryState> {
constructor(props: OfflineErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
this.state = {
hasError: false,
isOnline: typeof navigator === 'undefined' ? true : navigator.onLine,
};
}

static getDerivedStateFromError(error: Error): OfflineErrorBoundaryState {
static getDerivedStateFromError(error: Error): Partial<OfflineErrorBoundaryState> {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: any) {
this.setState({ errorInfo });

// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
componentDidMount() {
window.addEventListener('online', this.handleOnline);
window.addEventListener('offline', this.handleOffline);
}

// Log error for debugging
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo);
console.error('[OfflineErrorBoundary] Error caught:', error, errorInfo);
}

handleRetry = () => {
componentWillUnmount() {
window.removeEventListener('online', this.handleOnline);
window.removeEventListener('offline', this.handleOffline);
}

private handleOnline = () => {
this.setState({ isOnline: true });

if (this.state.hasError && this.state.error && shouldShowOfflineUI(this.state.error)) {
this.handleRetry();
}
};

private handleOffline = () => {
this.setState({ isOnline: false });
};

private handleRetry = () => {
this.props.onRetry?.();
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
};

render() {
if (this.state.hasError) {
const { error } = this.state;
const isOfflineError = error ? shouldShowOfflineUI(error) : false;
const errorMessage = error ? getOfflineErrorMessage(error) : 'An unexpected error occurred';

// Use custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}

// Default error UI
return (
<div className="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
<div className="max-w-md w-full bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl">
<div className="text-center">
<div className="text-4xl mb-4">
{isOfflineError ? '📱' : '⚠️'}
</div>

<h1 className="text-xl font-semibold mb-2">
{isOfflineError ? 'Connection Issue' : 'Something went wrong'}
</h1>

<p className="text-slate-300 mb-6 text-sm">
{errorMessage}
if (!this.state.hasError) {
return this.props.children;
}

const error = this.state.error;
const isOfflineError = error ? shouldShowOfflineUI(error) : false;

if (!isOfflineError && error) {
throw error;
}

if (this.props.fallback) {
return this.props.fallback;
}

const errorMessage = error ? getOfflineErrorMessage(error) : 'Connection interrupted. Please try again.';

return (
<div className="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
<section
className="max-w-md w-full bg-slate-900 border border-slate-800 rounded-2xl p-6 shadow-xl"
aria-labelledby="offline-error-title"
role="alert"
>
<div className="text-center">
<div className="text-4xl mb-4" aria-hidden="true">
!
</div>

<h1 id="offline-error-title" className="text-xl font-semibold mb-2">
Connection Issue
</h1>

<p className="text-slate-300 mb-6 text-sm">{errorMessage}</p>

<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 mb-6">
<p className="text-amber-300 text-xs">
{this.state.isOnline
? 'Your connection is back. Retry to continue where you left off.'
: "You're offline. Actions that support offline sync will be saved and completed when you reconnect."}
</p>
</div>

<div className="space-y-3">
<button
type="button"
onClick={this.handleRetry}
className="w-full bg-sky-500 hover:bg-sky-400 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Try Again
</button>

{isOfflineError && (
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 mb-6">
<p className="text-amber-300 text-xs">
Your actions will be saved and completed automatically when you're back online.
</p>
</div>
)}

<div className="space-y-3">
<button
onClick={this.handleRetry}
className="w-full bg-sky-500 hover:bg-sky-400 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Try Again
</button>

<button
onClick={() => window.location.reload()}
className="w-full bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Refresh Page
</button>
</div>

{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-6 text-start">
<summary className="text-xs text-slate-400 cursor-pointer hover:text-slate-300">
Error Details (Development)
</summary>
<pre className="mt-2 text-xs text-red-400 bg-slate-950 p-2 rounded overflow-auto">
{this.state.error.stack}
</pre>
</details>
)}
<button
type="button"
onClick={() => window.location.reload()}
className="w-full bg-slate-700 hover:bg-slate-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
Refresh Page
</button>
</div>
</div>
</div>
);
}

return this.props.children;
{import.meta.env.DEV && error && (
<details className="mt-6 text-left">
<summary className="text-xs text-slate-400 cursor-pointer hover:text-slate-300">
Error Details
</summary>
<pre className="mt-2 text-xs text-red-400 bg-slate-950 p-2 rounded overflow-auto">
{error.stack}
{this.state.errorInfo?.componentStack}
</pre>
</details>
)}
</div>
</section>
</div>
);
}
}
Loading