瀏覽代碼

Add common empty state component (#1141)

* Add common empty state component

* Lint fix

* PR FIX

* PR FIX 2
Rafał Pawłow 3 年之前
父節點
當前提交
2831a8a88d

+ 38 - 0
src/shared/components/EmptyFallback/EmptyFallback.stories.tsx

@@ -0,0 +1,38 @@
+import { Story } from '@storybook/react'
+import React from 'react'
+
+import { Button } from '@/shared/components'
+import { SvgGlyphUpload } from '@/shared/icons'
+
+import { EmptyFallback, EmptyFallbackProps } from './EmptyFallback'
+
+export default {
+  title: 'Shared/E/EmptyFallback',
+  argTypes: {
+    title: {
+      control: { type: 'text' },
+      defaultValue: 'No draft here yet',
+    },
+    subtitle: {
+      control: { type: 'text' },
+      defaultValue: 'Each unfinished project will be saved here as a draft. Start publishing to see something here.',
+    },
+    variant: {
+      control: { type: 'select', options: ['small', 'large'] },
+      defaultValue: 'large',
+    },
+  },
+}
+
+const Template: Story<EmptyFallbackProps> = (args) => (
+  <EmptyFallback
+    {...args}
+    button={
+      <Button icon={<SvgGlyphUpload />} variant="secondary" size="large">
+        Upload video
+      </Button>
+    }
+  />
+)
+
+export const Default = Template.bind({})

+ 40 - 0
src/shared/components/EmptyFallback/EmptyFallback.styles.ts

@@ -0,0 +1,40 @@
+import styled from '@emotion/styled'
+
+import { media, sizes } from '@/shared/theme'
+
+import { EmptyFallbackSizes } from './EmptyFallback'
+
+import { Text } from '../Text'
+
+export const Container = styled.div<{ variant?: EmptyFallbackSizes }>`
+  margin: ${sizes(10)} auto;
+  display: grid;
+  place-items: center;
+
+  ${media.compact} {
+    width: ${({ variant }) => (variant === 'large' ? sizes(90) : 'auto')};
+  }
+
+  ${({ variant }) => `
+    ${Title} {
+      margin-top: ${sizes(variant === 'large' ? 10 : 6)};
+  `}
+`
+
+export const Message = styled.div`
+  display: flex;
+  flex-direction: column;
+  text-align: center;
+`
+
+export const Title = styled(Text)`
+  line-height: 1.25;
+`
+export const Subtitle = styled(Text)`
+  margin-top: ${sizes(2)};
+  line-height: ${sizes(5)};
+`
+
+export const ButtonWrapper = styled.div`
+  margin-top: ${sizes(6)};
+`

+ 40 - 0
src/shared/components/EmptyFallback/EmptyFallback.tsx

@@ -0,0 +1,40 @@
+import React, { FC, ReactNode } from 'react'
+
+import { SvgEmptyStateIllustration } from '@/shared/illustrations'
+
+import { ButtonWrapper, Container, Message, Subtitle, Title } from './EmptyFallback.styles'
+
+export type EmptyFallbackSizes = 'small' | 'large'
+
+export type EmptyFallbackProps = {
+  title: string
+  subtitle?: string | null
+  variant?: EmptyFallbackSizes
+  button?: ReactNode
+}
+
+const ILLUSTRATION_SIZES = {
+  small: {
+    width: 180,
+    height: 114,
+  },
+  large: {
+    width: 240,
+    height: 152,
+  },
+}
+
+export const EmptyFallback: FC<EmptyFallbackProps> = ({ title, subtitle, variant = 'large', button }) => (
+  <Container variant={variant}>
+    <SvgEmptyStateIllustration width={ILLUSTRATION_SIZES[variant].width} height={ILLUSTRATION_SIZES[variant].height} />
+    <Message>
+      {title && <Title variant={variant === 'large' ? 'h4' : 'body1'}>{title}</Title>}
+      {subtitle && (
+        <Subtitle variant="body2" secondary>
+          {subtitle}
+        </Subtitle>
+      )}
+    </Message>
+    <ButtonWrapper>{button}</ButtonWrapper>
+  </Container>
+)

+ 1 - 0
src/shared/components/EmptyFallback/index.ts

@@ -0,0 +1 @@
+export * from './EmptyFallback'

+ 1 - 0
src/shared/components/index.ts

@@ -43,3 +43,4 @@ export * from './HelperText'
 export * from './LegalText'
 export * from './Loader'
 export * from './AnimatedError'
+export * from './EmptyFallback'

File diff suppressed because it is too large
+ 3 - 5
src/shared/illustrations/EmptyStateIllustration.tsx


File diff suppressed because it is too large
+ 0 - 0
src/shared/illustrations/svgs/empty-state-illustration.svg


+ 0 - 58
src/views/studio/MyUploadsView/EmptyUploadsView.tsx

@@ -1,58 +0,0 @@
-import styled from '@emotion/styled'
-import React from 'react'
-
-import { absoluteRoutes } from '@/config/routes'
-import { Button, Text } from '@/shared/components'
-import { SvgGlyphUpload } from '@/shared/icons'
-import { SvgTheaterMaskIllustration } from '@/shared/illustrations'
-import { media, sizes, zIndex } from '@/shared/theme'
-
-export const EmptyUploadsView: React.FC = () => {
-  return (
-    <ContainerView>
-      <StyledEmptyUploadsIllustration />
-      <InnerContainerView>
-        <MessageView>
-          <Text variant="body2" secondary>
-            There are no uploads on your list
-          </Text>
-        </MessageView>
-        <Button icon={<SvgGlyphUpload />} to={absoluteRoutes.studio.editVideo()} size="large">
-          Upload video
-        </Button>
-      </InnerContainerView>
-    </ContainerView>
-  )
-}
-
-const ContainerView = styled.div`
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  overflow: hidden;
-  ${media.medium} {
-    margin-top: ${sizes(16)};
-  }
-`
-
-const InnerContainerView = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-start;
-  align-items: center;
-  margin-top: -200px;
-`
-
-const StyledEmptyUploadsIllustration = styled(SvgTheaterMaskIllustration)`
-  transform: scale(1.5);
-  width: 100%;
-  z-index: ${zIndex.farBackground};
-  ${media.small} {
-    transform: scale(1);
-  }
-`
-
-const MessageView = styled.div`
-  text-align: center;
-  margin-bottom: ${sizes(4)};
-`

+ 12 - 2
src/views/studio/MyUploadsView/MyUploadsView.tsx

@@ -1,11 +1,13 @@
 import React from 'react'
 import shallow from 'zustand/shallow'
 
+import { absoluteRoutes } from '@/config/routes'
 import { useUser } from '@/providers'
 import { useUploadsStore } from '@/providers/uploadsManager/store'
 import { AssetUpload } from '@/providers/uploadsManager/types'
+import { Button, EmptyFallback } from '@/shared/components'
+import { SvgGlyphUpload } from '@/shared/icons'
 
-import { EmptyUploadsView } from './EmptyUploadsView'
 import { StyledText, UploadsContainer } from './MyUploadsView.style'
 import { UploadStatusGroup } from './UploadStatusGroup'
 import { UploadStatusGroupSkeletonLoader } from './UploadStatusGroup/UploadStatusGroupSkeletonLoader'
@@ -49,7 +51,15 @@ export const MyUploadsView: React.FC = () => {
       ) : hasUploads ? (
         groupedUploadsState.map((files) => <UploadStatusGroup key={files[0].parentObject.id} uploads={files} />)
       ) : (
-        <EmptyUploadsView />
+        <EmptyFallback
+          title="No ongoing uploads"
+          subtitle="You will see status of each ongoing upload here."
+          button={
+            <Button icon={<SvgGlyphUpload />} variant="secondary" size="large" to={absoluteRoutes.studio.editVideo()}>
+              Upload video
+            </Button>
+          }
+        />
       )}
     </UploadsContainer>
   )

+ 0 - 132
src/views/studio/MyVideosView/EmptyVideosView.tsx

@@ -1,132 +0,0 @@
-import styled from '@emotion/styled'
-import React from 'react'
-
-import { absoluteRoutes } from '@/config/routes'
-import { Button, Text } from '@/shared/components'
-import { SvgGlyphAddVideo } from '@/shared/icons'
-import { SvgEmptyVideosIllustration, SvgTheaterMaskIllustration } from '@/shared/illustrations'
-import { colors, media, sizes } from '@/shared/theme'
-
-// for when there is absolutely no videos available
-export const EmptyVideosView: React.FC = () => {
-  return (
-    <ContainerView>
-      <InnerContainerView>
-        <MessageView>
-          <Text variant="h3">Add your first video</Text>
-          <Subtitle variant="body2">
-            No videos uploaded yet. Start publishing by adding your first video to Joystream.
-          </Subtitle>
-        </MessageView>
-        <div>
-          <Button icon={<SvgGlyphAddVideo />} to={absoluteRoutes.studio.editVideo()}>
-            Upload video
-          </Button>
-        </div>
-      </InnerContainerView>
-      <StyledWEmptyIllustration />
-    </ContainerView>
-  )
-}
-
-const Subtitle = styled(Text)`
-  margin-top: 8px;
-  color: ${colors.gray[300]};
-`
-
-const StyledWEmptyIllustration = styled(SvgEmptyVideosIllustration)`
-  margin: 0 auto;
-  grid-row-start: 1;
-  width: 100%;
-  max-height: 50vh;
-  max-width: 75vw;
-
-  ${media.medium} {
-    max-width: initial;
-    grid-row-start: initial;
-    max-height: 60vh;
-  }
-
-  ${media.xlarge} {
-    max-height: 70vh;
-  }
-
-  ${media.xxlarge} {
-    transform: scale(1.2);
-  }
-`
-
-const ContainerView = styled.div`
-  display: grid;
-  padding: 0 0 ${sizes(10)} 0;
-
-  ${media.medium} {
-    grid-template-columns: auto 1fr;
-    margin: ${sizes(20)} auto 0;
-  }
-`
-
-const InnerContainerView = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-
-  ${media.medium} {
-    align-items: initial;
-  }
-`
-
-const MessageView = styled.div`
-  display: flex;
-  flex-direction: column;
-  text-align: center;
-  margin-top: ${sizes(8)};
-  margin-bottom: ${sizes(8)};
-  max-width: 450px;
-
-  ${media.medium} {
-    text-align: left;
-    margin-top: ${sizes(12)};
-  }
-`
-
-// for tabs
-export const EmptyVideos: React.FC<{ text: string }> = ({ text }) => {
-  return (
-    <Container>
-      <SvgTheaterMaskIllustration />
-      <Message>
-        <Text secondary variant="body2">
-          {text}
-        </Text>
-        <Button icon={<SvgGlyphAddVideo />} to={absoluteRoutes.studio.editVideo()} size="large">
-          Upload video
-        </Button>
-      </Message>
-    </Container>
-  )
-}
-
-const Container = styled.div`
-  display: grid;
-  place-items: center;
-
-  > svg {
-    max-width: 100%;
-  }
-  ${media.small} {
-    margin: ${sizes(20)} auto 0;
-  }
-`
-
-const Message = styled.div`
-  position: relative;
-  top: -256px;
-  display: grid;
-  gap: ${sizes(4)};
-  justify-items: center;
-  text-align: center;
-  margin-top: 48px;
-  margin-bottom: ${sizes(4)};
-`

+ 36 - 9
src/views/studio/MyVideosView/MyVideosView.tsx

@@ -16,9 +16,9 @@ import {
   useEditVideoSheet,
   useSnackbar,
 } from '@/providers'
-import { Grid, Pagination, Select, Tabs, Text } from '@/shared/components'
+import { Button, EmptyFallback, Grid, Pagination, Select, Tabs, Text } from '@/shared/components'
+import { SvgGlyphUpload } from '@/shared/icons'
 
-import { EmptyVideos, EmptyVideosView } from './EmptyVideosView'
 import {
   PaginationContainer,
   SortContainer,
@@ -254,7 +254,15 @@ export const MyVideosView = () => {
       <ViewContainer>
         <Text variant="h2">My videos</Text>
         {hasNoVideos ? (
-          <EmptyVideosView />
+          <EmptyFallback
+            title="Add your first video"
+            subtitle="No videos uploaded yet. Start publishing by adding your first video to Joystream."
+            button={
+              <Button icon={<SvgGlyphUpload />} to={absoluteRoutes.studio.editVideo()} variant="secondary" size="large">
+                Upload video
+              </Button>
+            }
+          />
         ) : (
           <>
             <TabsContainer>
@@ -285,15 +293,34 @@ export const MyVideosView = () => {
             </Grid>
             {((isDraftTab && drafts.length === 0) ||
               (!isDraftTab && !loading && totalCount === 0 && (!videos || videos.length === 0))) && (
-              <EmptyVideos
-                text={
+              <EmptyFallback
+                title={
+                  currentTabName === 'All Videos'
+                    ? 'No videos yet'
+                    : currentTabName === 'Public'
+                    ? 'No public videos yet'
+                    : currentTabName === 'Drafts'
+                    ? 'No drafts here yet'
+                    : 'No unlisted videos here yet'
+                }
+                subtitle={
                   currentTabName === 'All Videos'
-                    ? "You don't have any published videos at the moment"
+                    ? null
                     : currentTabName === 'Public'
-                    ? "You don't have any public videos at the moment"
+                    ? 'Videos published with "Public" privacy setting will show up here.'
                     : currentTabName === 'Drafts'
-                    ? "You don't have any drafts at the moment"
-                    : "You don't have any unlisted videos at the moment"
+                    ? "Each video that hasn't been published yet will be available here as a draft."
+                    : 'Videos published with "Unlisted" privacy setting will show up here.'
+                }
+                button={
+                  <Button
+                    icon={<SvgGlyphUpload />}
+                    to={absoluteRoutes.studio.editVideo()}
+                    variant="secondary"
+                    size="large"
+                  >
+                    Upload video
+                  </Button>
                 }
               />
             )}

+ 11 - 1
src/views/viewer/ChannelView/ChannelView.tsx

@@ -19,7 +19,7 @@ import {
 import { LimitedWidthContainer, VideoTile, ViewWrapper } from '@/components'
 import { SORT_OPTIONS } from '@/config/sorting'
 import { AssetType, useAsset, useDialog, usePersonalDataStore } from '@/providers'
-import { ChannelCover, Grid, Pagination, Select, Text } from '@/shared/components'
+import { ChannelCover, EmptyFallback, Grid, Pagination, Select, Text } from '@/shared/components'
 import { SvgGlyphCheck, SvgGlyphPlus, SvgGlyphSearch } from '@/shared/icons'
 import { transitions } from '@/shared/theme'
 import { Logger } from '@/utils/logger'
@@ -68,6 +68,7 @@ export const ChannelView: React.FC = () => {
     searchInputRef,
     search,
     errorSearch,
+    searchQuery,
   } = useSearchVideos({ id })
   const { followChannel } = useFollowChannel()
   const { unfollowChannel } = useUnfollowChannel()
@@ -221,6 +222,12 @@ export const ChannelView: React.FC = () => {
         return (
           <>
             <VideoSection className={transitions.names.slide}>
+              {!videosWithPlaceholders.length && isSearching && (
+                <EmptyFallback title={`No videos matching "${searchQuery}" query found`} variant="small" />
+              )}
+              {!videosWithPlaceholders.length && !isSearching && (
+                <EmptyFallback title="No videos on this channel" variant="small" />
+              )}
               <Grid maxColumns={null} onResize={handleOnResizeGrid}>
                 {videosWithPlaceholders.map((video, idx) => (
                   <VideoTile key={idx} id={video.id} showChannel={false} />
@@ -338,10 +345,12 @@ type UseSearchVideosParams = {
 const useSearchVideos = ({ id }: UseSearchVideosParams) => {
   const [isSearchInputOpen, setIsSearchingInputOpen] = useState(false)
   const [isSearching, setIsSearching] = useState(false)
+  const [searchQuery, setSearchQuery] = useState('')
   const [searchVideo, { loading: loadingSearch, data: searchData, error: errorSearch }] = useSearchLazyQuery()
   const searchInputRef = useRef<HTMLInputElement>(null)
   const search = useCallback(
     (searchQuery: string) => {
+      setSearchQuery(searchQuery)
       searchVideo({
         variables: {
           text: searchQuery,
@@ -373,6 +382,7 @@ const useSearchVideos = ({ id }: UseSearchVideosParams) => {
     isSearching,
     setIsSearching,
     searchInputRef,
+    searchQuery,
   }
 }
 

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