_app.tsx 3.57 KB
Newer Older
Stefan Probst's avatar
Stefan Probst committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import Layout from '@stefanprobst/next-app-layout'
import ErrorBoundary from '@stefanprobst/next-error-boundary'
import type { AppProps, NextWebVitalsMetric } from 'next/app'
import Head from 'next/head'
import { Router } from 'next/router'
import np from 'nprogress'
import type { PropsWithChildren } from 'react'
import { Fragment } from 'react'
import { QueryCache, ReactQueryCacheProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query-devtools'
import { ToastContainer, Slide } from 'react-toastify'
import AuthProvider from '@/modules/auth/AuthContext'
import ClientError from '@/modules/error/ClientError'
import PageLayout from '@/modules/page/PageLayout'

import 'focus-visible'
import '@/styles/global.css'
import 'tailwindcss/tailwind.css'
import '@/styles/nprogress.css'
/** should use ReactToastify.minimal.css */
import 'react-toastify/dist/ReactToastify.css'
import '@/styles/combobox.css'
import '@reach/dialog/styles.css'

/**
 * Report web vitals.
 */
export function reportWebVitals(metric: NextWebVitalsMetric): void {
  /** should be dispatched to an analytics service */
  console.info(metric)
}

/**
 * Report page transitions to Matomo analytics.
 */
Router.events.on('routeChangeComplete', (url) => {
  if (typeof window !== 'undefined') {
    const w = window as typeof window & { _paq?: Array<unknown> }
    if (w._paq !== undefined) {
      w._paq.push(['setCustomUrl', url])
      w._paq.push(['setDocumentTitle', document.title])
      w._paq.push(['trackPageView'])
    }
  }
})

/**
 * Progress bar for client-side page transitions.
 */
np.configure({ showSpinner: false })
/** show progress indicator only if transition takes longer than `delay` */
const delay = 250
let timeout: number
function startProgressIndicator() {
  timeout = window.setTimeout(np.start, delay)
}
function stopProgressIndicator() {
  window.clearTimeout(timeout)
  np.done()
}
Router.events.on('routeChangeStart', startProgressIndicator)
Router.events.on('routeChangeComplete', stopProgressIndicator)
Router.events.on('routeChangeError', stopProgressIndicator)

/**
 * Client side cache for server data.
 */
const queryCache = new QueryCache({
  defaultConfig: {
    queries: {
      cacheTime: Infinity,
      staleTime: Infinity,
      structuralSharing: false,
    },
  },
})

/**
 * Providers.
 */
function Providers({ children }: PropsWithChildren<unknown>) {
  return (
    <ReactQueryCacheProvider queryCache={queryCache}>
      <AuthProvider>{children}</AuthProvider>
    </ReactQueryCacheProvider>
  )
}

/**
 * App shell.
 */
export default function App({ Component, pageProps }: AppProps): JSX.Element {
  return (
    <Fragment>
      <Head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      </Head>
      <ErrorBoundary fallback={ClientError}>
        <Providers {...pageProps}>
          <Layout {...pageProps} default={PageLayout}>
            <Component {...pageProps} />
          </Layout>
          <ToastContainer
            transition={Slide}
            toastClassName={({ type } = {}) => {
              const shared =
                'relative mb-4 p-4 rounded shadow-md flex justify-between overflow-hidden cursor-pointer'
              switch (type) {
                case 'error':
                  return [shared, 'bg-error-600 text-white'].join(' ')
                default:
                  return [shared, 'bg-secondary-600 text-white'].join(' ')
              }
            }}
            bodyClassName="text-sm font-medium p-2 flex-1 mx-auto"
          />
          <ReactQueryDevtools />
        </Providers>
      </ErrorBoundary>
    </Fragment>
  )
}