Skip to content

Commit

Permalink
Merge pull request #48 from oasisprotocol/ml/error-bundary
Browse files Browse the repository at this point in the history
Add error boundaries
  • Loading branch information
lubej authored Mar 19, 2024
2 parents 19aafc5 + 46dfe63 commit 68831f5
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 60 deletions.
19 changes: 12 additions & 7 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { EIP1193ContextProvider } from './providers/EIP1193Provider.tsx'
import { Web3ContextProvider } from './providers/Web3Provider.tsx'
import { ResultsPage } from './pages/ResultsPage'
import { AppStateContextProvider } from './providers/AppStateProvider.tsx'
import { ErrorBoundary } from './components/ErrorBoundary'
import { RouterErrorBoundary } from './components/RouterErrorBoundary'

const router = createHashRouter([
{
path: '/',
element: <Layout />,
errorElement: <RouterErrorBoundary />,
children: [
{
path: 'results',
Expand All @@ -26,12 +29,14 @@ const router = createHashRouter([

export const App: FC = () => {
return (
<EIP1193ContextProvider>
<Web3ContextProvider>
<AppStateContextProvider>
<RouterProvider router={router} />
</AppStateContextProvider>
</Web3ContextProvider>
</EIP1193ContextProvider>
<ErrorBoundary>
<EIP1193ContextProvider>
<Web3ContextProvider>
<AppStateContextProvider>
<RouterProvider router={router} />
</AppStateContextProvider>
</Web3ContextProvider>
</EIP1193ContextProvider>
</ErrorBoundary>
)
}
6 changes: 4 additions & 2 deletions frontend/src/components/Alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { WarningCircleIcon } from '../icons/WarningCircleIcon.tsx'
import { CheckCircleIcon } from '../icons/CheckCircleIcon.tsx'
import { SpinnerIcon } from '../icons/SpinnerIcon.tsx'
import { PiggyBankSvgIcon } from '../icons/PiggyBankIcon.tsx'
import { StringUtils } from '../../utils/string.utils.ts'

type AlertType = 'error' | 'success' | 'loading' | 'insufficient-balance'

Expand Down Expand Up @@ -43,13 +44,14 @@ interface Props extends PropsWithChildren {
type: AlertType
actions?: ReactElement
headerText?: string
className?: string
}

export const Alert: FC<Props> = ({ children, type, actions, headerText }) => {
export const Alert: FC<Props> = ({ children, className, type, actions, headerText }) => {
const { header, icon } = alertTypeValuesMap[type]

return (
<Card className={alertTypeClassMap[type]}>
<Card className={StringUtils.clsx(className, alertTypeClassMap[type])}>
<div className={classes.alert}>
<h2>{headerText ?? header}</h2>
<p>{children}</p>
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/ErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Component, PropsWithChildren } from 'react'
import { ErrorBoundaryLayout } from '../ErrorBoundaryLayout'

interface Props {
hasError: boolean
error?: unknown
}

export class ErrorBoundary extends Component<PropsWithChildren, Props> {
constructor(props: PropsWithChildren) {
super(props)
this.state = { hasError: false }
}

static getDerivedStateFromError(error: unknown) {
console.error(error)
return { hasError: true, error }
}

render() {
if (this.state.hasError) {
return <ErrorBoundaryLayout error={this.state.error} />
}

return this.props.children
}
}
9 changes: 9 additions & 0 deletions frontend/src/components/ErrorBoundaryLayout/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.errorAlert {
margin-top: 10rem;
}

@media screen and (max-width: 1000px) {
.errorAlert {
margin-top: 0;
}
}
17 changes: 17 additions & 0 deletions frontend/src/components/ErrorBoundaryLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FC } from 'react'
import { LayoutBase } from '../LayoutBase'
import { StringUtils } from '../../utils/string.utils.ts'
import { Alert } from '../Alert'
import classes from './index.module.css'

interface Props {
error: unknown
}

export const ErrorBoundaryLayout: FC<Props> = ({ error }) => (
<LayoutBase>
<Alert className={classes.errorAlert} type="error">
{StringUtils.truncate((error as Error).message ?? JSON.stringify(error))}
</Alert>
</LayoutBase>
)
17 changes: 0 additions & 17 deletions frontend/src/components/Layout/index.module.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
.layout {
min-height: 100dvh;
display: flex;
flex-direction: column;
justify-content: space-between;
}

.main {
margin: 0 auto;
width: 100%;
max-width: 1200px;
}

.header {
display: flex;
justify-content: space-between;
Expand All @@ -30,10 +17,6 @@
}

@media screen and (max-width: 1000px) {
.layout {
padding: 0 1.5rem 0.25rem;
}

.inViewPlaceholder {
position: absolute;
height: 1px;
Expand Down
67 changes: 33 additions & 34 deletions frontend/src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAppState } from '../../hooks/useAppState.ts'
import { Button } from '../Button'
import { StringUtils } from '../../utils/string.utils.ts'
import { useInView } from 'react-intersection-observer'
import { LayoutBase } from '../LayoutBase'

export const Layout: FC = () => {
const {
Expand All @@ -23,40 +24,38 @@ export const Layout: FC = () => {
return (
<>
{isMobileScreen && <div className={classes.inViewPlaceholder} ref={ref} />}
<div className={classes.layout}>
<main className={classes.main}>
<header
className={StringUtils.clsx(
classes.header,
isMobileScreen && !inView ? classes.headerSticky : undefined
)}
>
<LogoIcon className={classes.logo} />
<ConnectWallet mobileSticky={isMobileScreen && !inView} />
</header>
<section className={classes.subHeader}>
<h1>Oasis Mascot</h1>
</section>
<section>
{!isInitialLoading && appError && (
<Alert
type="error"
actions={
<Button variant="text" onClick={clearAppError}>
&lt; Go back&nbsp;
</Button>
}
>
{StringUtils.truncate(appError)}
</Alert>
)}
{isInitialLoading && (
<Alert headerText="Please wait" type="loading" actions={<span>Fetching poll...</span>} />
)}
{!isInitialLoading && !appError && <Outlet />}
</section>
</main>
</div>
<LayoutBase>
<header
className={StringUtils.clsx(
classes.header,
isMobileScreen && !inView ? classes.headerSticky : undefined
)}
>
<LogoIcon className={classes.logo} />
<ConnectWallet mobileSticky={isMobileScreen && !inView} />
</header>
<section className={classes.subHeader}>
<h1>Oasis Mascot</h1>
</section>
<section>
{!isInitialLoading && appError && (
<Alert
type="error"
actions={
<Button variant="text" onClick={clearAppError}>
&lt; Go back&nbsp;
</Button>
}
>
{StringUtils.truncate(appError)}
</Alert>
)}
{isInitialLoading && (
<Alert headerText="Please wait" type="loading" actions={<span>Fetching poll...</span>} />
)}
{!isInitialLoading && !appError && <Outlet />}
</section>
</LayoutBase>
</>
)
}
18 changes: 18 additions & 0 deletions frontend/src/components/LayoutBase/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.layout {
min-height: 100dvh;
display: flex;
flex-direction: column;
justify-content: space-between;
}

.main {
margin: 0 auto;
width: 100%;
max-width: 1200px;
}

@media screen and (max-width: 1000px) {
.layout {
padding: 0 1.5rem 0.25rem;
}
}
10 changes: 10 additions & 0 deletions frontend/src/components/LayoutBase/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FC, PropsWithChildren } from 'react'
import classes from './index.module.css'

export const LayoutBase: FC<PropsWithChildren> = ({ children }) => {
return (
<div className={classes.layout}>
<main className={classes.main}>{children}</main>
</div>
)
}
8 changes: 8 additions & 0 deletions frontend/src/components/RouterErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useRouteError } from 'react-router-dom'
import { ErrorBoundaryLayout } from '../ErrorBoundaryLayout'

export const RouterErrorBoundary = () => {
const error = useRouteError()

return <ErrorBoundaryLayout error={error} />
}

0 comments on commit 68831f5

Please sign in to comment.