Merge branch 'main' into codex/1056-web-freezing-locality-map

This commit is contained in:
Kari Lintulaakso
2026-05-29 16:05:11 +03:00
committed by GitHub
7 changed files with 230 additions and 4 deletions
@@ -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'
}
@@ -72,8 +72,8 @@ export const UnsavedChangesProvider = ({
const handleConfirm = useCallback(() => {
if (blocker.state === 'blocked') {
setDirty(false)
blocker.proceed?.()
setDirty(false)
}
}, [blocker, setDirty])
+6 -3
View File
@@ -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>
)
+2
View File
@@ -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')
})
})