mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Merge branch 'main' into codex/1184-time-unit-error
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
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): void {
|
||||
// Error is logged to console by React in development
|
||||
// In production, consider logging to an error tracking service
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
// Only show error details in development mode to avoid leaking implementation details
|
||||
const isDev = import.meta.env.DEV
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<h1 style={styles.heading}>Something went wrong</h1>
|
||||
<p style={styles.message}>The application encountered an unexpected error.</p>
|
||||
<button style={styles.button} onClick={() => window.location.reload()}>
|
||||
Reload Application
|
||||
</button>
|
||||
{isDev && this.state.error && (
|
||||
<details style={styles.details}>
|
||||
<summary style={styles.summary}>Error Details</summary>
|
||||
<pre style={styles.pre}>{this.state.error.toString()}</pre>
|
||||
<p style={styles.stack}>{this.state.error.stack}</p>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
padding: '2rem',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
heading: {
|
||||
fontSize: '2rem',
|
||||
color: '#d32f2f',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
message: {
|
||||
fontSize: '1.2rem',
|
||||
color: '#555',
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
button: {
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
details: {
|
||||
marginTop: '2rem',
|
||||
textAlign: 'left' as const,
|
||||
maxWidth: '800px',
|
||||
},
|
||||
summary: {
|
||||
cursor: 'pointer',
|
||||
color: '#1976d2',
|
||||
fontWeight: 'bold' as const,
|
||||
},
|
||||
pre: {
|
||||
whiteSpace: 'pre-wrap' as const,
|
||||
wordWrap: 'break-word' as const,
|
||||
backgroundColor: '#fff',
|
||||
padding: '1rem',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd',
|
||||
},
|
||||
stack: {
|
||||
whiteSpace: 'pre-wrap' as const,
|
||||
wordWrap: 'break-word' as const,
|
||||
fontSize: '0.875rem',
|
||||
color: '#757575',
|
||||
marginTop: '1rem',
|
||||
},
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useRouteError } from 'react-router-dom'
|
||||
|
||||
import { ENV } from '@/util/config'
|
||||
import { getRouterErrorMessage } from './RouterErrorMessages'
|
||||
|
||||
export const RouterError = () => {
|
||||
const error = useRouteError()
|
||||
const errorMessage = getRouterErrorMessage(error, ENV === 'dev')
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<h1 style={styles.heading}>Navigation Error</h1>
|
||||
<p style={styles.message}>The page could not be loaded.</p>
|
||||
<p style={styles.errorMessage}>{errorMessage}</p>
|
||||
<button style={styles.button} onClick={() => window.location.reload()}>
|
||||
Reload Application
|
||||
</button>
|
||||
<button style={styles.secondaryButton} onClick={() => window.history.back()}>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
padding: '2rem',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
heading: {
|
||||
fontSize: '2rem',
|
||||
color: '#d32f2f',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
message: {
|
||||
fontSize: '1.2rem',
|
||||
color: '#555',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
errorMessage: {
|
||||
fontSize: '1rem',
|
||||
color: '#757575',
|
||||
marginBottom: '2rem',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
button: {
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
margin: '0.5rem',
|
||||
},
|
||||
secondaryButton: {
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#1976d2',
|
||||
border: '1px solid #1976d2',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
margin: '0.5rem',
|
||||
},
|
||||
}
|
||||
|
||||
export default RouterError
|
||||
@@ -0,0 +1,21 @@
|
||||
const genericErrorMessage = 'An unexpected navigation error occurred.'
|
||||
|
||||
export const getRouterErrorMessage = (error: unknown, isDevelopment: boolean): string => {
|
||||
if (!isDevelopment) {
|
||||
return genericErrorMessage
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (error instanceof Response) {
|
||||
return `${error.status} ${error.statusText}`
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error
|
||||
}
|
||||
|
||||
return 'An unknown error occurred'
|
||||
}
|
||||
@@ -5,11 +5,14 @@ import { Provider } from 'react-redux'
|
||||
import './styles/global.css'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import router from './router'
|
||||
import { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<RouterProvider router={router} />
|
||||
</Provider>
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<RouterProvider router={router} />
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate, createBrowserRouter } from 'react-router-dom'
|
||||
import App from '../App'
|
||||
import { RouterError } from '../components/ErrorBoundary/RouterError'
|
||||
|
||||
const loadPagesElement = async (
|
||||
key:
|
||||
@@ -25,6 +26,7 @@ const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <App />,
|
||||
errorElement: <RouterError />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from '@jest/globals'
|
||||
|
||||
import { getRouterErrorMessage } from '@/components/ErrorBoundary/RouterErrorMessages'
|
||||
|
||||
describe('getRouterErrorMessage', () => {
|
||||
it('returns a generic message outside development', () => {
|
||||
expect(getRouterErrorMessage(new Error('sensitive route loader details'), false)).toBe(
|
||||
'An unexpected navigation error occurred.'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns raw error details in development', () => {
|
||||
expect(getRouterErrorMessage(new Error('route loader failed'), true)).toBe('route loader failed')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user