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

Adjust InfiniteVideoGrid to handle query on demand, add LoadMoreButton component (#994)

Rafał Pawłow 3 роки тому
батько
коміт
ef3f7ae95d

+ 14 - 5
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -8,7 +8,7 @@ import {
   GetVideosConnectionQueryVariables,
   VideoWhereInput,
 } from '@/api/queries'
-import { Grid, SkeletonLoader, Text } from '@/shared/components'
+import { Grid, LoadMoreButton, SkeletonLoader, Text } from '@/shared/components'
 import { sizes } from '@/shared/theme'
 import { SentryLogger } from '@/utils/logs'
 
@@ -33,7 +33,7 @@ type InfiniteVideoGridProps = {
   currentlyWatchedVideoId?: string
 }
 
-const INITIAL_ROWS = 4
+const INITIAL_ROWS = 2
 const INITIAL_VIDEOS_PER_ROW = 4
 
 export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
@@ -73,24 +73,24 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
 
   const targetRowsCount = targetRowsCountByCategory[cachedCategoryId]
 
-  const onScrollToBottom = useCallback(() => {
+  const fetchMore = useCallback(() => {
     setTargetRowsCountByCategory((prevState) => ({
       ...prevState,
       [cachedCategoryId]: targetRowsCount + 2,
     }))
   }, [cachedCategoryId, targetRowsCount])
 
-  const { placeholdersCount, displayedItems, error } = useInfiniteGrid<
+  const { placeholdersCount, displayedItems, error, allItemsLoaded, loading } = useInfiniteGrid<
     GetVideosConnectionQuery,
     GetVideosConnectionQuery['videosConnection'],
     GetVideosConnectionQueryVariables
   >({
     query: GetVideosConnectionDocument,
-    onScrollToBottom,
     isReady: ready,
     skipCount,
     queryVariables,
     targetRowsCount,
+    onDemand: true,
     dataAccessor: (rawData) => {
       if (currentlyWatchedVideoId) {
         return (
@@ -158,6 +158,11 @@ export const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
     <section className={className}>
       {title && (!ready ? <StyledSkeletonLoader height={23} width={250} /> : <Title variant="h5">{title}</Title>)}
       <Grid onResize={(sizes) => setVideosPerRow(sizes.length)}>{gridContent}</Grid>
+      {!allItemsLoaded && !loading && (
+        <LoadMoreButtonWrapper>
+          <LoadMoreButton onClick={fetchMore} />
+        </LoadMoreButtonWrapper>
+      )}
     </section>
   )
 }
@@ -169,3 +174,7 @@ const Title = styled(Text)`
 const StyledSkeletonLoader = styled(SkeletonLoader)`
   margin-bottom: ${sizes(4)};
 `
+
+const LoadMoreButtonWrapper = styled.div`
+  margin-top: ${sizes(10)};
+`

+ 12 - 4
src/components/InfiniteGrids/useInfiniteGrid.ts

@@ -4,7 +4,7 @@ import { DocumentNode } from 'graphql'
 import { debounce } from 'lodash'
 import { useEffect } from 'react'
 
-type PaginatedData<T> = {
+export type PaginatedData<T> = {
   edges: {
     cursor: string
     node: T
@@ -38,12 +38,14 @@ type UseInfiniteGridParams<TRawData, TPaginatedData extends PaginatedData<unknow
   onScrollToBottom: () => void
   onError?: (error: unknown) => void
   queryVariables: TArgs
-}
+} & ({ onDemand?: false; onScrollToBottom: () => void } | { onDemand: true; onScrollToBottom?: undefined })
 
 type UseInfiniteGridReturn<TPaginatedData extends PaginatedData<unknown>> = {
   displayedItems: TPaginatedData['edges'][0]['node'][]
   placeholdersCount: number
   error?: ApolloError
+  allItemsLoaded: boolean
+  loading: boolean
 }
 
 export const useInfiniteGrid = <
@@ -60,6 +62,7 @@ export const useInfiniteGrid = <
   onScrollToBottom,
   onError,
   queryVariables,
+  onDemand,
 }: UseInfiniteGridParams<TRawData, TPaginatedData, TArgs>): UseInfiniteGridReturn<TPaginatedData> => {
   const targetDisplayedItemsCount = targetRowsCount * itemsPerRow
   const targetLoadedItemsCount = targetDisplayedItemsCount + skipCount
@@ -109,19 +112,22 @@ export const useInfiniteGrid = <
 
   // handle scroll to bottom
   useEffect(() => {
+    if (onDemand) {
+      return
+    }
     if (error) return
 
     const scrollHandler = debounce(() => {
       const scrolledToBottom =
         window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight
-      if (scrolledToBottom && isReady && !loading && !allItemsLoaded) {
+      if (onScrollToBottom && scrolledToBottom && isReady && !loading && !allItemsLoaded) {
         onScrollToBottom()
       }
     }, 100)
 
     window.addEventListener('scroll', scrollHandler)
     return () => window.removeEventListener('scroll', scrollHandler)
-  }, [error, isReady, loading, allItemsLoaded, onScrollToBottom])
+  }, [error, isReady, loading, allItemsLoaded, onScrollToBottom, onDemand])
 
   const displayedEdges = data?.edges.slice(skipCount, targetLoadedItemsCount) ?? []
   const displayedItems = displayedEdges.map((edge) => edge.node)
@@ -134,6 +140,8 @@ export const useInfiniteGrid = <
   return {
     displayedItems,
     placeholdersCount,
+    allItemsLoaded,
     error,
+    loading,
   }
 }

+ 6 - 0
src/shared/components/Button/Button.stories.tsx

@@ -14,6 +14,12 @@ export default {
     className: { table: { disable: true } },
     to: { table: { disable: true } },
     type: { table: { disable: true } },
+    variant: { table: { disable: true } },
+    fullWidth: {
+      table: { disable: false, type: { summary: 'boolean' }, defaultValue: { summary: false } },
+      type: { name: 'boolean', required: false },
+      defaultValue: false,
+    },
     textOnly: { table: { disable: true } },
     iconPlacement: {
       control: { type: 'select', options: ['left', 'right'] },

+ 1 - 1
src/shared/components/Button/Button.tsx

@@ -8,7 +8,7 @@ import { TextVariant } from '../Text'
 export type ButtonProps = {
   icon?: React.ReactNode
   iconPlacement?: IconPlacement
-  children: string
+  children: React.ReactNode
 } & Omit<ButtonBaseProps, 'children'>
 
 const BUTTON_SIZE_TO_TEXT_VARIANT: Record<ButtonSize, TextVariant> = {

+ 2 - 0
src/shared/components/ButtonBase/ButtonBase.style.ts

@@ -17,6 +17,7 @@ export type ButtonBaseStyleProps = {
   variant: ButtonVariant
   size: ButtonSize
   clickable?: boolean
+  fullWidth?: boolean
   textOnly: boolean
   iconOnly: boolean
 }
@@ -227,6 +228,7 @@ export const StyledButtonBase = styled('button', { shouldForwardProp: isPropVali
   border: 0;
   background-color: transparent;
   cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')};
+  width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
 
   &:disabled,
   &[aria-disabled='true'] {

+ 1 - 1
src/shared/components/ButtonBase/ButtonBase.tsx

@@ -13,7 +13,7 @@ export type ButtonBaseProps = {
   iconOnly?: boolean
   children?: React.ReactNode
   className?: string
-} & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant'>>
+} & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant' | 'fullWidth'>>
 
 const getLinkPropsFromTo = (to?: To) => {
   if (!to) {

+ 28 - 0
src/shared/components/LoadMoreButton/LoadMoreButton.tsx

@@ -0,0 +1,28 @@
+import styled from '@emotion/styled'
+import React, { FC, MouseEvent } from 'react'
+
+import { Button } from '@/shared/components'
+import { SvgGlyphChevronDown } from '@/shared/icons'
+
+type LoadMoreButtonProps = {
+  onClick: (event: MouseEvent<HTMLButtonElement>) => void
+}
+
+export const LoadMoreButton: FC<LoadMoreButtonProps> = ({ onClick }) => (
+  <LoadMore
+    variant="secondary"
+    size="large"
+    fullWidth
+    onClick={onClick}
+    iconPlacement="right"
+    icon={<SvgGlyphChevronDown width={12} height={12} />}
+  >
+    Show more videos
+  </LoadMore>
+)
+
+const LoadMore = styled(Button)`
+  span {
+    display: flex;
+  }
+`

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

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

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

@@ -45,3 +45,4 @@ export * from './Loader'
 export * from './AnimatedError'
 export * from './EmptyFallback'
 export * from './LayoutGrid/LayoutGrid'
+export * from './LoadMoreButton'