diff --git a/src/features/HomePage/hooks.tsx b/src/features/HomePage/hooks.tsx index 5cc3db21..6607d6a2 100644 --- a/src/features/HomePage/hooks.tsx +++ b/src/features/HomePage/hooks.tsx @@ -1,9 +1,17 @@ -import { useEffect, useState } from 'react' +import { + useCallback, + useEffect, + useState, + useRef, +} from 'react' import type { Matches } from 'requests' import { getMatches } from 'requests' import { useHeaderFiltersStore } from 'features/HeaderFilters' +import { useRequest } from 'hooks' + +const MATCHES_LIMIT = 60 export const useHomePage = () => { const { @@ -13,26 +21,66 @@ export const useHomePage = () => { selectedTournamentId, } = useHeaderFiltersStore() + const { + isFetching, + request: requestMatches, + } = useRequest(getMatches) + const [matches, setMatches] = useState({ broadcast: [], features: [], + hasNextPage: true, highlights: [], isVideoSections: false, }) + const pageRef = useRef(0) - useEffect(() => { - getMatches({ + const fetchMatches = useCallback((page: number) => ( + requestMatches({ date: selectedDateFormatted, + limit: MATCHES_LIMIT, matchStatus: selectedMatchStatus, + offset: page * MATCHES_LIMIT, sportType: selectedSportTypeId, tournamentId: selectedTournamentId, - }).then(setMatches) - }, [ + }) + ), [ selectedDateFormatted, selectedMatchStatus, selectedSportTypeId, selectedTournamentId, + requestMatches, + ]) + + const { hasNextPage } = matches + + const fetchMoreMatches = useCallback(async () => { + if (!hasNextPage || isFetching) return + + const newMatches = await fetchMatches(pageRef.current) + setMatches((oldMatches): Matches => { + const broadcast = [...oldMatches.broadcast, ...newMatches.broadcast] + return { + ...oldMatches, + broadcast, + hasNextPage: newMatches.hasNextPage, + } + }) + pageRef.current += 1 + }, [ + fetchMatches, + hasNextPage, + isFetching, ]) - return { matches } + useEffect(() => { + fetchMatches(0).then(setMatches) + pageRef.current = 1 + }, [fetchMatches]) + + return { + fetchMoreMatches, + isFetching, + matches, + } } diff --git a/src/features/HomePage/index.tsx b/src/features/HomePage/index.tsx index 0cc78fb1..ef5a28f0 100644 --- a/src/features/HomePage/index.tsx +++ b/src/features/HomePage/index.tsx @@ -1,15 +1,23 @@ import React from 'react' +import { InfiniteScroll } from 'features/InfiniteScroll' import { Matches } from 'features/Matches' import { useHomePage } from './hooks' -import { Content } from './styled' +import { Content, Loading } from './styled' export const HomePage = () => { - const { matches } = useHomePage() + const { + fetchMoreMatches, + isFetching, + matches, + } = useHomePage() return ( - + + + + {isFetching && Loading...} ) } diff --git a/src/features/HomePage/styled.tsx b/src/features/HomePage/styled.tsx index f95b2f2f..d8b43704 100644 --- a/src/features/HomePage/styled.tsx +++ b/src/features/HomePage/styled.tsx @@ -13,3 +13,11 @@ export const Content = styled.main` padding: 0; } ` + +export const Loading = styled.div` + height: 30px; + margin-top: 20px; + font-size: 24px; + color: #fff; + text-align: center; +` diff --git a/src/features/InfiniteScroll/hooks.tsx b/src/features/InfiniteScroll/hooks.tsx new file mode 100644 index 00000000..0f76cb86 --- /dev/null +++ b/src/features/InfiniteScroll/hooks.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react' + +import noop from 'lodash/noop' + +type Args = { + onIntersect: ( + target: IntersectionObserverEntry, + observer: IntersectionObserver, + ) => void, + options?: IntersectionObserverInit, +} + +/** + * Хук для отслежения пересечение targetRef с rootRef + * targetRef нужно повесить на целевой элемент который будет наблюдаться, + * rootRef на корневой элемент с которым пересекается targetRef. + * Также можно проигнорить rootRef тогда по-умолчанию отслеживается + * пересечение с областью видимости документа + */ +export const useIntersectionObserver = ({ onIntersect, options }: Args) => { + const rootRef = useRef(null) + const targetRef = useRef(null) + const rootElement = rootRef.current + const targetElement = targetRef.current + + useEffect(() => { + if (!targetElement) { + return noop + } + + const observerOptions = { root: rootElement, ...options } + const callback = ([target]: Array) => { + if (target.isIntersecting) { + onIntersect(target, observer) + } + } + + const observer = new IntersectionObserver(callback, observerOptions) + observer.observe(targetElement) + + return () => { + observer.disconnect() + } + }, [ + rootElement, + targetElement, + onIntersect, + options, + ]) + + return { rootRef, targetRef } +} diff --git a/src/features/InfiniteScroll/index.tsx b/src/features/InfiniteScroll/index.tsx new file mode 100644 index 00000000..929fc78c --- /dev/null +++ b/src/features/InfiniteScroll/index.tsx @@ -0,0 +1,29 @@ +import type { ReactNode } from 'react' +import React from 'react' + +import { useIntersectionObserver } from './hooks' +import { Root, Target } from './styled' + +type Props = { + children: ReactNode, + onFetchMore: () => void, + options?: IntersectionObserverInit, +} + +export const InfiniteScroll = ({ + children, + onFetchMore, + options, +}: Props) => { + const { targetRef } = useIntersectionObserver({ + onIntersect: onFetchMore, + options, + }) + + return ( + + {children} + + + ) +} diff --git a/src/features/InfiniteScroll/styled.tsx b/src/features/InfiniteScroll/styled.tsx new file mode 100644 index 00000000..c302717f --- /dev/null +++ b/src/features/InfiniteScroll/styled.tsx @@ -0,0 +1,8 @@ +import styled from 'styled-components/macro' + +export const Root = styled.div`` + +export const Target = styled.div` + position: relative; + bottom: 20vh; +` diff --git a/src/features/MatchPage/hooks.tsx b/src/features/MatchPage/hooks.tsx index 14f9efe9..cbd18b56 100644 --- a/src/features/MatchPage/hooks.tsx +++ b/src/features/MatchPage/hooks.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react' import type { MatchInfo } from 'requests' import { getMatchInfo } from 'requests' -import { useHeaderFiltersStore } from 'features/HeaderFilters' import { useLexicsStore } from 'features/LexicsStore' import { useSportNameParam, usePageId } from 'hooks' @@ -16,11 +15,6 @@ export const useMatchPage = () => { const pageId = usePageId() const { suffix } = useLexicsStore() - const { - setSelectedSportTypeId, - setSelectedTournamentId, - } = useHeaderFiltersStore() - const matchProfileNames = { team1Name: matchProfile?.team1[`name_${suffix}` as Name], team2Name: matchProfile?.team2[`name_${suffix}` as Name], @@ -28,22 +22,12 @@ export const useMatchPage = () => { } useEffect(() => { - setSelectedSportTypeId(sportType) - setSelectedTournamentId(pageId) - getMatchInfo(sportType, pageId) .then(setMatchProfile) - - return () => { - setSelectedSportTypeId(null) - setSelectedTournamentId(null) - } }, [ sportType, pageId, - setSelectedSportTypeId, - setSelectedTournamentId, ]) return { diff --git a/src/features/Matches/hooks.tsx b/src/features/Matches/hooks.tsx index 3738f55a..83bfa82f 100644 --- a/src/features/Matches/hooks.tsx +++ b/src/features/Matches/hooks.tsx @@ -4,7 +4,6 @@ import map from 'lodash/map' import flatten from 'lodash/flatten' import pipe from 'lodash/fp/pipe' import fpMap from 'lodash/fp/map' -import fpOrderBy from 'lodash/fp/orderBy' import type { Matches, Content } from 'requests' import { ProfileTypes } from 'config' @@ -69,7 +68,6 @@ const prepareMatches = (content: Array, suffix: string) => pipe( tournamentName: rest[`name_${suffix}` as Name], }))), flatten, - fpOrderBy((match: Match) => Number(new Date(match.date)), 'desc'), )(content) as Array export const useMatches = ({ matches }: Props) => { diff --git a/src/requests/getMatches.tsx b/src/requests/getMatches.tsx index 550f51f6..b7afe624 100644 --- a/src/requests/getMatches.tsx +++ b/src/requests/getMatches.tsx @@ -9,18 +9,19 @@ import { MatchStatuses } from 'features/HeaderFilters' const proc = PROCEDURES.get_matches -export type Data = { +type Data = { is_video_sections: boolean, video_content: VideoContent, } -export type VideoContent = { +type VideoContent = { broadcast: Items, features: Items, highlights: Items, + show: boolean, } -export type Items = { +type Items = { content: Array | null, name: string, } @@ -33,7 +34,7 @@ export type Content = { sport: SportTypes, } -export type Match = { +type Match = { date: string, id: number, round_id: number | null, @@ -42,7 +43,7 @@ export type Match = { team2: Team, } -export type Team = { +type Team = { id: number, name_eng: string, name_rus: string, @@ -51,7 +52,9 @@ export type Team = { type Args = { date: string, + limit?: number, matchStatus: MatchStatuses | null, + offset?: number, playerId?: number | null, sportType: SportTypes | null, teamId?: number | null, @@ -61,13 +64,16 @@ type Args = { export type Matches = { broadcast: Array, features: Array, + hasNextPage?: boolean, highlights: Array, isVideoSections: boolean, } export const getMatches = async ({ date, + limit, matchStatus, + offset, playerId, sportType, teamId, @@ -77,6 +83,8 @@ export const getMatches = async ({ body: { params: { _p_date: date, + _p_limit: limit, + _p_offset: offset, _p_player_id: playerId || null, _p_sport: sportType, _p_stream_status: matchStatus, @@ -96,11 +104,13 @@ export const getMatches = async ({ broadcast, features, highlights, + show, } = data.video_content return { broadcast: broadcast.content || [], features: features.content || [], + hasNextPage: Boolean(show), highlights: highlights.content || [], isVideoSections: data.is_video_sections, }