* * add error boundary immediately above page component * send error logs to server via LoggerContext * display error stack and component stack in copyable text area * wrap every context provider in an error boundary * memoize context values to fix error boundary propagation issue * Wrap page component in an error boundary so we can use our context utilities for a better experience, like toast and logger Still have a top-level error boundary that catches things from the context providers, just in case. Don't display the whole error stack, just because it's ugly, but make it copyable
93 lines
2.8 KiB
JavaScript
93 lines
2.8 KiB
JavaScript
import { useRouter } from 'next/router'
|
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
import Button from 'react-bootstrap/Button'
|
|
import Toast from 'react-bootstrap/Toast'
|
|
import ToastBody from 'react-bootstrap/ToastBody'
|
|
import ToastContainer from 'react-bootstrap/ToastContainer'
|
|
import styles from './toast.module.css'
|
|
|
|
const ToastContext = createContext(() => {})
|
|
|
|
export const ToastProvider = ({ children }) => {
|
|
const router = useRouter()
|
|
const [toasts, setToasts] = useState([])
|
|
const toastId = useRef(0)
|
|
const dispatchToast = useCallback((toastConfig) => {
|
|
toastConfig = {
|
|
...toastConfig,
|
|
id: toastId.current++
|
|
}
|
|
setToasts(toasts => [...toasts, toastConfig])
|
|
}, [])
|
|
|
|
const removeToast = useCallback(id => {
|
|
setToasts(toasts => toasts.filter(toast => toast.id !== id))
|
|
}, [])
|
|
|
|
const toaster = useMemo(() => ({
|
|
success: body => {
|
|
dispatchToast({
|
|
body,
|
|
variant: 'success',
|
|
autohide: true,
|
|
delay: 5000
|
|
})
|
|
},
|
|
danger: (body, onCloseCallback) => {
|
|
const id = toastId.current
|
|
dispatchToast({
|
|
id,
|
|
body,
|
|
variant: 'danger',
|
|
autohide: false,
|
|
onCloseCallback
|
|
})
|
|
return {
|
|
removeToast: () => removeToast(id)
|
|
}
|
|
}
|
|
}), [dispatchToast, removeToast])
|
|
|
|
// Clear all toasts on page navigation
|
|
useEffect(() => {
|
|
const handleRouteChangeStart = () => setToasts([])
|
|
router.events.on('routeChangeStart', handleRouteChangeStart)
|
|
|
|
return () => {
|
|
router.events.off('routeChangeStart', handleRouteChangeStart)
|
|
}
|
|
}, [router])
|
|
|
|
return (
|
|
<ToastContext.Provider value={toaster}>
|
|
<ToastContainer className={`pb-3 pe-3 ${styles.toastContainer}`} position='bottom-end' containerPosition='fixed'>
|
|
{toasts.map(toast => (
|
|
<Toast
|
|
key={toast.id} bg={toast.variant} show autohide={toast.autohide}
|
|
delay={toast.delay} className={`${styles.toast} ${styles[toast.variant]}`} onClose={() => removeToast(toast.id)}
|
|
>
|
|
<ToastBody>
|
|
<div className='d-flex align-items-center'>
|
|
<div className='flex-grow-1'>{toast.body}</div>
|
|
<Button
|
|
variant={null}
|
|
className='p-0 ps-2'
|
|
aria-label='close'
|
|
onClick={() => {
|
|
if (toast.onCloseCallback) toast.onCloseCallback()
|
|
removeToast(toast.id)
|
|
}}
|
|
><div className={styles.toastClose}>X</div>
|
|
</Button>
|
|
</div>
|
|
</ToastBody>
|
|
</Toast>
|
|
))}
|
|
</ToastContainer>
|
|
{children}
|
|
</ToastContext.Provider>
|
|
)
|
|
}
|
|
|
|
export const useToast = () => useContext(ToastContext)
|