Переглянути джерело

browse view -> videos view (#211)

* personalisation

* change BrowseView to VideosView, change Discover to Videos in sidebar

* change background colors for category picker, edit scrollIntoView for topics in Videos view

* add fix scrollIntoView for Topics in Videos view

* add comment for Topics... text padding
mikkio 4 роки тому
батько
коміт
c497595b84

+ 1 - 1
src/config/routes.ts

@@ -3,6 +3,6 @@ export default {
   video: (id = ':id') => `/video/${id}`,
   search: (searchStr = ':search') => `/search/${searchStr}`,
   channel: (id = ':id') => `/channel/${id}`,
-  browse: () => '/browse',
+  videos: () => '/videos',
   channels: () => '/channels',
 }

+ 23 - 7
src/mocking/server/data.ts

@@ -1,7 +1,14 @@
 import { ModelInstance } from 'miragejs/-types'
 import faker from 'faker'
 
-import { mockCategories, mockChannels, mockVideos, mockVideosMedia, mockLicenses } from '@/mocking/data'
+import {
+  mockCategories,
+  mockChannels,
+  mockVideos,
+  mockVideosMedia,
+  mockLicenses,
+  FEATURED_VIDEOS_INDEXES,
+} from '@/mocking/data'
 import { AllChannelFields } from '@/api/queries/__generated__/AllChannelFields'
 import { CategoryFields } from '@/api/queries/__generated__/CategoryFields'
 import {
@@ -59,10 +66,15 @@ export const createMockData = (server: MirageJSServer) => {
     })
     return model
   })
-  mockVideos.forEach((video, idx) => {
+  const videos = mockVideos.map((video, idx) => {
     const mediaIndex = idx % mockVideosMedia.length
 
-    server.schema.create('Video', {
+    server.create('EntityViewsInfo', {
+      id: video.id,
+      views: video.views,
+    })
+
+    return server.schema.create('Video', {
       ...video,
       views: undefined,
       duration: mockVideosMedia[mediaIndex].duration,
@@ -71,12 +83,16 @@ export const createMockData = (server: MirageJSServer) => {
       media: videoMedias[mediaIndex],
       license: licenseEntities[idx % licenseEntities.length],
     })
+  })
 
-    server.create('EntityViewsInfo', {
-      id: video.id,
-      views: video.views,
+  videos
+    .filter((_, idx) => FEATURED_VIDEOS_INDEXES.includes(idx))
+    .forEach((video) => {
+      server.schema.create('FeaturedVideo', {
+        id: faker.random.uuid(),
+        video,
+      })
     })
-  })
 
   createCoverVideoData(server, categories)
 }

+ 1 - 1
src/mocking/server/index.ts

@@ -35,7 +35,7 @@ createServer({
           video: videoResolver,
           videosConnection: videosResolver,
           coverVideo: coverVideoResolver,
-          featured_videos: featuredVideosResolver,
+          featuredVideos: featuredVideosResolver,
           channel: channelResolver,
           channelsConnection: channelsResolver,
           search: searchResolver,

+ 13 - 4
src/mocking/server/resolvers.ts

@@ -1,5 +1,4 @@
 import { mirageGraphQLFieldResolver } from '@miragejs/graphql'
-import { FEATURED_VIDEOS_INDEXES } from '@/mocking/data'
 import { Search_search, SearchVariables } from '@/api/queries/__generated__/Search'
 import { VideoFields } from '@/api/queries/__generated__/VideoFields'
 import {
@@ -27,6 +26,10 @@ type VideoQueryArgs = {
   } | null
 }
 
+type FeaturedVideosQueryArgs = {
+  orderBy?: string
+}
+
 type UniqueArgs = {
   where: { id: string }
 }
@@ -79,9 +82,15 @@ export const coverVideoResolver: QueryResolver<never, GetCoverVideo_coverVideo>
   return coverVideo
 }
 
-export const featuredVideosResolver: QueryResolver<object, VideoFields[]> = (...params) => {
-  const videos = mirageGraphQLFieldResolver(...params) as VideoFields[]
-  return videos.filter((_, idx) => FEATURED_VIDEOS_INDEXES.includes(idx))
+export const featuredVideosResolver: QueryResolver<FeaturedVideosQueryArgs, VideoFields[]> = (
+  obj,
+  args,
+  context,
+  info
+) => {
+  delete args.orderBy
+  const videos = mirageGraphQLFieldResolver(obj, args, context, info) as VideoFields[]
+  return videos
 }
 
 export const channelResolver: QueryResolver<UniqueArgs, AllChannelFields> = (obj, args, context, info) => {

+ 15 - 4
src/shared/components/CategoryPicker/CategoryPicker.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useRef } from 'react'
+import React from 'react'
 import { Container, StyledToggleButton, StyledPlaceholder } from './CategoryPicker.style'
 import { CategoryFields } from '@/api/queries/__generated__/CategoryFields'
 
@@ -6,10 +6,16 @@ type CategoryPickerProps = {
   categories?: CategoryFields[]
   selectedCategoryId: string | null
   loading?: boolean
-  onChange: (category: CategoryFields) => void
+  onChange: (categoryId: string | null) => void
   className?: string
 }
 
+export const ALL_CATEGORY = {
+  __typename: 'Category',
+  id: null,
+  name: 'All',
+}
+
 const CATEGORY_PLACEHOLDER_WIDTHS = [80, 170, 120, 110, 80, 170, 120]
 
 const CategoryPicker: React.FC<CategoryPickerProps> = ({
@@ -19,18 +25,23 @@ const CategoryPicker: React.FC<CategoryPickerProps> = ({
   onChange,
   className,
 }) => {
+  const displayedCategories = [ALL_CATEGORY, ...(categories || [])]
+
+  const handleCategoryChange = (categoryId: string | null) => {
+    onChange(categoryId === ALL_CATEGORY.id ? null : categoryId)
+  }
   const content =
     !categories || loading
       ? CATEGORY_PLACEHOLDER_WIDTHS.map((width, idx) => (
           <StyledPlaceholder key={`placeholder-${idx}`} width={width} height="48px" />
         ))
-      : categories.map((category) => (
+      : displayedCategories.map((category) => (
           <StyledToggleButton
             key={category.id}
             controlled
             toggled={category.id === selectedCategoryId}
             variant="secondary"
-            onClick={() => onChange(category)}
+            onClick={() => handleCategoryChange(category.id)}
           >
             {category.name}
           </StyledToggleButton>

+ 2 - 0
src/shared/icons/index.ts

@@ -22,6 +22,7 @@ export { ReactComponent as SoundOn } from './sound-on.svg'
 export { ReactComponent as SoundOff } from './sound-off.svg'
 export { ReactComponent as Search } from './search.svg'
 export { ReactComponent as Times } from './times.svg'
+export { ReactComponent as Videos } from './videos.svg'
 
 const icons = [
   'bars',
@@ -48,6 +49,7 @@ const icons = [
   'sound-on',
   'sound-off',
   'times',
+  'videos',
 ] as const
 
 export type IconType = typeof icons[number]

+ 4 - 0
src/shared/icons/videos.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="4" y="3" width="16" height="2" fill="currentColor"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M23 6H1V20H23V6ZM15 13L10 16V10L15 13Z" fill="currentColor"/>
+</svg>

+ 0 - 71
src/views/BrowseView/BrowseView.tsx

@@ -1,71 +0,0 @@
-import React, { useState, useRef } from 'react'
-
-import { RouteComponentProps } from '@reach/router'
-import { ErrorBoundary } from '@sentry/react'
-import { useQuery } from '@apollo/client'
-import { useInView } from 'react-intersection-observer'
-
-import { ErrorFallback, BackgroundPattern } from '@/components'
-import { Text } from '@/shared/components'
-import { TOP_NAVBAR_HEIGHT } from '@/components/TopNavbar'
-import {
-  StyledCategoryPicker,
-  Container,
-  StyledInfiniteVideoGrid,
-  IntersectionTarget,
-  Header,
-  GRID_TOP_PADDING,
-} from './BrowseView.style'
-import { GET_CATEGORIES } from '@/api/queries'
-import { GetCategories } from '@/api/queries/__generated__/GetCategories'
-import { CategoryFields } from '@/api/queries/__generated__/CategoryFields'
-
-const BrowseView: React.FC<RouteComponentProps> = () => {
-  const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
-  const { loading: categoriesLoading, data: categoriesData, error: categoriesError } = useQuery<GetCategories>(
-    GET_CATEGORIES,
-    {
-      onCompleted: (data) => {
-        handleCategoryChange(data.categories[0], false)
-      },
-    }
-  )
-
-  const headerRef = useRef<HTMLHeadingElement>(null)
-  const { ref: targetRef, inView } = useInView({
-    rootMargin: `-${TOP_NAVBAR_HEIGHT - GRID_TOP_PADDING}px 0px 0px`,
-  })
-  const handleCategoryChange = (category: CategoryFields, scrollTop = true) => {
-    setSelectedCategoryId(category.id)
-    if (headerRef.current && scrollTop) {
-      headerRef.current.scrollIntoView({ block: 'end', inline: 'nearest', behavior: 'smooth' })
-    }
-  }
-
-  if (categoriesError) {
-    throw categoriesError
-  }
-
-  return (
-    <Container>
-      <BackgroundPattern />
-      <Header variant="hero" ref={headerRef}>
-        Browse
-      </Header>
-      <Text variant="h5">Topics that may interest you</Text>
-      <IntersectionTarget ref={targetRef} />
-      <StyledCategoryPicker
-        categories={categoriesData?.categories}
-        loading={categoriesLoading}
-        selectedCategoryId={selectedCategoryId}
-        onChange={handleCategoryChange}
-        isAtTop={inView}
-      />
-      <ErrorBoundary fallback={ErrorFallback}>
-        <StyledInfiniteVideoGrid categoryId={selectedCategoryId || undefined} ready={!!categoriesData?.categories} />
-      </ErrorBoundary>
-    </Container>
-  )
-}
-
-export default BrowseView

+ 0 - 3
src/views/BrowseView/index.ts

@@ -1,3 +0,0 @@
-import BrowseView from './BrowseView'
-
-export default BrowseView

+ 1 - 1
src/views/ChannelsView/ChannelsView.tsx

@@ -1,5 +1,5 @@
 import React from 'react'
-import { Container, Header } from '@/views/BrowseView/BrowseView.style'
+import { Container, Header } from '@/views/VideosView/VideosView.style'
 import { BackgroundPattern, InfiniteChannelGrid } from '@/components'
 
 const ChannelsView: React.FC = () => {

+ 1 - 17
src/views/HomeView.tsx

@@ -5,7 +5,7 @@ import { useQuery } from '@apollo/client'
 import { ErrorBoundary } from '@sentry/react'
 
 import { ErrorFallback, CoverVideo, InfiniteVideoGrid, VideoGallery } from '@/components'
-import { GET_FEATURED_VIDEOS, GET_NEWEST_VIDEOS } from '@/api/queries'
+import { GET_NEWEST_VIDEOS } from '@/api/queries'
 import { GetFeaturedVideos } from '@/api/queries/__generated__/GetFeaturedVideos'
 import { GetNewestVideos, GetNewestVideosVariables } from '@/api/queries/__generated__/GetNewestVideos'
 
@@ -21,20 +21,10 @@ const HomeView: React.FC<RouteComponentProps> = () => {
     variables: { first: NEWEST_VIDEOS_COUNT },
     notifyOnNetworkStatusChange: true,
   })
-  const {
-    loading: featuredVideosLoading,
-    data: featuredVideosData,
-    error: featuredVideosError,
-    refetch: refetchFeaturedVideos,
-  } = useQuery<GetFeaturedVideos>(GET_FEATURED_VIDEOS, {
-    notifyOnNetworkStatusChange: true,
-  })
 
   const newestVideos = videosData?.videosConnection.edges.slice(0, NEWEST_VIDEOS_COUNT).map((e) => e.node)
-  const featuredVideos = featuredVideosData?.featuredVideos.map((featuredVideo) => featuredVideo.video)
 
   const hasNewestVideosError = newestVideosError && !newestVideosLoading
-  const hasFeaturedVideosError = featuredVideosError && !featuredVideosLoading
 
   return (
     <>
@@ -46,12 +36,6 @@ const HomeView: React.FC<RouteComponentProps> = () => {
           <ErrorFallback error={newestVideosError} resetError={() => refetchNewestVideos()} />
         )}
 
-        {!hasFeaturedVideosError ? (
-          <VideoGallery title="Featured videos" loading={featuredVideosLoading} videos={featuredVideos} />
-        ) : (
-          <ErrorFallback error={featuredVideosError} resetError={() => refetchFeaturedVideos()} />
-        )}
-
         <ErrorBoundary fallback={ErrorFallback}>
           <StyledInfiniteVideoGrid title="More videos" skipCount={NEWEST_VIDEOS_COUNT} />
         </ErrorBoundary>

+ 6 - 6
src/views/LayoutWithRouting.tsx

@@ -5,7 +5,7 @@ import { ErrorBoundary } from '@sentry/react'
 
 import { GlobalStyle, SideNavbar } from '@/shared/components'
 import { TopNavbar, ViewErrorFallback } from '@/components'
-import { HomeView, VideoView, SearchView, ChannelView, BrowseView, ChannelsView } from '@/views'
+import { HomeView, VideoView, SearchView, ChannelView, VideosView, ChannelsView } from '@/views'
 import routes from '@/config/routes'
 import { globalStyles } from '@/styles/global'
 import { breakpoints, sizes } from '@/shared/theme'
@@ -19,10 +19,10 @@ const SIDENAVBAR_ITEMS: NavItemType[] = [
     to: routes.index(),
   },
   {
-    icon: 'binocular',
-    iconFilled: 'binocular-fill',
-    name: 'Discover',
-    to: routes.browse(),
+    icon: 'videos',
+    iconFilled: 'videos',
+    name: 'Videos',
+    to: routes.videos(),
   },
   {
     icon: 'channels',
@@ -67,7 +67,7 @@ const LayoutWithRouting: React.FC = () => {
           <Route default Component={HomeView} />
           <Route path={routes.video()} Component={VideoView} />
           <Route path={routes.search()} Component={SearchView} />
-          <Route path={routes.browse()} Component={BrowseView} />
+          <Route path={routes.videos()} Component={VideosView} />
           <Route path={routes.channels()} Component={ChannelsView} />
           <Route path={routes.channel()} Component={ChannelView} />
         </Router>

+ 6 - 1
src/views/BrowseView/BrowseView.style.ts → src/views/VideosView/VideosView.style.ts

@@ -13,6 +13,11 @@ export const GRID_TOP_PADDING = sizes(2, true)
 export const Header = styled(Text)`
   margin: 0 0 ${sizes(10)} 0;
 `
+export const StyledText = styled(Text)`
+  /* Navbar Height padding so the text is not overlapped by Navbar when scrollIntoview */
+  padding-top: ${TOP_NAVBAR_HEIGHT}px;
+`
+
 export const StyledCategoryPicker = styled(CategoryPicker)<IsAtTop>`
   z-index: ${zIndex.overlay};
   position: sticky;
@@ -20,7 +25,7 @@ export const StyledCategoryPicker = styled(CategoryPicker)<IsAtTop>`
   top: ${TOP_NAVBAR_HEIGHT}px;
   padding: ${sizes(5)} var(--global-horizontal-padding) ${sizes(2)};
   margin: 0 calc(-1 * var(--global-horizontal-padding));
-  background-color: ${(props) => (props.isAtTop ? colors.transparent : colors.black)};
+  background-color: ${colors.black};
   border-bottom: 1px solid ${(props) => (props.isAtTop ? colors.black : colors.gray[800])};
   transition: background-color ${transitions.timings.regular} ${transitions.easing};
 `

+ 82 - 0
src/views/VideosView/VideosView.tsx

@@ -0,0 +1,82 @@
+import React, { useState, useRef } from 'react'
+
+import { RouteComponentProps } from '@reach/router'
+import { ErrorBoundary } from '@sentry/react'
+import { useQuery } from '@apollo/client'
+import { useInView } from 'react-intersection-observer'
+
+import { ErrorFallback, BackgroundPattern, VideoGallery } from '@/components'
+import { TOP_NAVBAR_HEIGHT } from '@/components/TopNavbar'
+import {
+  StyledText,
+  StyledCategoryPicker,
+  Container,
+  StyledInfiniteVideoGrid,
+  IntersectionTarget,
+  Header,
+  GRID_TOP_PADDING,
+} from './VideosView.style'
+import { GET_CATEGORIES, GET_FEATURED_VIDEOS } from '@/api/queries'
+import { GetCategories } from '@/api/queries/__generated__/GetCategories'
+import { GetFeaturedVideos } from '@/api/queries/__generated__/GetFeaturedVideos'
+
+const VideosView: React.FC<RouteComponentProps> = () => {
+  const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
+  const { loading: categoriesLoading, data: categoriesData, error: categoriesError } = useQuery<GetCategories>(
+    GET_CATEGORIES
+  )
+  const {
+    loading: featuredVideosLoading,
+    data: featuredVideosData,
+    error: featuredVideosError,
+    refetch: refetchFeaturedVideos,
+  } = useQuery<GetFeaturedVideos>(GET_FEATURED_VIDEOS, {
+    notifyOnNetworkStatusChange: true,
+  })
+
+  const topicsRef = useRef<HTMLHeadingElement>(null)
+  const { ref: targetRef, inView } = useInView({
+    rootMargin: `-${TOP_NAVBAR_HEIGHT - GRID_TOP_PADDING}px 0px 0px`,
+  })
+  const handleCategoryChange = (categoryId: string | null, scrollTop = true) => {
+    setSelectedCategoryId(categoryId)
+    if (topicsRef.current && scrollTop) {
+      setTimeout(() => {
+        topicsRef.current?.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' })
+      })
+    }
+  }
+
+  if (categoriesError) {
+    throw categoriesError
+  }
+  const featuredVideos = featuredVideosData?.featuredVideos.map((featuredVideo) => featuredVideo.video)
+  const hasFeaturedVideosError = featuredVideosError && !featuredVideosLoading
+  return (
+    <Container>
+      <BackgroundPattern />
+      <Header variant="hero">Videos</Header>
+      {!hasFeaturedVideosError ? (
+        <VideoGallery title="Featured" loading={featuredVideosLoading} videos={featuredVideos} />
+      ) : (
+        <ErrorFallback error={featuredVideosError} resetError={() => refetchFeaturedVideos()} />
+      )}
+      <StyledText ref={topicsRef} variant="h5">
+        Topics that may interest you
+      </StyledText>
+      <IntersectionTarget ref={targetRef} />
+      <StyledCategoryPicker
+        categories={categoriesData?.categories}
+        loading={categoriesLoading}
+        selectedCategoryId={selectedCategoryId}
+        onChange={handleCategoryChange}
+        isAtTop={inView}
+      />
+      <ErrorBoundary fallback={ErrorFallback}>
+        <StyledInfiniteVideoGrid categoryId={selectedCategoryId || undefined} ready={!!categoriesData?.categories} />
+      </ErrorBoundary>
+    </Container>
+  )
+}
+
+export default VideosView

+ 3 - 0
src/views/VideosView/index.ts

@@ -0,0 +1,3 @@
+import VideosView from './VideosView'
+
+export default VideosView

+ 2 - 2
src/views/index.ts

@@ -2,7 +2,7 @@ import HomeView from './HomeView'
 import VideoView from './VideoView'
 import SearchView from './SearchView'
 import ChannelView from './ChannelView'
-import BrowseView from './BrowseView'
+import VideosView from './VideosView'
 import ChannelsView from './ChannelsView'
 
-export { HomeView, VideoView, SearchView, ChannelView, BrowseView, ChannelsView }
+export { HomeView, VideoView, SearchView, ChannelView, VideosView, ChannelsView }