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 './styles/global.css'
|
||||||
import { RouterProvider } from 'react-router-dom'
|
import { RouterProvider } from 'react-router-dom'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<ErrorBoundary>
|
||||||
<RouterProvider router={router} />
|
<Provider store={store}>
|
||||||
</Provider>
|
<RouterProvider router={router} />
|
||||||
|
</Provider>
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Navigate, createBrowserRouter } from 'react-router-dom'
|
import { Navigate, createBrowserRouter } from 'react-router-dom'
|
||||||
import App from '../App'
|
import App from '../App'
|
||||||
|
import { RouterError } from '../components/ErrorBoundary/RouterError'
|
||||||
|
|
||||||
const loadPagesElement = async (
|
const loadPagesElement = async (
|
||||||
key:
|
key:
|
||||||
@@ -25,6 +26,7 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <App />,
|
element: <App />,
|
||||||
|
errorElement: <RouterError />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
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