Browse Source

Merge pull request #1621 from Gamaranto/error-states

Add Error Boundaries To Routes and Galleries
Bedeho Mender 4 years ago
parent
commit
5305328d79

+ 1 - 0
package.json

@@ -94,6 +94,7 @@
     "react-docgen-typescript-loader": "^3.7.1",
     "react-dom": "^16.13.1",
     "react-glider": "^2.0.2",
+    "react-error-boundary": "^3.0.2",
     "react-player": "^2.2.0",
     "react-scripts": "3.4.1",
     "react-spring": "^8.0.27",

File diff suppressed because it is too large
+ 6 - 0
public/fonts.css


File diff suppressed because it is too large
+ 39 - 0
src/assets/error.svg


+ 0 - 1
src/components/ChannelGallery.tsx

@@ -1,5 +1,4 @@
 import React from 'react'
-import styled from '@emotion/styled'
 
 import { ChannelPreviewBase, Gallery } from '@/shared/components'
 import ChannelPreview from './ChannelPreviewWithNavigation'

+ 32 - 0
src/components/ErrorFallback.tsx

@@ -0,0 +1,32 @@
+import React from 'react'
+
+import styled from '@emotion/styled'
+import { FallbackProps } from 'react-error-boundary'
+
+import { Button } from '@/shared/components'
+import { sizes, colors } from '@/shared/theme'
+
+const Container = styled.div`
+  padding: ${sizes.b4}px;
+  color: ${colors.gray[400]};
+  display: grid;
+  place-items: center;
+`
+
+const StyledButton = styled(Button)`
+  color: ${colors.white};
+`
+
+const ErrorFallback: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
+  console.error(error)
+  return (
+    <Container>
+      <p>Something went wrong...</p>
+      <StyledButton variant="tertiary" onClick={resetErrorBoundary}>
+        Try again
+      </StyledButton>
+    </Container>
+  )
+}
+
+export default ErrorFallback

+ 25 - 8
src/components/LayoutWithRouting.tsx

@@ -1,12 +1,29 @@
 import React from 'react'
 import styled from '@emotion/styled'
-import { Router } from '@reach/router'
+import { RouteComponentProps, Router, navigate } from '@reach/router'
+import { ErrorBoundary } from 'react-error-boundary'
 
 import { GlobalStyle } from '@/shared/components'
-import { Navbar } from '@/components'
-import { BrowseView, ChannelView, HomeView, SearchView, VideoView } from '@/views'
+import { Navbar, ViewErrorFallback } from '@/components'
+import { HomeView, VideoView, SearchView, ChannelView, BrowseView } from '@/views'
 import routes from '@/config/routes'
 
+type RouteProps = {
+  Component: React.ComponentType
+} & RouteComponentProps
+const Route: React.FC<RouteProps> = ({ Component, ...pathProps }) => {
+  return (
+    <ErrorBoundary
+      FallbackComponent={ViewErrorFallback}
+      onReset={() => {
+        navigate('/')
+      }}
+    >
+      <Component {...pathProps} />
+    </ErrorBoundary>
+  )
+}
+
 const LayoutWithRouting: React.FC = () => (
   <>
     <GlobalStyle />
@@ -15,11 +32,11 @@ const LayoutWithRouting: React.FC = () => (
     </Router>
     <MainContainer>
       <Router primary={false}>
-        <HomeView default />
-        <VideoView path={routes.video()} />
-        <SearchView path={routes.search()} />
-        <BrowseView path={routes.browse()} />
-        <ChannelView path={routes.channel()} />
+        <Route default Component={HomeView} />
+        <Route path={routes.video()} Component={VideoView} />
+        <Route path={routes.search()} Component={SearchView} />
+        <Route Component={BrowseView} path={routes.browse()} />
+        <Route Component={ChannelView} path={routes.channel()} />
       </Router>
     </MainContainer>
   </>

+ 51 - 0
src/components/ViewErrorFallback.tsx

@@ -0,0 +1,51 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import { FallbackProps } from 'react-error-boundary'
+
+import { ReactComponent as ErrorIllustration } from '@/assets/error.svg'
+import { Button, Typography } from '@/shared/components'
+import { sizes, colors } from '@/shared/theme'
+
+const Container = styled.div`
+  margin: ${sizes.b10 * 2}px auto 0;
+  display: grid;
+  place-items: center;
+
+  > svg {
+    max-width: 650px;
+  }
+`
+
+const Message = styled.div`
+  display: flex;
+  flex-direction: column;
+  text-align: center;
+  margin-top: 90px;
+  margin-bottom: ${sizes.b10}px;
+
+  > p {
+    margin: 0;
+    line-height: 1.75;
+    color: ${colors.gray[300]};
+  }
+`
+
+const Title = styled(Typography)`
+  line-height: 1.25;
+`
+
+const ErrorFallback: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
+  console.error(error)
+  return (
+    <Container>
+      <ErrorIllustration />
+      <Message>
+        <Title variant="h3">Ops! An Error occurred</Title>
+        <p>We could not acquire expected results. Please try reloading or return to the home page.</p>
+      </Message>
+      <Button onClick={resetErrorBoundary}>Return to home page</Button>
+    </Container>
+  )
+}
+
+export default ErrorFallback

+ 2 - 0
src/components/index.ts

@@ -8,3 +8,5 @@ export { default as PlaceholderVideoGrid } from './PlaceholderVideoGrid'
 export { default as VideoPreview } from './VideoPreviewWithNavigation'
 export { default as ChannelPreview } from './ChannelPreviewWithNavigation'
 export { default as ChannelGrid } from './ChannelGrid'
+export { default as ViewErrorFallback } from './ViewErrorFallback'
+export { default as ErrorFallback } from './ErrorFallback'

+ 5 - 1
src/shared/components/InfiniteVideoGrid/InfiniteVideoGrid.tsx

@@ -40,13 +40,17 @@ const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   const targetDisplayedVideosCount = targetRowsCount * videosPerRow
   const targetLoadedVideosCount = targetDisplayedVideosCount + skipCount
 
-  const [fetchVideos, { loading, data, fetchMore, called, refetch }] = useLazyQuery<
+  const [fetchVideos, { loading, data, error, fetchMore, called, refetch }] = useLazyQuery<
     GetNewestVideos,
     GetNewestVideosVariables
   >(GET_NEWEST_VIDEOS, {
     notifyOnNetworkStatusChange: true,
   })
 
+  if (error) {
+    throw error
+  }
+
   const loadedVideosCount = data?.videosConnection.edges.length || 0
   const allVideosLoaded = data ? !data.videosConnection.pageInfo.hasNextPage : false
 

+ 16 - 4
src/views/BrowseView.tsx

@@ -1,6 +1,9 @@
 import React, { useState } from 'react'
 import styled from '@emotion/styled'
 import { RouteComponentProps } from '@reach/router'
+import { ErrorBoundary } from 'react-error-boundary'
+
+import { ErrorFallback } from '@/components'
 import { CategoryPicker, InfiniteVideoGrid, Typography } from '@/shared/components'
 import { colors, sizes } from '@/shared/theme'
 import { useQuery } from '@apollo/client'
@@ -10,14 +13,21 @@ import { CategoryFields } from '@/api/queries/__generated__/CategoryFields'
 
 const BrowseView: React.FC<RouteComponentProps> = () => {
   const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
-  const { loading: categoriesLoading, data: categoriesData } = useQuery<GetCategories>(GET_CATEGORIES, {
-    onCompleted: (data) => handleCategoryChange(data.categories[0]),
-  })
+  const { loading: categoriesLoading, data: categoriesData, error: categoriesError } = useQuery<GetCategories>(
+    GET_CATEGORIES,
+    {
+      onCompleted: (data) => handleCategoryChange(data.categories[0]),
+    }
+  )
 
   const handleCategoryChange = (category: CategoryFields) => {
     setSelectedCategoryId(category.id)
   }
 
+  if (categoriesError) {
+    throw categoriesError
+  }
+
   return (
     <div>
       <Header variant="hero">Browse</Header>
@@ -28,7 +38,9 @@ const BrowseView: React.FC<RouteComponentProps> = () => {
         selectedCategoryId={selectedCategoryId}
         onChange={handleCategoryChange}
       />
-      <StyledInfiniteVideoGrid categoryId={selectedCategoryId || undefined} ready={!!selectedCategoryId} />
+      <ErrorBoundary FallbackComponent={ErrorFallback}>
+        <StyledInfiniteVideoGrid categoryId={selectedCategoryId || undefined} ready={!!selectedCategoryId} />
+      </ErrorBoundary>
     </div>
   )
 }

+ 8 - 1
src/views/ChannelView/ChannelView.tsx

@@ -23,10 +23,17 @@ const DEFAULT_CHANNEL_COVER_URL = 'https://eu-central-1.linodeobjects.com/atlas-
 
 const ChannelView: React.FC<RouteComponentProps> = () => {
   const { id } = useParams()
-  const { loading, data } = useQuery<GetChannel, GetChannelVariables>(GET_CHANNEL, {
+  const { loading, data, error } = useQuery<GetChannel, GetChannelVariables>(GET_CHANNEL, {
     variables: { id },
   })
 
+  if (error) {
+    throw error
+  }
+
+  if (loading || !data?.channel) {
+    return <p>Loading Channel...</p>
+  }
   const videos = data?.channel?.videos || []
 
   return (

+ 56 - 20
src/views/HomeView.tsx

@@ -1,8 +1,10 @@
 import React from 'react'
 import styled from '@emotion/styled'
-import { ChannelGallery, FeaturedVideoHeader, VideoGallery } from '@/components'
+import { ChannelGallery, FeaturedVideoHeader, ErrorFallback, VideoGallery } from '@/components'
+
 import { RouteComponentProps } from '@reach/router'
 import { useQuery } from '@apollo/client'
+import { ErrorBoundary } from 'react-error-boundary'
 import { InfiniteVideoGrid } from '@/shared/components'
 import { GET_FEATURED_VIDEOS, GET_NEWEST_CHANNELS, GET_NEWEST_VIDEOS } from '@/api/queries'
 import { GetFeaturedVideos } from '@/api/queries/__generated__/GetFeaturedVideos'
@@ -14,33 +16,67 @@ const NEWEST_VIDEOS_COUNT = 8
 const NEWEST_CHANNELS_COUNT = 8
 
 const HomeView: React.FC<RouteComponentProps> = () => {
-  const { loading: newestVideosLoading, data: videosData } = useQuery<GetNewestVideos, GetNewestVideosVariables>(
-    GET_NEWEST_VIDEOS,
-    {
-      variables: { first: NEWEST_VIDEOS_COUNT },
-    }
-  )
-  const { loading: featuredVideosLoading, data: featuredVideosData } = useQuery<GetFeaturedVideos>(GET_FEATURED_VIDEOS)
-  const { loading: newestChannelsLoading, data: newestChannelsData } = useQuery<
-    GetNewestChannels,
-    GetNewestChannelsVariables
-  >(GET_NEWEST_CHANNELS, { variables: { first: NEWEST_CHANNELS_COUNT } })
+  const {
+    loading: newestVideosLoading,
+    data: videosData,
+    error: newestVideosError,
+    refetch: refetchNewestVideos,
+  } = useQuery<GetNewestVideos, GetNewestVideosVariables>(GET_NEWEST_VIDEOS, {
+    variables: { first: NEWEST_VIDEOS_COUNT },
+    notifyOnNetworkStatusChange: true,
+  })
+  const {
+    loading: featuredVideosLoading,
+    data: featuredVideosData,
+    error: featuredVideosError,
+    refetch: refetchFeaturedVideos,
+  } = useQuery<GetFeaturedVideos>(GET_FEATURED_VIDEOS, {
+    notifyOnNetworkStatusChange: true,
+  })
+  const {
+    loading: newestChannelsLoading,
+    data: newestChannelsData,
+    error: newestChannelsError,
+    refetch: refetchNewestChannels,
+  } = useQuery<GetNewestChannels, GetNewestChannelsVariables>(GET_NEWEST_CHANNELS, {
+    variables: { first: NEWEST_CHANNELS_COUNT },
+    notifyOnNetworkStatusChange: true,
+  })
 
   const newestVideos = videosData?.videosConnection.edges.slice(0, NEWEST_VIDEOS_COUNT).map((e) => e.node)
   const newestChannels = newestChannelsData?.channelsConnection.edges.map((e) => e.node)
+  const hasNewestVideosError = newestVideosError && !newestVideosLoading
+  const hasFeaturedVideosError = featuredVideosError && !featuredVideosLoading
+  const hasNewestChannelsError = newestChannelsError && !newestChannelsLoading
 
   return (
     <>
       <FeaturedVideoHeader />
       <Container>
-        <VideoGallery title="Newest videos" loading={newestVideosLoading} videos={newestVideos} />
-        <VideoGallery
-          title="Featured videos"
-          loading={featuredVideosLoading}
-          videos={featuredVideosData?.featured_videos}
-        />
-        <ChannelGallery title="Newest channels" loading={newestChannelsLoading} channels={newestChannels} />
-        <StyledInfiniteVideoGrid title="More videos" skipCount={NEWEST_VIDEOS_COUNT} />
+        {!hasNewestVideosError ? (
+          <VideoGallery title="Newest videos" loading={newestVideosLoading} videos={newestVideos} />
+        ) : (
+          <ErrorFallback error={newestVideosError} resetErrorBoundary={() => refetchNewestVideos()} />
+        )}
+
+        {!hasFeaturedVideosError ? (
+          <VideoGallery
+            title="Featured videos"
+            loading={featuredVideosLoading}
+            videos={featuredVideosData?.featured_videos}
+          />
+        ) : (
+          <ErrorFallback error={featuredVideosError} resetErrorBoundary={() => refetchFeaturedVideos()} />
+        )}
+
+        {!hasNewestChannelsError ? (
+          <ChannelGallery title="Newest channels" loading={newestChannelsLoading} channels={newestChannels} />
+        ) : (
+          <ErrorFallback error={newestChannelsError} resetErrorBoundary={() => refetchNewestChannels()} />
+        )}
+        <ErrorBoundary FallbackComponent={ErrorFallback}>
+          <StyledInfiniteVideoGrid title="More videos" skipCount={NEWEST_VIDEOS_COUNT} />
+        </ErrorBoundary>
       </Container>
     </>
   )

+ 8 - 2
src/views/SearchView/SearchView.tsx

@@ -18,7 +18,7 @@ const tabs = ['all results', 'videos', 'channels']
 
 const SearchView: React.FC<SearchViewProps> = ({ search = '' }) => {
   const [selectedIndex, setSelectedIndex] = useState(0)
-  const { data, loading } = useQuery<Search, SearchVariables>(SEARCH, { variables: { query_string: search } })
+  const { data, loading, error } = useQuery<Search, SearchVariables>(SEARCH, { variables: { query_string: search } })
 
   const getChannelsAndVideos = (loading: boolean, data: Search | undefined) => {
     if (loading || !data?.search) {
@@ -32,8 +32,14 @@ const SearchView: React.FC<SearchViewProps> = ({ search = '' }) => {
 
   const { channels, videos } = useMemo(() => getChannelsAndVideos(loading, data), [loading, data])
 
+  if (error) {
+    throw error
+  }
   if (!loading && !data?.search) {
-    return <p>Something went wrong...</p>
+    throw new Error(`There was a problem with your search...`)
+  }
+  if (loading || !data) {
+    return <p>Loading...</p>
   }
 
   if (!loading && channels.length === 0 && videos.length === 0) {

+ 5 - 1
src/views/VideoView/VideoView.tsx

@@ -26,7 +26,7 @@ import { AddVideoView, AddVideoViewVariables } from '@/api/queries/__generated__
 
 const VideoView: React.FC<RouteComponentProps> = () => {
   const { id } = useParams()
-  const { loading, data } = useQuery<GetVideo, GetVideoVariables>(GET_VIDEO_WITH_CHANNEL_VIDEOS, {
+  const { loading, data, error } = useQuery<GetVideo, GetVideoVariables>(GET_VIDEO_WITH_CHANNEL_VIDEOS, {
     variables: { id },
   })
   const [addVideoView] = useMutation<AddVideoView, AddVideoViewVariables>(ADD_VIDEO_VIEW)
@@ -55,6 +55,10 @@ const VideoView: React.FC<RouteComponentProps> = () => {
     })
   }, [addVideoView, videoID])
 
+  if(error) {
+    throw error
+  }
+
   if (!loading && !data?.video) {
     return <p>Video not found</p>
   }

+ 7 - 0
yarn.lock

@@ -14046,6 +14046,13 @@ react-element-to-jsx-string@^14.1.0, react-element-to-jsx-string@^14.3.1:
     "@base2/pretty-print-object" "1.0.0"
     is-plain-object "3.0.0"
 
+react-error-boundary@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.0.2.tgz#74399a4d9a68bfede1f3f4261ea0aabfc65d9868"
+  integrity sha512-KVzCusRTFpUYG0OFJbzbdRuxNQOBiGXVCqyNpBXM9z5NFsFLzMjUXMjx8gTja6M6WH+D2PvP3yKz4d8gD1PRaA==
+  dependencies:
+    "@babel/runtime" "^7.11.2"
+
 react-error-overlay@^6.0.3, react-error-overlay@^6.0.7:
   version "6.0.7"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"

Some files were not shown because too many files changed in this diff