瀏覽代碼

Add popular channels section to popular view (#1125)

* Add popular channels section to popular view

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

+ 43 - 0
src/api/hooks/channel.ts

@@ -8,6 +8,8 @@ import {
   GetChannelQuery,
   GetChannelsQuery,
   GetChannelsQueryVariables,
+  GetMostViewedChannelsAllTimeQuery,
+  GetMostViewedChannelsAllTimeQueryVariables,
   GetMostViewedChannelsQuery,
   GetMostViewedChannelsQueryVariables,
   GetVideoCountQuery,
@@ -16,6 +18,7 @@ import {
   useGetBasicChannelQuery,
   useGetChannelQuery,
   useGetChannelsQuery,
+  useGetMostViewedChannelsAllTimeQuery,
   useGetMostViewedChannelsQuery,
   useGetVideoCountQuery,
   useUnfollowChannelMutation,
@@ -166,3 +169,43 @@ export const useMostViewedChannels = (
     ...rest,
   }
 }
+
+type MostPopularChannelsAllTimeOpts = QueryHookOptions<GetMostViewedChannelsAllTimeQuery>
+export const useMostViewedChannelsAllTimeIds = (
+  variables?: GetMostViewedChannelsAllTimeQueryVariables,
+  opts?: MostPopularChannelsAllTimeOpts
+) => {
+  const { data, ...rest } = useGetMostViewedChannelsAllTimeQuery({ ...opts, variables })
+  return {
+    mostViewedChannelsAllTime: data?.mostViewedChannelsAllTime,
+    ...rest,
+  }
+}
+
+export const useMostViewedChannelsAllTime = (
+  variables?: GetMostViewedChannelsAllTimeQueryVariables,
+  opts?: MostPopularChannelsOpts
+) => {
+  const { mostViewedChannelsAllTime } = useMostViewedChannelsAllTimeIds(variables, opts)
+
+  const mostViewedChannelsIds = useMemo(() => {
+    if (mostViewedChannelsAllTime) {
+      return mostViewedChannelsAllTime.map((item) => item.id)
+    }
+    return null
+  }, [mostViewedChannelsAllTime])
+
+  const { channels, ...rest } = useChannels(
+    {
+      where: {
+        id_in: mostViewedChannelsIds,
+      },
+    },
+    { skip: !mostViewedChannelsIds }
+  )
+
+  return {
+    channels,
+    ...rest,
+  }
+}

+ 6 - 0
src/api/queries/__generated__/baseTypes.generated.ts

@@ -304,6 +304,8 @@ export type Query = {
   memberships: Array<Membership>
   /** Get list of channels with most views in given period */
   mostViewedChannels?: Maybe<Array<EntityViewsInfo>>
+  /** Get list of channels with most views in given period */
+  mostViewedChannelsAllTime?: Maybe<Array<EntityViewsInfo>>
   /** Get list of most viewed videos in given period */
   mostViewedVideos?: Maybe<Array<EntityViewsInfo>>
   search: Array<SearchFtsOutput>
@@ -374,6 +376,10 @@ export type QueryMostViewedChannelsArgs = {
   limit?: Maybe<Scalars['Int']>
 }
 
+export type QueryMostViewedChannelsAllTimeArgs = {
+  limit?: Maybe<Scalars['Int']>
+}
+
 export type QueryMostViewedVideosArgs = {
   period: Scalars['Int']
   limit?: Maybe<Scalars['Int']>

+ 61 - 0
src/api/queries/__generated__/channels.generated.tsx

@@ -152,6 +152,15 @@ export type GetMostViewedChannelsQuery = {
   mostViewedChannels?: Types.Maybe<Array<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>>
 }
 
+export type GetMostViewedChannelsAllTimeQueryVariables = Types.Exact<{
+  limit: Types.Scalars['Int']
+}>
+
+export type GetMostViewedChannelsAllTimeQuery = {
+  __typename?: 'Query'
+  mostViewedChannelsAllTime?: Types.Maybe<Array<{ __typename?: 'EntityViewsInfo'; id: string; views: number }>>
+}
+
 export const BasicChannelFieldsFragmentDoc = gql`
   fragment BasicChannelFields on Channel {
     id
@@ -713,3 +722,55 @@ export type GetMostViewedChannelsQueryResult = Apollo.QueryResult<
   GetMostViewedChannelsQuery,
   GetMostViewedChannelsQueryVariables
 >
+export const GetMostViewedChannelsAllTimeDocument = gql`
+  query GetMostViewedChannelsAllTime($limit: Int!) {
+    mostViewedChannelsAllTime(limit: $limit) {
+      id
+      views
+    }
+  }
+`
+
+/**
+ * __useGetMostViewedChannelsAllTimeQuery__
+ *
+ * To run a query within a React component, call `useGetMostViewedChannelsAllTimeQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetMostViewedChannelsAllTimeQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetMostViewedChannelsAllTimeQuery({
+ *   variables: {
+ *      limit: // value for 'limit'
+ *   },
+ * });
+ */
+export function useGetMostViewedChannelsAllTimeQuery(
+  baseOptions: Apollo.QueryHookOptions<GetMostViewedChannelsAllTimeQuery, GetMostViewedChannelsAllTimeQueryVariables>
+) {
+  return Apollo.useQuery<GetMostViewedChannelsAllTimeQuery, GetMostViewedChannelsAllTimeQueryVariables>(
+    GetMostViewedChannelsAllTimeDocument,
+    baseOptions
+  )
+}
+export function useGetMostViewedChannelsAllTimeLazyQuery(
+  baseOptions?: Apollo.LazyQueryHookOptions<
+    GetMostViewedChannelsAllTimeQuery,
+    GetMostViewedChannelsAllTimeQueryVariables
+  >
+) {
+  return Apollo.useLazyQuery<GetMostViewedChannelsAllTimeQuery, GetMostViewedChannelsAllTimeQueryVariables>(
+    GetMostViewedChannelsAllTimeDocument,
+    baseOptions
+  )
+}
+export type GetMostViewedChannelsAllTimeQueryHookResult = ReturnType<typeof useGetMostViewedChannelsAllTimeQuery>
+export type GetMostViewedChannelsAllTimeLazyQueryHookResult = ReturnType<
+  typeof useGetMostViewedChannelsAllTimeLazyQuery
+>
+export type GetMostViewedChannelsAllTimeQueryResult = Apollo.QueryResult<
+  GetMostViewedChannelsAllTimeQuery,
+  GetMostViewedChannelsAllTimeQueryVariables
+>

+ 7 - 0
src/api/queries/channels.graphql

@@ -130,3 +130,10 @@ query GetMostViewedChannels($viewedWithinDays: Int!, $limit: Int) {
     views
   }
 }
+
+query GetMostViewedChannelsAllTime($limit: Int!) {
+  mostViewedChannelsAllTime(limit: $limit) {
+    id
+    views
+  }
+}

+ 5 - 0
src/api/schemas/orion.graphql

@@ -70,4 +70,9 @@ type Query {
   Get list of channels with most views in given period
   """
   mostViewedChannels(period: Int!, limit: Int): [EntityViewsInfo!]
+
+  """
+  Get list of channels with most views in given period
+  """
+  mostViewedChannelsAllTime(limit: Int): [EntityViewsInfo!]
 }

+ 48 - 16
src/components/InfiniteGrids/InfiniteChannelWithVideosGrid.tsx

@@ -11,9 +11,18 @@ import {
 import { ChannelWithVideos } from '@/components'
 import { useInfiniteGrid } from '@/components/InfiniteGrids/useInfiniteGrid'
 import { languages } from '@/config/languages'
-import { LoadMoreButton, Select, Text } from '@/shared/components'
+import { GridHeadingContainer, LoadMoreButton, Select } from '@/shared/components'
+import { SvgGlyphChevronRight } from '@/shared/icons'
 
-import { LanguageSelectWrapper, LoadMoreButtonWrapper, Separator, TitleWrapper } from './InfiniteGrid.style'
+import {
+  AdditionalLink,
+  LanguageSelectWrapper,
+  LoadMoreButtonWrapper,
+  Separator,
+  StyledSkeletonLoader,
+  Title,
+  TitleWrapper,
+} from './InfiniteGrid.style'
 
 type InfiniteChannelWithVideosGridProps = {
   onDemand?: boolean
@@ -22,6 +31,11 @@ type InfiniteChannelWithVideosGridProps = {
   isReady?: boolean
   className?: string
   languageSelector?: boolean
+  idIn?: string[] | null
+  additionalLink?: {
+    name: string
+    url: string
+  }
 }
 
 const INITIAL_ROWS = 3
@@ -34,6 +48,8 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
   isReady = true,
   className,
   languageSelector,
+  idIn = null,
+  additionalLink,
 }) => {
   const [selectedLanguage, setSelectedLanguage] = useState<string | null | undefined>(null)
   const [targetRowsCount, setTargetRowsCount] = useState(INITIAL_ROWS)
@@ -45,6 +61,7 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
   const queryVariables: { where: ChannelWhereInput } = {
     where: {
       ...(selectedLanguage ? { languageId_eq: selectedLanguage } : {}),
+      ...(idIn ? { id_in: idIn } : {}),
       isPublic_eq: true,
       isCensored_eq: false,
     },
@@ -56,7 +73,7 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
     GetChannelsConnectionQueryVariables
   >({
     query: GetChannelsConnectionDocument,
-    isReady: isReady && !!selectedLanguage,
+    isReady: languageSelector ? isReady && !!selectedLanguage : isReady,
     skipCount,
     orderBy: ChannelOrderByInput.CreatedAtAsc,
     queryVariables,
@@ -92,10 +109,10 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
 
   // Set initial language
   useEffect(() => {
-    if (mappedLanguages.length) {
+    if (mappedLanguages.length && languageSelector) {
       setSelectedLanguage(mappedLanguages.find((item) => item.name === 'English')?.value)
     }
-  }, [mappedLanguages])
+  }, [mappedLanguages, languageSelector])
 
   const onSelectLanguage = (value?: string | null) => {
     setTargetRowsCount(INITIAL_ROWS)
@@ -105,17 +122,32 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
   return (
     <section className={className}>
       <TitleWrapper>
-        {title && <Text variant="h4">{title}</Text>}
-        {languageSelector && (
-          <LanguageSelectWrapper>
-            <Select
-              items={mappedLanguages || []}
-              disabled={languagesLoading}
-              value={selectedLanguage}
-              size="regular"
-              onChange={onSelectLanguage}
-            />
-          </LanguageSelectWrapper>
+        {title && (
+          <GridHeadingContainer>
+            {!isReady ? <StyledSkeletonLoader height={23} width={250} /> : <Title variant="h4">{title}</Title>}
+            {languageSelector && (
+              <LanguageSelectWrapper>
+                <Select
+                  items={mappedLanguages || []}
+                  disabled={languagesLoading}
+                  value={selectedLanguage}
+                  size="regular"
+                  onChange={onSelectLanguage}
+                />
+              </LanguageSelectWrapper>
+            )}
+            {additionalLink && (
+              <AdditionalLink
+                to={additionalLink.url}
+                size="medium"
+                variant="secondary"
+                iconPlacement="right"
+                icon={<SvgGlyphChevronRight width={12} height={12} />}
+              >
+                {additionalLink.name}
+              </AdditionalLink>
+            )}
+          </GridHeadingContainer>
         )}
       </TitleWrapper>
       {itemsToShow.map((channel, idx) => (

+ 5 - 2
src/components/InfiniteGrids/InfiniteGrid.style.ts

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled'
 
-import { SkeletonLoader, Text } from '@/shared/components'
+import { Button, SkeletonLoader, Text } from '@/shared/components'
 import { colors, sizes } from '@/shared/theme'
 
 export const Title = styled(Text)`
@@ -17,7 +17,6 @@ export const LoadMoreButtonWrapper = styled.div`
 
 export const TitleWrapper = styled.div`
   display: flex;
-  border-bottom: 1px solid ${colors.gray[700]};
 `
 
 export const LanguageSelectWrapper = styled.div`
@@ -31,3 +30,7 @@ export const Separator = styled.div`
   margin-top: ${sizes(12)};
   margin-bottom: ${sizes(24)};
 `
+
+export const AdditionalLink = styled(Button)`
+  margin-left: auto;
+`

+ 20 - 18
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -7,11 +7,11 @@ import {
   GetVideosConnectionQueryVariables,
   VideoWhereInput,
 } from '@/api/queries'
-import { Button, Grid, GridHeadingContainer, LoadMoreButton } from '@/shared/components'
+import { Grid, GridHeadingContainer, LoadMoreButton } from '@/shared/components'
 import { SvgGlyphChevronRight } from '@/shared/icons'
 import { SentryLogger } from '@/utils/logs'
 
-import { LoadMoreButtonWrapper, StyledSkeletonLoader, Title } from './InfiniteGrid.style'
+import { AdditionalLink, LoadMoreButtonWrapper, StyledSkeletonLoader, Title, TitleWrapper } from './InfiniteGrid.style'
 import { useInfiniteGrid } from './useInfiniteGrid'
 
 import { VideoTile } from '../VideoTile'
@@ -169,22 +169,24 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   // Right now we'll make the first request and then right after another one based on the resized columns
   return (
     <section className={className}>
-      {title && (
-        <GridHeadingContainer>
-          {!ready ? <StyledSkeletonLoader height={23} width={250} /> : <Title variant="h4">{title}</Title>}
-          {additionalLink && (
-            <Button
-              to={additionalLink.url}
-              size="medium"
-              variant="secondary"
-              iconPlacement="right"
-              icon={<SvgGlyphChevronRight width={12} height={12} />}
-            >
-              {additionalLink.name}
-            </Button>
-          )}
-        </GridHeadingContainer>
-      )}
+      <TitleWrapper>
+        {title && (
+          <GridHeadingContainer>
+            {!ready ? <StyledSkeletonLoader height={23} width={250} /> : <Title variant="h4">{title}</Title>}
+            {additionalLink && (
+              <AdditionalLink
+                to={additionalLink.url}
+                size="medium"
+                variant="secondary"
+                iconPlacement="right"
+                icon={<SvgGlyphChevronRight />}
+              >
+                {additionalLink.name}
+              </AdditionalLink>
+            )}
+          </GridHeadingContainer>
+        )}
+      </TitleWrapper>
       <Grid onResize={(sizes) => setVideosPerRow(sizes.length)}>{gridContent}</Grid>
       {shouldShowLoadMoreButton && (
         <LoadMoreButtonWrapper>

+ 3 - 0
src/components/VideoGallery.tsx

@@ -30,6 +30,7 @@ type VideoGalleryProps = {
   onVideoClick?: (id: string) => void
   hasRanking?: boolean
   seeAllUrl?: string
+  className?: string
 }
 
 const PLACEHOLDERS_COUNT = 12
@@ -46,6 +47,7 @@ export const VideoGallery: React.FC<VideoGalleryProps> = ({
   onVideoNotFound,
   seeAllUrl,
   hasRanking = false,
+  className,
 }) => {
   const breakpoints = useMemo(() => {
     return breakpointsOfGrid({
@@ -91,6 +93,7 @@ export const VideoGallery: React.FC<VideoGalleryProps> = ({
       itemWidth={MIN_VIDEO_PREVIEW_WIDTH}
       dotsVisible
       seeAllUrl={seeAllUrl}
+      className={className}
     >
       {[...videos, ...placeholderItems]?.map((video, idx) => (
         <GalleryWrapper key={`${idx}-${video.id}`} hasRanking={hasRanking}>

+ 1 - 1
src/shared/components/CallToActionButton/CallToActionButton.style.ts

@@ -17,7 +17,7 @@ export const CallToActionWrapper = styled.div`
 
   ${media.medium} {
     display: grid;
-    grid-template-columns: auto auto auto;
+    grid-template-columns: 1fr 1fr 1fr;
     grid-column-gap: ${sizes(6)};
   }
 `

+ 1 - 1
src/shared/components/GridHeading/GridHeading.styles.ts

@@ -4,7 +4,7 @@ import { colors, sizes } from '@/shared/theme'
 
 export const GridHeadingContainer = styled.div`
   display: flex;
-  justify-content: space-between;
+  width: 100%;
   align-items: start;
   margin-bottom: ${sizes(10)};
   padding-bottom: ${sizes(4)};

+ 27 - 4
src/views/viewer/PopularView/PopularView.tsx

@@ -2,18 +2,27 @@ import styled from '@emotion/styled'
 import React, { FC } from 'react'
 
 import { useMostViewedVideos } from '@/api/hooks'
-import { VideoGallery, ViewWrapper } from '@/components'
+import { useMostViewedChannelsAllTimeIds } from '@/api/hooks'
+import { InfiniteChannelWithVideosGrid, VideoGallery, ViewWrapper } from '@/components'
 import { absoluteRoutes } from '@/config/routes'
 import { CallToActionButton, CallToActionWrapper, Text } from '@/shared/components'
 import { SvgNavChannels, SvgNavHome, SvgNavNew } from '@/shared/icons'
 import { sizes } from '@/shared/theme'
 
 export const PopularView: FC = () => {
+  const { mostViewedChannelsAllTime } = useMostViewedChannelsAllTimeIds({ limit: 15 })
+  const mostViewedChannelsAllTimeIds = mostViewedChannelsAllTime?.map((item) => item.id)
   const { videos, loading } = useMostViewedVideos({ viewedWithinDays: 30, limit: 10 })
   return (
-    <ViewWrapper>
+    <StyledViewWrapper>
       <Header variant="h2">Popular</Header>
-      <VideoGallery hasRanking title="Top 10 this month" videos={videos || []} loading={loading} />
+      <StyledVideoGallery hasRanking title="Top 10 this month" videos={videos || []} loading={loading} />
+      <StyledInfiniteChannelWithVideosGrid
+        title="Popular channels"
+        onDemand
+        idIn={mostViewedChannelsAllTimeIds}
+        additionalLink={{ name: 'Browse channels', url: absoluteRoutes.viewer.channels() }}
+      />
       <CallToActionWrapper>
         <CallToActionButton
           label="New & Noteworthy"
@@ -34,10 +43,24 @@ export const PopularView: FC = () => {
           icon={<SvgNavChannels />}
         />
       </CallToActionWrapper>
-    </ViewWrapper>
+    </StyledViewWrapper>
   )
 }
 
 const Header = styled(Text)`
   margin: ${sizes(16)} 0;
 `
+
+const StyledVideoGallery = styled(VideoGallery)`
+  margin-bottom: ${sizes(38)};
+`
+
+const StyledViewWrapper = styled(ViewWrapper)`
+  padding-bottom: ${sizes(10)};
+`
+
+const StyledInfiniteChannelWithVideosGrid = styled(InfiniteChannelWithVideosGrid)`
+  :not(:last-of-type) {
+    margin-bottom: ${sizes(38)};
+  }
+`