Browse Source

Add promising channels section (#1137)

* update channels query and cache

* create promising channels section

* cr fixes

* change sort function name

* naming
Bartosz Dryl 3 years ago
parent
commit
093c1f43bb

+ 11 - 1
src/api/client/cache.ts

@@ -7,6 +7,7 @@ import { parseISO } from 'date-fns'
 import {
   AllChannelFieldsFragment,
   AssetAvailability,
+  GetChannelsConnectionQueryVariables,
   GetVideosConnectionQueryVariables,
   Query,
   VideoConnection,
@@ -35,6 +36,15 @@ const getVideoKeyArgs = (args: GetVideosConnectionQueryVariables | null) => {
   return `${onlyCount}:${channelId}:${categoryId}:${channelIdIn}:${createdAtGte}:${isPublic}:${idEq}:${idIn}:${sorting}:${isFeatured}`
 }
 
+const getChannelKeyArgs = (args: Record<string, GetChannelsConnectionQueryVariables['where']> | null) => {
+  // make sure queries asking for a specific category are separated in cache
+  const languageId = args?.where?.languageId_eq || ''
+  const idIn = args?.where?.id_in || []
+  const orderBy = args?.orderBy || []
+
+  return `${languageId}:${idIn}:${orderBy}`
+}
+
 const createDateHandler = () => ({
   merge: (_: unknown, existingData: string | Date): Date => {
     if (typeof existingData !== 'string') {
@@ -88,7 +98,7 @@ const createCachedAvailabilityHandler = () => ({
 type CachePolicyFields<T extends string> = Partial<Record<T, FieldPolicy | FieldReadFunction>>
 
 const queryCacheFields: CachePolicyFields<keyof Query> = {
-  channelsConnection: relayStylePagination(),
+  channelsConnection: relayStylePagination(getChannelKeyArgs),
   videosConnection: {
     ...relayStylePagination(getVideoKeyArgs),
     read(

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

@@ -348,6 +348,7 @@ export type QueryChannelViewsArgs = {
 export type QueryChannelsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
+  orderBy?: Maybe<Array<ChannelOrderByInput>>
   where?: Maybe<ChannelWhereInput>
 }
 

+ 6 - 6
src/api/queries/__generated__/channels.generated.tsx

@@ -57,9 +57,9 @@ export type GetVideoCountQuery = {
 }
 
 export type GetChannelsQueryVariables = Types.Exact<{
-  offset?: Types.Maybe<Types.Scalars['Int']>
-  limit?: Types.Maybe<Types.Scalars['Int']>
   where?: Types.Maybe<Types.ChannelWhereInput>
+  limit?: Types.Maybe<Types.Scalars['Int']>
+  orderBy?: Types.Maybe<Array<Types.ChannelOrderByInput> | Types.ChannelOrderByInput>
 }>
 
 export type GetChannelsQuery = {
@@ -312,8 +312,8 @@ export type GetVideoCountQueryHookResult = ReturnType<typeof useGetVideoCountQue
 export type GetVideoCountLazyQueryHookResult = ReturnType<typeof useGetVideoCountLazyQuery>
 export type GetVideoCountQueryResult = Apollo.QueryResult<GetVideoCountQuery, GetVideoCountQueryVariables>
 export const GetChannelsDocument = gql`
-  query GetChannels($offset: Int, $limit: Int, $where: ChannelWhereInput) {
-    channels(offset: $offset, limit: $limit, where: $where) {
+  query GetChannels($where: ChannelWhereInput, $limit: Int = 50, $orderBy: [ChannelOrderByInput!] = [createdAt_DESC]) {
+    channels(where: $where, orderBy: $orderBy, limit: $limit) {
       ...AllChannelFields
     }
   }
@@ -332,9 +332,9 @@ export const GetChannelsDocument = gql`
  * @example
  * const { data, loading, error } = useGetChannelsQuery({
  *   variables: {
- *      offset: // value for 'offset'
- *      limit: // value for 'limit'
  *      where: // value for 'where'
+ *      limit: // value for 'limit'
+ *      orderBy: // value for 'orderBy'
  *   },
  * });
  */

+ 2 - 2
src/api/queries/channels.graphql

@@ -51,8 +51,8 @@ query GetVideoCount($where: VideoWhereInput) {
   }
 }
 
-query GetChannels($offset: Int, $limit: Int, $where: ChannelWhereInput) {
-  channels(offset: $offset, limit: $limit, where: $where) {
+query GetChannels($where: ChannelWhereInput, $limit: Int = 50, $orderBy: [ChannelOrderByInput!] = [createdAt_DESC]) {
+  channels(where: $where, orderBy: $orderBy, limit: $limit) {
     ...AllChannelFields
   }
 }

+ 1 - 1
src/api/schemas/extendedQueryNode.graphql

@@ -292,7 +292,7 @@ type Query {
   channelByUniqueInput(where: ChannelWhereUniqueInput!): Channel
 
   # List all channels by given constraints
-  channels(offset: Int, limit: Int, where: ChannelWhereInput): [Channel!]!
+  channels(offset: Int, limit: Int, orderBy: [ChannelOrderByInput!], where: ChannelWhereInput): [Channel!]!
 
   # List all channel by given constraints
   channelsConnection(

+ 19 - 6
src/components/InfiniteGrids/InfiniteChannelWithVideosGrid.tsx

@@ -2,11 +2,12 @@ import React, { FC, Fragment, useCallback, useEffect, useMemo, useState } from '
 
 import { useLanguages } from '@/api/hooks'
 import {
+  ChannelEdge,
   ChannelOrderByInput,
-  ChannelWhereInput,
   GetChannelsConnectionDocument,
   GetChannelsConnectionQuery,
   GetChannelsConnectionQueryVariables,
+  VideoEdge,
 } from '@/api/queries'
 import { ChannelWithVideos } from '@/components'
 import { useInfiniteGrid } from '@/components/InfiniteGrids/useInfiniteGrid'
@@ -26,8 +27,11 @@ import {
 
 type InfiniteChannelWithVideosGridProps = {
   onDemand?: boolean
+  sortByViews?: boolean
   title?: string
   skipCount?: number
+  first?: number
+  orderBy?: ChannelOrderByInput
   isReady?: boolean
   className?: string
   languageSelector?: boolean
@@ -36,6 +40,7 @@ type InfiniteChannelWithVideosGridProps = {
     name: string
     url: string
   }
+  additionalSortFn?: (edge?: ChannelEdge[] | VideoEdge[]) => (ChannelEdge | VideoEdge)[]
 }
 
 const INITIAL_ROWS = 3
@@ -46,10 +51,14 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
   title,
   skipCount = 0,
   isReady = true,
+  first,
+  orderBy = ChannelOrderByInput.CreatedAtAsc,
   className,
+  sortByViews,
   languageSelector,
   idIn = null,
   additionalLink,
+  additionalSortFn,
 }) => {
   const [selectedLanguage, setSelectedLanguage] = useState<string | null | undefined>(null)
   const [targetRowsCount, setTargetRowsCount] = useState(INITIAL_ROWS)
@@ -58,7 +67,9 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
     setTargetRowsCount((prevState) => prevState + 3)
   }, [])
 
-  const queryVariables: { where: ChannelWhereInput } = {
+  const queryVariables: GetChannelsConnectionQueryVariables = {
+    ...(first ? { first } : {}),
+    ...(orderBy ? { orderBy } : {}),
     where: {
       ...(selectedLanguage ? { languageId_eq: selectedLanguage } : {}),
       ...(idIn ? { id_in: idIn } : {}),
@@ -75,11 +86,13 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
     query: GetChannelsConnectionDocument,
     isReady: languageSelector ? isReady && !!selectedLanguage : isReady,
     skipCount,
-    orderBy: ChannelOrderByInput.CreatedAtAsc,
+    orderBy,
     queryVariables,
     targetRowsCount,
     dataAccessor: (rawData) => rawData?.channelsConnection,
     itemsPerRow: INITIAL_CHANNELS_PER_ROW,
+    sortByViews,
+    additionalSortFn,
   })
 
   if (error) {
@@ -88,7 +101,6 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
 
   const placeholderItems = Array.from({ length: placeholdersCount }, () => ({ id: undefined }))
   const shouldShowLoadMoreButton = onDemand && !loading && displayedItems.length < totalCount
-
   const itemsToShow = [...displayedItems, ...placeholderItems]
 
   const mappedLanguages = useMemo(() => {
@@ -131,7 +143,8 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
                   items={mappedLanguages || []}
                   disabled={languagesLoading}
                   value={selectedLanguage}
-                  size="regular"
+                  size="small"
+                  helperText={null}
                   onChange={onSelectLanguage}
                 />
               </LanguageSelectWrapper>
@@ -142,7 +155,7 @@ export const InfiniteChannelWithVideosGrid: FC<InfiniteChannelWithVideosGridProp
                 size="medium"
                 variant="secondary"
                 iconPlacement="right"
-                icon={<SvgGlyphChevronRight width={12} height={12} />}
+                icon={<SvgGlyphChevronRight />}
               >
                 {additionalLink.name}
               </AdditionalLink>

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

@@ -17,6 +17,8 @@ export const LoadMoreButtonWrapper = styled.div`
 
 export const TitleWrapper = styled.div`
   display: flex;
+  align-items: center;
+  padding-bottom: ${sizes(5)};
 `
 
 export const LanguageSelectWrapper = styled.div`

+ 9 - 3
src/components/InfiniteGrids/useInfiniteGrid.ts

@@ -4,7 +4,7 @@ import { DocumentNode } from 'graphql'
 import { debounce, isEqual } from 'lodash'
 import { useEffect, useRef } from 'react'
 
-import { ChannelOrderByInput } from '@/api/queries'
+import { ChannelEdge, ChannelOrderByInput, VideoEdge } from '@/api/queries'
 
 export type PaginatedData<T> = {
   edges: {
@@ -42,6 +42,8 @@ type UseInfiniteGridParams<TRawData, TPaginatedData extends PaginatedData<unknow
   onDemand?: boolean
   onScrollToBottom?: () => void
   orderBy?: ChannelOrderByInput
+  sortByViews?: boolean
+  additionalSortFn?: (edge?: ChannelEdge[] | VideoEdge[]) => (ChannelEdge | VideoEdge)[]
 }
 
 type UseInfiniteGridReturn<TPaginatedData extends PaginatedData<unknown>> = {
@@ -68,7 +70,9 @@ export const useInfiniteGrid = <
   onError,
   queryVariables,
   onDemand,
+  sortByViews,
   orderBy = ChannelOrderByInput.CreatedAtDesc,
+  additionalSortFn,
 }: UseInfiniteGridParams<TRawData, TPaginatedData, TArgs>): UseInfiniteGridReturn<TPaginatedData> => {
   const targetDisplayedItemsCount = targetRowsCount * itemsPerRow
   const targetLoadedItemsCount = targetDisplayedItemsCount + skipCount
@@ -81,7 +85,7 @@ export const useInfiniteGrid = <
     variables: {
       ...queryVariables,
       orderBy,
-      first: targetLoadedItemsCount,
+      first: sortByViews ? 100 : targetDisplayedItemsCount,
     },
     onError,
   })
@@ -145,7 +149,9 @@ export const useInfiniteGrid = <
     return () => window.removeEventListener('scroll', scrollHandler)
   }, [error, isReady, loading, allItemsLoaded, onScrollToBottom, onDemand])
 
-  const displayedEdges = data?.edges.slice(skipCount, targetLoadedItemsCount) ?? []
+  const edges = additionalSortFn ? additionalSortFn(data?.edges as ChannelEdge[] | VideoEdge[]) : data?.edges
+
+  const displayedEdges = edges?.slice(skipCount, targetLoadedItemsCount) ?? []
   const displayedItems = displayedEdges.map((edge) => edge.node)
 
   const displayedItemsCount = data

+ 32 - 0
src/components/PromisingNewChannels.tsx

@@ -0,0 +1,32 @@
+import React from 'react'
+
+import { ChannelEdge, ChannelOrderByInput, VideoEdge } from '@/api/queries'
+import { absoluteRoutes } from '@/config/routes'
+
+import { InfiniteChannelWithVideosGrid } from './InfiniteGrids'
+
+export const PromisingNewChannels = () => {
+  const sortChannelsByViewsDesc = (edges?: ChannelEdge[] | VideoEdge[]) => {
+    if (!edges) {
+      return []
+    }
+    return [...edges].sort((a, b) => {
+      return (b.node.views || 0) - (a.node.views || 0)
+    })
+  }
+
+  return (
+    <InfiniteChannelWithVideosGrid
+      title="Promising new channels"
+      onDemand
+      first={100}
+      orderBy={ChannelOrderByInput.CreatedAtDesc}
+      additionalLink={{
+        name: 'Browse Channels',
+        url: absoluteRoutes.viewer.channels(),
+      }}
+      additionalSortFn={sortChannelsByViewsDesc}
+      sortByViews
+    />
+  )
+}

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

@@ -39,7 +39,7 @@ export const SelectButton = styled.button<SelectButtonProps>`
         `
       case 'small':
         return css`
-          min-height: ${sizes(8)};
+          min-height: ${sizes(10)};
           font-size: 14px !important;
           padding: 0 ${sizes(4)} !important;
         `

+ 7 - 2
src/views/viewer/NewView/NewView.tsx

@@ -2,14 +2,16 @@ import styled from '@emotion/styled'
 import React from 'react'
 
 import { InfiniteVideoGrid, ViewWrapper } from '@/components'
+import { PromisingNewChannels } from '@/components/PromisingNewChannels'
 import { absoluteRoutes } from '@/config/routes'
 import { CallToActionButton, CallToActionWrapper, Text } from '@/shared/components'
 import { SvgNavChannels, SvgNavHome, SvgNavPopular } from '@/shared/icons'
 import { sizes } from '@/shared/theme'
 
 export const NewView: React.FC = () => (
-  <ViewWrapper>
+  <StyledViewWrapper>
     <Header variant="h2">New & Noteworthy</Header>
+    <PromisingNewChannels />
     <StyledInfiniteVideoGrid title="Videos worth watching" isFeatured onDemand />
     <CallToActionWrapper>
       <CallToActionButton label="Home" to={absoluteRoutes.viewer.index()} colorVariant="yellow" icon={<SvgNavHome />} />
@@ -26,7 +28,7 @@ export const NewView: React.FC = () => (
         icon={<SvgNavPopular />}
       />
     </CallToActionWrapper>
-  </ViewWrapper>
+  </StyledViewWrapper>
 )
 
 const Header = styled(Text)`
@@ -41,3 +43,6 @@ const StyledInfiniteVideoGrid = styled(InfiniteVideoGrid)`
     margin-top: ${sizes(36)};
   }
 `
+const StyledViewWrapper = styled(ViewWrapper)`
+  padding-bottom: ${sizes(10)};
+`