Files
HKSingleParty/03_source/frontend/src/routes/components/error-boundary.tsx
2025-05-28 09:55:51 +08:00

169 lines
4.6 KiB
TypeScript

import type { Theme, CSSObject } from '@mui/material/styles';
import { useRouteError, isRouteErrorResponse } from 'react-router';
import GlobalStyles from '@mui/material/GlobalStyles';
// ----------------------------------------------------------------------
export function ErrorBoundary() {
const error = useRouteError();
return (
<>
{inputGlobalStyles()}
<div className={errorBoundaryClasses.root}>
<div className={errorBoundaryClasses.container}>{renderErrorMessage(error)}</div>
</div>
</>
);
}
// ----------------------------------------------------------------------
function parseStackTrace(stack?: string) {
if (!stack) return { filePath: null, functionName: null };
const filePathMatch = stack.match(/\/src\/[^?]+/);
const functionNameMatch = stack.match(/at (\S+)/);
return {
filePath: filePathMatch ? filePathMatch[0] : null,
functionName: functionNameMatch ? functionNameMatch[1] : null,
};
}
function renderErrorMessage(error: any) {
if (isRouteErrorResponse(error)) {
return (
<>
<h1 className={errorBoundaryClasses.title}>
{error.status}: {error.statusText}
</h1>
<p className={errorBoundaryClasses.message}>{error.data}</p>
</>
);
}
if (error instanceof Error) {
const { filePath, functionName } = parseStackTrace(error.stack);
return (
<>
<h1 className={errorBoundaryClasses.title}>Unexpected Application Error!</h1>
<p className={errorBoundaryClasses.message}>
{error.name}: {error.message}
</p>
<pre className={errorBoundaryClasses.details}>{error.stack}</pre>
{(filePath || functionName) && (
<p className={errorBoundaryClasses.filePath}>
{filePath} ({functionName})
</p>
)}
</>
);
}
return <h1 className={errorBoundaryClasses.title}>Unknown Error</h1>;
}
// ----------------------------------------------------------------------
const errorBoundaryClasses = {
root: 'error-boundary-root',
container: 'error-boundary-container',
title: 'error-boundary-title',
details: 'error-boundary-details',
message: 'error-boundary-message',
filePath: 'error-boundary-file-path',
};
const cssVars: CSSObject = {
'--info-color': '#2dd9da',
'--warning-color': '#e2aa53',
'--error-color': '#ff5555',
'--error-background': '#2a1e1e',
'--details-background': '#111111',
'--root-background': '#2c2c2e',
'--container-background': '#1c1c1e',
'--font-stack-monospace':
'"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',
'--font-stack-sans':
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
};
const rootStyles = (): CSSObject => ({
display: 'flex',
flex: '1 1 auto',
alignItems: 'center',
padding: '10vh 15px 0',
flexDirection: 'column',
fontFamily: 'var(--font-stack-sans)',
});
const contentStyles = (): CSSObject => ({
gap: 24,
padding: 20,
width: '100%',
maxWidth: 960,
display: 'flex',
borderRadius: 8,
flexDirection: 'column',
backgroundColor: 'var(--container-background)',
});
const titleStyles = (theme: Theme): CSSObject => ({
margin: 0,
lineHeight: 1.2,
fontSize: theme.typography.pxToRem(20),
fontWeight: theme.typography.fontWeightBold,
});
const messageStyles = (theme: Theme): CSSObject => ({
margin: 0,
lineHeight: 1.5,
padding: '12px 16px',
whiteSpace: 'pre-wrap',
color: 'var(--error-color)',
fontSize: theme.typography.pxToRem(14),
fontFamily: 'var(--font-stack-monospace)',
backgroundColor: 'var(--error-background)',
borderLeft: '2px solid var(--error-color)',
fontWeight: theme.typography.fontWeightBold,
});
const detailsStyles = (): CSSObject => ({
margin: 0,
padding: 16,
lineHeight: 1.5,
overflow: 'auto',
borderRadius: 'inherit',
color: 'var(--warning-color)',
backgroundColor: 'var(--details-background)',
});
const filePathStyles = (): CSSObject => ({
marginTop: 0,
color: 'var(--info-color)',
});
const inputGlobalStyles = () => (
<GlobalStyles
styles={(theme) => ({
body: {
...cssVars,
margin: 0,
color: 'white',
backgroundColor: 'var(--root-background)',
[`& .${errorBoundaryClasses.root}`]: rootStyles(),
[`& .${errorBoundaryClasses.container}`]: contentStyles(),
[`& .${errorBoundaryClasses.title}`]: titleStyles(theme),
[`& .${errorBoundaryClasses.message}`]: messageStyles(theme),
[`& .${errorBoundaryClasses.filePath}`]: filePathStyles(),
[`& .${errorBoundaryClasses.details}`]: detailsStyles(),
},
})}
/>
);