From 269ddad403e7f97397e28ad726c0dd0ad9d02472 Mon Sep 17 00:00:00 2001 From: Ruslan Khayrullin Date: Tue, 15 Nov 2022 18:42:55 +0500 Subject: [PATCH] feat(in-142): match stats tab --- src/config/lexics/indexLexics.tsx | 5 + .../components/FinishedMatch/index.tsx | 9 +- .../components/LiveMatch/hooks/index.tsx | 8 +- src/features/MatchPage/store/hooks/index.tsx | 18 +- .../MatchPage/store/hooks/useMatchData.tsx | 28 +- .../MatchPage/store/hooks/usePlayersStats.tsx | 159 +++++++ .../MatchPage/store/hooks/useStatsTab.tsx | 12 + .../MatchPage/store/hooks/useTeamsStats.tsx | 78 ++++ .../components/MatchPlaylists/index.tsx | 39 +- .../components/VideoDate/index.tsx | 0 .../components/VideoDate/styled.tsx | 0 .../{TabVideo => Matches}/index.tsx | 12 +- .../{TabVideo => Matches}/styled.tsx | 9 +- .../components/PlayersPlaylists/index.tsx | 5 - .../components/PlayersPlaylists/styled.tsx | 5 +- .../components/PlayersTable/config.tsx | 6 + .../components/PlayersTable/hooks/index.tsx | 58 +++ .../PlayersTable/hooks/usePlayers.tsx | 83 ++++ .../PlayersTable/hooks/useTable.tsx | 171 ++++++++ .../components/PlayersTable/index.tsx | 397 +++++++----------- .../components/PlayersTable/styled.tsx | 231 ++++++++-- .../components/PlayersTable/types.tsx | 8 + .../components/TabStats/config.tsx | 5 + .../components/TabStats/hooks.tsx | 68 +++ .../components/TabStats/index.tsx | 109 ++++- .../components/TabStats/styled.tsx | 40 +- .../components/TabWatch/index.tsx | 77 ++-- .../components/TeamsStats/index.tsx | 276 ------------ .../components/TeamsStatsTable/hooks.tsx | 31 ++ .../components/TeamsStatsTable/index.tsx | 93 ++++ .../styled.tsx | 21 +- src/features/MatchSidePlaylists/hooks.tsx | 72 +++- src/features/MatchSidePlaylists/index.tsx | 25 +- src/features/MatchSidePlaylists/styled.tsx | 44 +- .../MultiSourcePlayer/hooks/index.tsx | 3 + .../components/YoutubePlayer/index.tsx | 4 +- src/features/StreamPlayer/index.tsx | 4 +- src/helpers/getTeamAbbr/index.tsx | 25 ++ src/helpers/index.tsx | 1 + src/hooks/usePageParams.tsx | 1 + src/requests/getMatchParticipants.tsx | 65 +++ src/requests/getPlayersStats.tsx | 54 +++ src/requests/getTeamsStats.tsx | 56 +++ src/requests/index.tsx | 3 + 44 files changed, 1704 insertions(+), 714 deletions(-) create mode 100644 src/features/MatchPage/store/hooks/usePlayersStats.tsx create mode 100644 src/features/MatchPage/store/hooks/useStatsTab.tsx create mode 100644 src/features/MatchPage/store/hooks/useTeamsStats.tsx rename src/features/MatchSidePlaylists/components/{TabVideo => Matches}/components/VideoDate/index.tsx (100%) rename src/features/MatchSidePlaylists/components/{TabVideo => Matches}/components/VideoDate/styled.tsx (100%) rename src/features/MatchSidePlaylists/components/{TabVideo => Matches}/index.tsx (90%) rename src/features/MatchSidePlaylists/components/{TabVideo => Matches}/styled.tsx (63%) create mode 100644 src/features/MatchSidePlaylists/components/PlayersTable/config.tsx create mode 100644 src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx create mode 100644 src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx create mode 100644 src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx create mode 100644 src/features/MatchSidePlaylists/components/PlayersTable/types.tsx create mode 100644 src/features/MatchSidePlaylists/components/TabStats/hooks.tsx delete mode 100644 src/features/MatchSidePlaylists/components/TeamsStats/index.tsx create mode 100644 src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx create mode 100644 src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx rename src/features/MatchSidePlaylists/components/{TeamsStats => TeamsStatsTable}/styled.tsx (68%) create mode 100644 src/helpers/getTeamAbbr/index.tsx create mode 100644 src/requests/getMatchParticipants.tsx create mode 100644 src/requests/getPlayersStats.tsx create mode 100644 src/requests/getTeamsStats.tsx diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index e44e4d09..545eea28 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -9,8 +9,12 @@ const matchPopupLexics = { apply: 13491, choose_fav_team: 19776, commentators: 15424, + current_stats: 19592, + display_all_stats: 19932, + display_stats_according_to_video: 19931, episode_duration: 13410, events: 1020, + final_stats: 19591, from_end_match: 15396, from_price: 3992, from_start_match: 15395, @@ -160,6 +164,7 @@ export const indexLexics = { no_match_access_body: 13419, no_match_access_title: 13418, player: 14975, + players: 164, players_video: 13032, privacy_policy_and_statement: 15404, round_highilights: 13050, diff --git a/src/features/MatchPage/components/FinishedMatch/index.tsx b/src/features/MatchPage/components/FinishedMatch/index.tsx index d10f60d0..d8e22586 100644 --- a/src/features/MatchPage/components/FinishedMatch/index.tsx +++ b/src/features/MatchPage/components/FinishedMatch/index.tsx @@ -17,7 +17,11 @@ import { useMatchPageStore } from '../../store' export const FinishedMatch = () => { const [circleAnimation, setCircleAnimation] = useState(initialCircleAnimation) - const { isOpenPopup, profile } = useMatchPageStore() + const { + isOpenFiltersPopup, + profile, + setPlayingProgress, + } = useMatchPageStore() const { chapters, closeSettingsPopup, @@ -48,9 +52,10 @@ export const FinishedMatch = () => { diff --git a/src/features/MatchPage/components/LiveMatch/hooks/index.tsx b/src/features/MatchPage/components/LiveMatch/hooks/index.tsx index e4240ad0..eab6f4b1 100644 --- a/src/features/MatchPage/components/LiveMatch/hooks/index.tsx +++ b/src/features/MatchPage/components/LiveMatch/hooks/index.tsx @@ -21,6 +21,7 @@ export const useLiveMatch = () => { profile, selectedPlaylist, setFullMatchPlaylistDuration, + setPlayingProgress, } = useMatchPageStore() const { profileId: matchId, sportType } = usePageParams() const resume = useResumeUrlParam() @@ -45,7 +46,7 @@ export const useLiveMatch = () => { } = usePlaylistLogger() const { - onPlayerProgressChange, + onPlayerProgressChange: playerProgressChange, onPlayingChange: notifyProgressLogger, } = usePlayerProgressReporter() @@ -66,6 +67,11 @@ export const useLiveMatch = () => { handlePlaylistClick(playlist, e) } + const onPlayerProgressChange = (seconds: number, period = 0) => { + playerProgressChange(seconds, period) + setPlayingProgress(seconds * 1000) + } + return { chapters, isPlayFilterEpisodes, diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index 8b472265..d0c0b76a 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -41,7 +41,7 @@ export const useMatchPage = () => { countOfFilters, filters, isEmptyFilters, - isOpen: isOpenPopup, + isOpen: isOpenFiltersPopup, resetEvents, resetPlayers, toggle: togglePopup, @@ -66,9 +66,16 @@ export const useMatchPage = () => { const { events, handlePlaylistClick, + isEmptyPlayersStats, matchPlaylists, + playersData, + playersStats, selectedPlaylist, setFullMatchPlaylistDuration, + setPlayingProgress, + setStatsType, + statsType, + teamsStats, } = useMatchData(matchProfile) const profile = matchProfile @@ -160,8 +167,9 @@ export const useMatchPage = () => { handlePlaylistClick, hideProfileCard, isEmptyFilters, + isEmptyPlayersStats, isLiveMatch, - isOpenPopup, + isOpenFiltersPopup, isPlayFilterEpisodes, isStarted, likeImage, @@ -170,6 +178,8 @@ export const useMatchPage = () => { plaingOrder, playEpisodes, playNextEpisode, + playersData, + playersStats, profile, profileCardShown, resetEvents, @@ -179,10 +189,14 @@ export const useMatchPage = () => { setFullMatchPlaylistDuration, setIsPlayinFiltersEpisodes, setPlaingOrder, + setPlayingProgress, setReversed, + setStatsType, setUnreversed, setWatchAllEpisodesTimer, showProfileCard, + statsType, + teamsStats, toggleActiveEvents, toggleActivePlayers, togglePopup, diff --git a/src/features/MatchPage/store/hooks/useMatchData.tsx b/src/features/MatchPage/store/hooks/useMatchData.tsx index 4cee0488..39fbf3b5 100644 --- a/src/features/MatchPage/store/hooks/useMatchData.tsx +++ b/src/features/MatchPage/store/hooks/useMatchData.tsx @@ -6,7 +6,7 @@ import { import debounce from 'lodash/debounce' -import { MatchInfo } from 'requests/getMatchInfo' +import type { MatchInfo } from 'requests/getMatchInfo' import { usePageParams } from 'hooks/usePageParams' import { useInterval } from 'hooks/useInterval' @@ -16,6 +16,9 @@ import { useMatchPopupStore } from 'features/MatchPopup' import { useMatchPlaylists } from './useMatchPlaylists' import { useEvents } from './useEvents' +import { useTeamsStats } from './useTeamsStats' +import { useStatsTab } from './useStatsTab' +import { usePlayersStats } from './usePlayersStats' const MATCH_DATA_POLL_INTERVAL = 60000 const MATCH_PLAYLISTS_DELAY = 5000 @@ -24,6 +27,7 @@ export const useMatchData = (profile: MatchInfo) => { const { profileId: matchId, sportType } = usePageParams() const { chapters } = useMatchPopupStore() const [matchDuration, setMatchDuration] = useState(0) + const [playingProgress, setPlayingProgress] = useState(0) const { fetchMatchPlaylists, handlePlaylistClick, @@ -33,6 +37,21 @@ export const useMatchData = (profile: MatchInfo) => { setSelectedPlaylist, } = useMatchPlaylists(profile) const { events, fetchMatchEvents } = useEvents() + const { setStatsType, statsType } = useStatsTab() + const { teamsStats } = useTeamsStats({ + matchProfile: profile, + playingProgress, + statsType, + }) + const { + isEmptyPlayersStats, + playersData, + playersStats, + } = usePlayersStats({ + matchProfile: profile, + playingProgress, + statsType, + }) const fetchPlaylistsDebounced = useMemo( () => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY), @@ -93,8 +112,15 @@ export const useMatchData = (profile: MatchInfo) => { return { events, handlePlaylistClick, + isEmptyPlayersStats, matchPlaylists, + playersData, + playersStats, selectedPlaylist, setFullMatchPlaylistDuration, + setPlayingProgress, + setStatsType, + statsType, + teamsStats, } } diff --git a/src/features/MatchPage/store/hooks/usePlayersStats.tsx b/src/features/MatchPage/store/hooks/usePlayersStats.tsx new file mode 100644 index 00000000..326d4f51 --- /dev/null +++ b/src/features/MatchPage/store/hooks/usePlayersStats.tsx @@ -0,0 +1,159 @@ +import { + useMemo, + useEffect, + useState, +} from 'react' + +import throttle from 'lodash/throttle' +import isEmpty from 'lodash/isEmpty' +import every from 'lodash/every' +import find from 'lodash/find' + +import type { + MatchInfo, + PlayersStats, + Player, +} from 'requests' +import { getPlayersStats, getMatchParticipants } from 'requests' + +import { useObjectState, usePageParams } from 'hooks' + +import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' + +const REQUEST_DELAY = 3000 +const STATS_POLL_INTERVAL = 30000 + +type UsePlayersStatsArgs = { + matchProfile: MatchInfo, + playingProgress: number, + statsType: StatsType, +} + +type PlayersData = { + team1: Array, + team2: Array, +} + +export const usePlayersStats = ({ + matchProfile, + playingProgress, + statsType, +}: UsePlayersStatsArgs) => { + const [playersStats, setPlayersStats] = useObjectState>({}) + const [playersData, setPlayersData] = useState({ team1: [], team2: [] }) + + const { + profileId: matchId, + sportName, + sportType, + } = usePageParams() + + const isCurrentStats = statsType === StatsType.CURRENT_STATS + + const progressSec = Math.floor(playingProgress / 1000) + + const isEmptyPlayersStats = (teamId: number) => ( + isEmpty(playersStats[teamId]) + || every(playersStats[teamId], isEmpty) + || isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2']) + ) + + const fetchPlayers = useMemo(() => throttle((second?: number) => { + if (!matchProfile?.team1.id || !matchProfile?.team1.id) return + + try { + getMatchParticipants({ + matchId, + second, + sportType, + }).then((data) => { + const team1Players = find(data, { team_id: matchProfile.team1.id })?.players || [] + const team2Players = find(data, { team_id: matchProfile.team2.id })?.players || [] + + setPlayersData({ + team1: team1Players, + team2: team2Players, + }) + }) + + // eslint-disable-next-line no-empty + } catch (e) {} + }, REQUEST_DELAY), [ + matchId, + matchProfile?.team1.id, + matchProfile?.team2.id, + sportType, + ]) + + const fetchPlayersStats = useMemo(() => throttle((second?: number) => { + if (!sportName || !matchProfile?.team1.id || !matchProfile?.team2.id) return + + try { + getPlayersStats({ + matchId, + second, + sportName, + teamId: matchProfile.team1.id, + }).then((data) => setPlayersStats({ [matchProfile.team1.id]: data })) + + getPlayersStats({ + matchId, + second, + sportName, + teamId: matchProfile.team2.id, + }).then((data) => setPlayersStats({ [matchProfile?.team2.id]: data })) + // eslint-disable-next-line no-empty + } catch (e) {} + }, REQUEST_DELAY), [ + matchId, + matchProfile?.team1.id, + matchProfile?.team2.id, + setPlayersStats, + sportName, + ]) + + useEffect(() => { + let interval: NodeJS.Timeout + + fetchPlayers() + + if (!isCurrentStats) { + fetchPlayersStats() + } + + if (matchProfile?.live) { + interval = setInterval(() => { + if (isCurrentStats) return + + fetchPlayersStats() + fetchPlayers() + }, STATS_POLL_INTERVAL) + } + + return () => clearInterval(interval) + }, [ + fetchPlayersStats, + fetchPlayers, + isCurrentStats, + matchProfile?.live, + ]) + + useEffect(() => { + if (isCurrentStats) { + fetchPlayersStats(progressSec) + fetchPlayers(progressSec) + } + }, [ + fetchPlayersStats, + fetchPlayers, + progressSec, + isCurrentStats, + matchProfile?.live, + ]) + + return { + isEmptyPlayersStats, + playersData, + playersStats, + } +} diff --git a/src/features/MatchPage/store/hooks/useStatsTab.tsx b/src/features/MatchPage/store/hooks/useStatsTab.tsx new file mode 100644 index 00000000..9a2a18f1 --- /dev/null +++ b/src/features/MatchPage/store/hooks/useStatsTab.tsx @@ -0,0 +1,12 @@ +import { useState } from 'react' + +import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' + +export const useStatsTab = () => { + const [statsType, setStatsType] = useState(StatsType.FINAL_STATS) + + return { + setStatsType, + statsType, + } +} diff --git a/src/features/MatchPage/store/hooks/useTeamsStats.tsx b/src/features/MatchPage/store/hooks/useTeamsStats.tsx new file mode 100644 index 00000000..f9f6ea58 --- /dev/null +++ b/src/features/MatchPage/store/hooks/useTeamsStats.tsx @@ -0,0 +1,78 @@ +import { + useEffect, + useState, + useMemo, +} from 'react' + +import throttle from 'lodash/throttle' + +import type { MatchInfo } from 'requests' +import { getTeamsStats, TeamStatItem } from 'requests' + +import { usePageParams } from 'hooks/usePageParams' + +import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' + +const REQUEST_DELAY = 3000 +const STATS_POLL_INTERVAL = 30000 + +type UseTeamsStatsArgs = { + matchProfile: MatchInfo, + playingProgress: number, + statsType: StatsType, +} + +export const useTeamsStats = ({ + matchProfile, + playingProgress, + statsType, +}: UseTeamsStatsArgs) => { + const [teamsStats, setTeamsStats] = useState<{ + [teamId: string]: Array, + }>({}) + + const { profileId: matchId, sportName } = usePageParams() + + const progressSec = Math.floor(playingProgress / 1000) + + const isCurrentStats = statsType === StatsType.CURRENT_STATS + + const fetchTeamsStats = useMemo(() => throttle((second?: number) => { + if (!sportName) return + + getTeamsStats({ + matchId, + second, + sportName, + }).then(setTeamsStats) + }, REQUEST_DELAY), [matchId, sportName]) + + useEffect(() => { + let timer: ReturnType + + if (!isCurrentStats) { + fetchTeamsStats() + } + + if (matchProfile?.live) { + timer = setInterval(() => { + if (isCurrentStats) return + + fetchTeamsStats() + }, STATS_POLL_INTERVAL) + } + + return () => clearInterval(timer) + }, [fetchTeamsStats, matchProfile?.live, isCurrentStats]) + + useEffect(() => { + if (isCurrentStats) { + fetchTeamsStats(progressSec) + } + }, [fetchTeamsStats, progressSec, isCurrentStats]) + + return { + statsType, + teamsStats, + } +} diff --git a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx index e0e49fb9..a0245220 100644 --- a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx @@ -1,8 +1,8 @@ -import { useMemo } from 'react' +import type { ForwardedRef } from 'react' +import { forwardRef } from 'react' import styled, { css } from 'styled-components/macro' -import filter from 'lodash/filter' import isEmpty from 'lodash/isEmpty' import map from 'lodash/map' @@ -15,6 +15,8 @@ import { T9n } from 'features/T9n' import { PlayButton } from '../PlayButton' +export const LIST_ITEM_INDENT = 12 + type Props = { live?: boolean, onSelect?: (selectedMathPlaylist: PlaylistOption) => void, @@ -25,7 +27,7 @@ type Props = { const List = styled.ul`` const Item = styled.li` - margin-bottom: 12px; + margin-bottom: ${LIST_ITEM_INDENT}px; width: 100%; height: 36px; ${isMobileDevice @@ -36,24 +38,17 @@ const Item = styled.li` : ''}; ` -export const MatchPlaylists = ({ - live, - onSelect, - playlists, - selectedMathPlaylist, -}: Props) => { - const filteredPlayListByDuration = useMemo(() => ( - filter(playlists, (playlist) => ( - live - ? Boolean(playlist.duration) || (playlist.id === 'full_game') - : Boolean(playlist.duration) - )) - ), [playlists, live]) - - return ( - +export const MatchPlaylists = forwardRef( + ({ + live, + onSelect, + playlists, + selectedMathPlaylist, + }: Props, + ref: ForwardedRef) => ( + { - map(filteredPlayListByDuration, (playlist) => ( + map(playlists, (playlist) => ( - ) -} + ), +) diff --git a/src/features/MatchSidePlaylists/components/TabVideo/components/VideoDate/index.tsx b/src/features/MatchSidePlaylists/components/Matches/components/VideoDate/index.tsx similarity index 100% rename from src/features/MatchSidePlaylists/components/TabVideo/components/VideoDate/index.tsx rename to src/features/MatchSidePlaylists/components/Matches/components/VideoDate/index.tsx diff --git a/src/features/MatchSidePlaylists/components/TabVideo/components/VideoDate/styled.tsx b/src/features/MatchSidePlaylists/components/Matches/components/VideoDate/styled.tsx similarity index 100% rename from src/features/MatchSidePlaylists/components/TabVideo/components/VideoDate/styled.tsx rename to src/features/MatchSidePlaylists/components/Matches/components/VideoDate/styled.tsx diff --git a/src/features/MatchSidePlaylists/components/TabVideo/index.tsx b/src/features/MatchSidePlaylists/components/Matches/index.tsx similarity index 90% rename from src/features/MatchSidePlaylists/components/TabVideo/index.tsx rename to src/features/MatchSidePlaylists/components/Matches/index.tsx index 2a8740d6..11d9d7de 100644 --- a/src/features/MatchSidePlaylists/components/TabVideo/index.tsx +++ b/src/features/MatchSidePlaylists/components/Matches/index.tsx @@ -23,13 +23,15 @@ import { VideoDate } from './components/VideoDate' import { MatchesWrapper } from './styled' type Props = { + additionalScrollHeight: number, profile: MatchInfo, tournamentData: TournamentData, } const formatDate = (date: Date) => format(date, 'yyyy-MM-dd') -export const TabVideo = ({ +export const Matches = ({ + additionalScrollHeight, profile, tournamentData, }: Props) => { @@ -75,7 +77,7 @@ export const TabVideo = ({ const hasScroll = scrollHeight > clientHeight setOverflow(hasScroll) - }, [ref, selectedDate]) + }, [ref.current?.clientHeight, selectedDate]) if (tournamentData.matches.length <= 1) return null @@ -88,7 +90,11 @@ export const TabVideo = ({ profileDate={profileDate} onDateClick={setSelectedDate} /> - + { map(sortBy(matches, ({ live }) => !live), (match) => ( ` +type MatchesWrapperProps = { + additionalScrollHeight: number, + hasScroll?: boolean, +} + +export const MatchesWrapper = styled.div` overflow-y: auto; - max-height: calc(100vh - 170px); + max-height: calc(100vh - 165px - ${({ additionalScrollHeight }) => additionalScrollHeight}px); padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')}; > * { diff --git a/src/features/MatchSidePlaylists/components/PlayersPlaylists/index.tsx b/src/features/MatchSidePlaylists/components/PlayersPlaylists/index.tsx index d05ac36e..22b69298 100644 --- a/src/features/MatchSidePlaylists/components/PlayersPlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersPlaylists/index.tsx @@ -15,12 +15,10 @@ import type { } from 'features/MatchPage/types' import { Name } from 'features/Name' -import { T9n } from 'features/T9n' import { isEqual } from '../../helpers' import { PlayButton } from '../PlayButton' -import { BlockTitle } from '../../styled' import { Wrapper, List, @@ -58,9 +56,6 @@ export const PlayersPlaylists = ({ return ( - - - { + const [sortCondition, setSortCondition] = useState({ dir: 'asc', paramId: null }) + + const { + getFullName, + getPlayerParams, + players, + } = usePlayers({ sortCondition, teamId }) + + const { + containerRef, + firstColumnWidth, + getDisplayedValue, + handleScroll, + handleSortClick, + isExpanded, + paramColumnWidth, + params, + showExpandButton, + showLeftArrow, + showRightArrow, + slideLeft, + slideRight, + tableWrapperRef, + toggleIsExpanded, + } = useTable({ + setSortCondition, + teamId, + }) + + return { + containerRef, + firstColumnWidth, + getDisplayedValue, + getFullName, + getPlayerParams, + handleScroll, + handleSortClick, + isExpanded, + paramColumnWidth, + params, + players, + showExpandButton, + showLeftArrow, + showRightArrow, + slideLeft, + slideRight, + sortCondition, + tableWrapperRef, + toggleIsExpanded, + } +} diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx new file mode 100644 index 00000000..ff2b8e3a --- /dev/null +++ b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx @@ -0,0 +1,83 @@ +import { useMemo, useCallback } from 'react' + +import orderBy from 'lodash/orderBy' +import isNil from 'lodash/isNil' +import trim from 'lodash/trim' + +import type { Player, PlayerParam } from 'requests' + +import { useToggle } from 'hooks' + +import { useMatchPageStore } from 'features/MatchPage/store' +import { useLexicsStore } from 'features/LexicsStore' + +import type { SortCondition } from '../types' + +type UsePlayersArgs = { + sortCondition: SortCondition, + teamId: number, +} + +export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => { + const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle() + const { + playersData, + playersStats, + profile: matchProfile, + } = useMatchPageStore() + const { suffix } = useLexicsStore() + + const getPlayerParams = useCallback( + (playerId: number) => playersStats[teamId][playerId] || {}, + [playersStats, teamId], + ) + + const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : val) + + const getFullName = useCallback((player: Player) => ( + trim(`${player[`firstname_${suffix}`]} ${player[`lastname_${suffix}`]}`) + ), [suffix]) + + const getParamValue = useCallback((playerId: number, paramId: number) => { + const playerParams = getPlayerParams(playerId) + const { val } = playerParams[paramId] || {} + + return val + }, [getPlayerParams]) + + const sortedPlayers = useMemo(() => { + const players = playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2'] + + return isNil(sortCondition.paramId) + ? orderBy(players, getFullName) + : orderBy( + players, + [ + (player) => { + const paramValue = getParamValue(player.id, sortCondition.paramId!) + + return isNil(paramValue) ? -1 : paramValue + }, + getFullName, + ], + sortCondition.dir, + ) + }, [ + getFullName, + getParamValue, + playersData, + matchProfile?.team1.id, + sortCondition.dir, + sortCondition.paramId, + teamId, + ]) + + return { + getDisplayedValue, + getFullName, + getPlayerParams, + isExpanded, + players: sortedPlayers, + toggleIsExpanded, + } +} diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx new file mode 100644 index 00000000..5bc2f55c --- /dev/null +++ b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx @@ -0,0 +1,171 @@ +import type { + SyntheticEvent, + Dispatch, + SetStateAction, +} from 'react' +import { + useRef, + useState, + useEffect, + useMemo, +} from 'react' + +import size from 'lodash/size' +import isNil from 'lodash/isNil' +import reduce from 'lodash/reduce' +import forEach from 'lodash/forEach' +import values from 'lodash/values' +import round from 'lodash/round' +import map from 'lodash/map' + +import { isMobileDevice } from 'config' + +import type { PlayerParam, PlayersStats } from 'requests' + +import { useToggle } from 'hooks' + +import { useMatchPageStore } from 'features/MatchPage/store' +import { useLexicsConfig } from 'features/LexicsStore' + +import type { SortCondition } from '../types' +import { + PARAM_COLUMN_WIDTH, + DISPLAYED_PARAMS_COLUMNS, + FIRST_COLUMN_WIDTH_DEFAULT, + SCROLLBAR_WIDTH, +} from '../config' + +type UseTableArgs = { + setSortCondition: Dispatch>, + teamId: number, +} + +type HeaderParam = Pick + +export const useTable = ({ + setSortCondition, + teamId, +}: UseTableArgs) => { + const containerRef = useRef(null) + const tableWrapperRef = useRef(null) + + const [showLeftArrow, setShowLeftArrow] = useState(false) + const [showRightArrow, setShowRightArrow] = useState(false) + + const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle() + const { playersStats } = useMatchPageStore() + + const params = useMemo(() => ( + reduce>( + playersStats[teamId], + (acc, curr) => { + forEach(values(curr), ({ + id, + lexic, + lexica_short, + }) => { + acc[id] = acc[id] || { + id, + lexic, + lexica_short, + } + }) + + return acc + }, + {}, + ) + ), [playersStats, teamId]) + + const lexics = useMemo(() => ( + reduce>( + values(params), + (acc, { lexic, lexica_short }) => { + if (lexic) acc.push(lexic) + if (lexica_short) acc.push(lexica_short) + + return acc + }, + [], + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + ), [map(params, 'id').sort().join('')]) + + useLexicsConfig(lexics) + + const paramsCount = size(params) + + const getParamColumnWidth = () => { + const rest = ( + (containerRef.current?.clientWidth || 0) - FIRST_COLUMN_WIDTH_DEFAULT - SCROLLBAR_WIDTH + ) + const desktopWith = PARAM_COLUMN_WIDTH + const mobileWidth = paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : rest / DISPLAYED_PARAMS_COLUMNS + + return isMobileDevice ? mobileWidth : desktopWith + } + + const getFirstColumnWidth = () => { + if (isExpanded) return 0 + + return paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : FIRST_COLUMN_WIDTH_DEFAULT + } + + const paramColumnWidth = getParamColumnWidth() + const firstColumnWidth = getFirstColumnWidth() + + const slideLeft = () => tableWrapperRef.current?.scrollBy(-paramColumnWidth, 0) + const slideRight = () => tableWrapperRef.current?.scrollBy(paramColumnWidth, 0) + + const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : round(val, 2)) + + const handleScroll = (e: SyntheticEvent) => { + const { + clientWidth, + scrollLeft, + scrollWidth, + } = e.currentTarget + + const scrollRight = scrollWidth - (scrollLeft + clientWidth) + + setShowLeftArrow(scrollLeft > 0) + setShowRightArrow(scrollRight > 0) + } + + const handleSortClick = (paramId: number) => () => { + setSortCondition((curr) => ({ + dir: curr.dir === 'asc' || curr.paramId !== paramId ? 'desc' : 'asc', + paramId, + })) + } + + useEffect(() => { + const { + clientWidth = 0, + scrollLeft = 0, + scrollWidth = 0, + } = tableWrapperRef.current || {} + + const scrollRight = scrollWidth - (scrollLeft + clientWidth) + + setShowRightArrow(scrollRight > 0) + }, [isExpanded]) + + return { + containerRef, + firstColumnWidth, + getDisplayedValue, + handleScroll, + handleSortClick, + isExpanded, + paramColumnWidth, + params, + showExpandButton: !isMobileDevice && paramsCount > DISPLAYED_PARAMS_COLUMNS, + showLeftArrow, + showRightArrow, + slideLeft, + slideRight, + tableWrapperRef, + toggleIsExpanded, + } +} diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx index 46520e1b..9e5ce602 100644 --- a/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx @@ -1,243 +1,166 @@ +import { Fragment } from 'react' + +import map from 'lodash/map' +import includes from 'lodash/includes' + +import { PlayerParam } from 'requests' + +import { T9n } from 'features/T9n' + +import type { PlayersTableProps } from './types' +import { usePlayersTable } from './hooks' import { + Container, + TableWrapper, Table, - Thead, - Th, - Tbody, - Tr, - Td, + FirstColumn, + Cell, + Row, PlayerNum, + PlayerNameWrapper, + PlayerName, ParamShortTitle, + ArrowButtonRight, + ArrowButtonLeft, + Arrow, + ExpandButton, + Tooltip, } from './styled' -export const PlayersTable = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Min - - Pt - - Reb - - Ass - - To -
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
57 Selikhov9712214
-) +export const PlayersTable = (props: PlayersTableProps) => { + const { + containerRef, + firstColumnWidth, + getDisplayedValue, + getFullName, + getPlayerParams, + handleScroll, + handleSortClick, + isExpanded, + paramColumnWidth, + params, + players, + showExpandButton, + showLeftArrow, + showRightArrow, + slideLeft, + slideRight, + sortCondition, + tableWrapperRef, + toggleIsExpanded, + } = usePlayersTable(props) + + return ( + + + {!isExpanded && ( + + {showLeftArrow && ( + + + + )} + {showRightArrow && ( + + + + )} + + )} + + + + {showExpandButton && ( + + + + + )} + + + {map(players, (player) => { + const fullName = getFullName(player) + + return ( + + + + {player.club_shirt_num} + {' '} + + + {fullName} + + + {fullName} + + + + + ) + })} + + + + {map(params, ({ + id, + lexic, + lexica_short, + }) => ( + + + + + + + ))} + + + {map(players, (player) => ( + + {map(params, ({ id }) => { + const playerParam = getPlayerParams(player.id)[id] as PlayerParam | undefined + const value = playerParam ? getDisplayedValue(playerParam) : '-' + const clickable = Boolean(playerParam?.clickable) && !includes([0, '-'], value) + const sorted = sortCondition.paramId === id + + return ( + + {value} + + ) + })} + + ))} +
+
+
+ ) +} diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx index 3d88c1f6..a0d9f33d 100644 --- a/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx @@ -1,72 +1,133 @@ import styled, { css } from 'styled-components/macro' +import { isMobileDevice } from 'config' + import { customScrollbar } from 'features/Common' +import { TooltipWrapper } from 'features/Tooltip' +import { + ArrowButton as ArrowButtonBase, + Arrow as ArrowBase, +} from 'features/HeaderFilters/components/DateFilter/styled' +import { T9n } from 'features/T9n' + +type ContainerProps = { + isExpanded?: boolean, +} -export const Table = styled.table` - width: 100%; +export const Container = styled.div` + ${({ isExpanded }) => (isExpanded + ? '' + : css` + position: relative; + `)} +` + +type TableWrapperProps = { + isExpanded?: boolean, +} + +export const TableWrapper = styled.div` + display: flex; + max-width: 100%; + max-height: calc(100vh - 235px); border-radius: 5px; - border-collapse: collapse; - letter-spacing: -0.078px; + overflow-x: auto; + scroll-behavior: smooth; background-color: #333333; - table-layout: fixed; - + z-index: 50; ${customScrollbar} -` -export const Thead = styled.thead` - height: 45px; - border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); + ${({ isExpanded }) => (isExpanded + ? css` + position: absolute; + right: 14px; + ` + : '')} ` -type ThProps = { - sorted?: boolean, -} +export const Table = styled.div` + flex-grow: 1; + border-radius: 5px; + border-collapse: collapse; + letter-spacing: -0.078px; +` -export const Th = styled.th` +export const Tooltip = styled(TooltipWrapper)` + left: auto; + padding: 2px 10px; + border-radius: 6px; + transform: none; font-size: 11px; - color: ${({ theme }) => theme.colors.white}; - text-transform: uppercase; + line-height: 1; + color: ${({ theme }) => theme.colors.black}; - :first-child { - width: 115px; + ::before { + display: none; } - - ${({ sorted }) => (sorted - ? '' - : css` - opacity: 0.5; - `)} ` -export const ParamShortTitle = styled.span`` - -export const Tbody = styled.tbody`` +export const ParamShortTitle = styled(T9n)` + text-transform: uppercase; +` -export const Tr = styled.tr` +export const Row = styled.div` + display: flex; + width: 100%; height: 45px; border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); - :last-child { - border-bottom: none; + :first-child { + position: sticky; + left: 0; + top: 0; + z-index: 1; } ` type TdProps = { clickable?: boolean, + columnWidth?: number, + headerCell?: boolean, sorted?: boolean, } -export const Td = styled.td` +export const Cell = styled.div.attrs(({ clickable }: TdProps) => ({ + ...clickable && { tabIndex: 0 }, +}))` + position: relative; + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')}; font-size: 11px; - text-align: center; - color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; + color: ${({ + clickable, + headerCell, + theme, + }) => (clickable && !headerCell ? '#5EB2FF' : theme.colors.white)}; + white-space: nowrap; + background-color: #333333; - :first-child { - padding-left: 13px; - text-align: left; - color: ${({ theme }) => theme.colors.white}; + ${Tooltip} { + top: 35px; + } + + :hover { + ${Tooltip} { + display: block; + } } + ${({ headerCell }) => (headerCell + ? '' + : css` + :first-child { + justify-content: unset; + padding-left: 13px; + color: ${({ theme }) => theme.colors.white}; + } + `)} ${({ sorted }) => (sorted ? css` @@ -74,13 +135,107 @@ export const Td = styled.td` ` : '')} - ${({ clickable }) => (clickable + ${({ clickable, headerCell }) => (clickable || headerCell ? css` cursor: pointer; ` : '')} ` +type FirstColumnProps = { + columnWidth?: number, +} + +export const FirstColumn = styled.div` + position: sticky; + left: 0; + width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')}; +` + export const PlayerNum = styled.span` + display: inline-block; + width: 20px; + flex-shrink: 0; + text-align: center; color: rgba(255, 255, 255, 0.5); ` + +type PlayerNameProps = { + columnWidth?: number, +} + +export const PlayerName = styled.span` + display: inline-block; + margin-top: 2px; + text-overflow: ellipsis; + overflow: hidden; + + ${({ columnWidth }) => (columnWidth + ? css` + max-width: calc(${columnWidth}px - 31px); + ` + : css` + max-width: 110px; + `)} +` + +export const PlayerNameWrapper = styled.span` + position: relative; + + ${Tooltip} { + top: 15px; + } + + :hover { + ${Tooltip} { + display: block; + } + } +` + +const ArrowButton = styled(ArrowButtonBase)` + position: absolute; + width: 17px; + margin-top: 2px; + background-color: #333333; + z-index: 3; + + ${isMobileDevice + ? css` + height: 45px; + margin-top: 0; + ` + : ''}; +` + +export const ArrowButtonRight = styled(ArrowButton)` + right: 0; +` + +export const ArrowButtonLeft = styled(ArrowButton)` + left: 75px; +` + +export const Arrow = styled(ArrowBase)` + width: 10px; + height: 10px; + + ${isMobileDevice + ? css` + border-color: ${({ theme }) => theme.colors.white}; + ` + : ''}; +` + +export const ExpandButton = styled(ArrowButton)` + left: 20px; + top: 0; + + ${Arrow} { + left: 0; + + :last-child { + margin-left: 7px; + } + } +` diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/types.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/types.tsx new file mode 100644 index 00000000..85b9d054 --- /dev/null +++ b/src/features/MatchSidePlaylists/components/PlayersTable/types.tsx @@ -0,0 +1,8 @@ +export type PlayersTableProps = { + teamId: number, +} + +export type SortCondition = { + dir: 'asc' | 'desc', + paramId: number | null, +} diff --git a/src/features/MatchSidePlaylists/components/TabStats/config.tsx b/src/features/MatchSidePlaylists/components/TabStats/config.tsx index 099a117f..3c14e7ff 100644 --- a/src/features/MatchSidePlaylists/components/TabStats/config.tsx +++ b/src/features/MatchSidePlaylists/components/TabStats/config.tsx @@ -3,3 +3,8 @@ export enum Tabs { TEAM1, TEAM2, } + +export enum StatsType { + FINAL_STATS, + CURRENT_STATS, +} diff --git a/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx b/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx new file mode 100644 index 00000000..1d621ce3 --- /dev/null +++ b/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react' + +import isEmpty from 'lodash/isEmpty' + +import { useMatchPageStore } from 'features/MatchPage/store' + +import { StatsType, Tabs } from './config' + +export const useTabStats = () => { + const [selectedTab, setSelectedTab] = useState(Tabs.TEAMS) + + const { + isEmptyPlayersStats, + profile: matchProfile, + setStatsType, + statsType, + teamsStats, + } = useMatchPageStore() + + const isFinalStatsType = statsType === StatsType.FINAL_STATS + + const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats' + const tooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video' + + const isVisibleTeamsTab = !isEmpty(teamsStats) + const isVisibleTeam1PlayersTab = Boolean( + matchProfile && !isEmptyPlayersStats(matchProfile.team1.id), + ) + const isVisibleTeam2PlayersTab = Boolean( + matchProfile && !isEmptyPlayersStats(matchProfile.team2.id), + ) + + const toggleStatsType = () => { + const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS + + setStatsType(newStatsType) + } + + useEffect(() => { + switch (true) { + case isVisibleTeamsTab: + setSelectedTab(Tabs.TEAMS) + break + + case isVisibleTeam1PlayersTab: + setSelectedTab(Tabs.TEAM1) + break + + case isVisibleTeam2PlayersTab: + setSelectedTab(Tabs.TEAM2) + break + + default: + } + }, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab]) + + return { + isFinalStatsType, + isVisibleTeam1PlayersTab, + isVisibleTeam2PlayersTab, + isVisibleTeamsTab, + selectedTab, + setSelectedTab, + switchTitleLexic, + toggleStatsType, + tooltipLexic, + } +} diff --git a/src/features/MatchSidePlaylists/components/TabStats/index.tsx b/src/features/MatchSidePlaylists/components/TabStats/index.tsx index c43592bb..d5b20d74 100644 --- a/src/features/MatchSidePlaylists/components/TabStats/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabStats/index.tsx @@ -1,7 +1,16 @@ +import { isMobileDevice } from 'config/userAgent' + +import { getTeamAbbr } from 'helpers' + import { Tooltip } from 'features/Tooltip' +import { T9n } from 'features/T9n' +import { useMatchPageStore } from 'features/MatchPage/store' +import { Name } from 'features/Name' +import { Tabs } from './config' +import { useTabStats } from './hooks' import { PlayersTable } from '../PlayersTable' -import { TeamsStats } from '../TeamsStats' +import { TeamsStatsTable } from '../TeamsStatsTable' import { Container, @@ -13,22 +22,82 @@ import { SwitchButton, } from './styled' -export const TabStats = () => ( - -
- - Teams - DIN - SPA - - - Final Stats - - - - -
- - {/* */} -
-) +const tabPanes = { + [Tabs.TEAMS]: TeamsStatsTable, + [Tabs.TEAM1]: PlayersTable, + [Tabs.TEAM2]: PlayersTable, +} + +export const TabStats = () => { + const { + isFinalStatsType, + isVisibleTeam1PlayersTab, + isVisibleTeam2PlayersTab, + isVisibleTeamsTab, + selectedTab, + setSelectedTab, + switchTitleLexic, + toggleStatsType, + tooltipLexic, + } = useTabStats() + const { profile: matchProfile } = useMatchPageStore() + + const TabPane = tabPanes[selectedTab] + + if (!matchProfile) return null + + const { team1, team2 } = matchProfile + + return ( + +
+ + {isVisibleTeamsTab && ( + setSelectedTab(Tabs.TEAMS)} + > + + + )} + {isVisibleTeam1PlayersTab && ( + setSelectedTab(Tabs.TEAM1)} + > + + + )} + {isVisibleTeam2PlayersTab && ( + setSelectedTab(Tabs.TEAM2)} + > + + + )} + + + + + {!isMobileDevice && } + + +
+ +
+ ) +} diff --git a/src/features/MatchSidePlaylists/components/TabStats/styled.tsx b/src/features/MatchSidePlaylists/components/TabStats/styled.tsx index f2f58203..13318a33 100644 --- a/src/features/MatchSidePlaylists/components/TabStats/styled.tsx +++ b/src/features/MatchSidePlaylists/components/TabStats/styled.tsx @@ -1,6 +1,7 @@ import styled, { css } from 'styled-components/macro' import { TooltipWrapper } from 'features/Tooltip' +import { T9n } from 'features/T9n' export const Container = styled.div`` @@ -14,42 +15,40 @@ export const TabList = styled.div.attrs({ role: 'tablist' })` display: flex; ` -type TabProps = { - selected?: boolean, -} - -export const Tab = styled.div.attrs(({ selected }: TabProps) => ({ - 'aria-pressed': selected, - role: 'tab', -}))` +export const Tab = styled.button.attrs({ role: 'tab' })` display: flex; justify-content: space-between; align-items: center; - padding: 0 15px 10px; + padding: 0 10px 10px; font-size: 12px; color: ${({ theme }) => theme.colors.white}; - opacity: ${({ selected }) => (selected ? '1' : '0.4')}; + opacity: 0.4; cursor: pointer; + border: none; + background: none; border-bottom: 2px solid transparent; - - ${({ selected, theme }) => (selected - ? css` - border-color: ${theme.colors.white}; - ` - : '')} + &[aria-pressed="true"] { + opacity: 1; + border-color: currentColor; + } ` export const Switch = styled.div` display: flex; ` -export const SwitchTitle = styled.span` +export const SwitchTitle = styled(T9n)` font-size: 12px; color: ${({ theme }) => theme.colors.white}; + white-space: nowrap; ` -export const SwitchButton = styled.button` +type SwitchButtonProps = { + isFinalStatsType: boolean, +} + +export const SwitchButton = styled.button` position: relative; width: 20px; height: 7px; @@ -63,9 +62,12 @@ export const SwitchButton = styled.button` ${TooltipWrapper} { left: auto; right: 0; + top: 15px; padding: 2px 10px; border-radius: 6px; transform: none; + font-size: 11px; + line-height: 1; ::before { display: none; @@ -78,7 +80,7 @@ export const SwitchButton = styled.button` } } - ${({ theme }) => (true // Позже будет добавлен пропс + ${({ isFinalStatsType, theme }) => (!isFinalStatsType ? css` background-image: linear-gradient( to right, diff --git a/src/features/MatchSidePlaylists/components/TabWatch/index.tsx b/src/features/MatchSidePlaylists/components/TabWatch/index.tsx index 3c1acf33..368e957e 100644 --- a/src/features/MatchSidePlaylists/components/TabWatch/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabWatch/index.tsx @@ -1,6 +1,11 @@ -import { Fragment } from 'react' +import { + Fragment, + useMemo, + useRef, +} from 'react' import size from 'lodash/size' +import filter from 'lodash/filter' import type { PlaylistOption, @@ -10,12 +15,13 @@ import type { import type { MatchInfo } from 'requests' import { DropdownSection } from '../DropdownSection' -import { MatchPlaylists } from '../MatchPlaylists' +import { MatchPlaylists, LIST_ITEM_INDENT } from '../MatchPlaylists' import { SideInterviews } from '../SideInterviews' -import { TabVideo } from '../../components/TabVideo' +import { Matches } from '../Matches' type Props = { onSelect: (option: PlaylistOption) => void, + playListFilter: number, playlists: Playlists, profile: MatchInfo, selectedPlaylist?: PlaylistOption, @@ -24,31 +30,50 @@ type Props = { export const TabWatch = ({ onSelect, + playListFilter, playlists, profile, selectedPlaylist, tournamentData, -}: Props) => ( - - - - { + const matchPlaylistsRef = useRef(null) + + const additionalScrollHeight = (matchPlaylistsRef.current?.clientHeight || 0) + LIST_ITEM_INDENT + + const filteredPlayListByDuration = useMemo(() => ( + filter(playlists.match, (playlist) => ( + profile?.live + ? Boolean(playlist.duration) || (playlist.id === 'full_game') + : Boolean(playlist.duration) + )) + ), [playlists.match, profile?.live]) + + return ( + + {playListFilter > 1 && ( + + )} + + + + - - - -) +
+ ) +} diff --git a/src/features/MatchSidePlaylists/components/TeamsStats/index.tsx b/src/features/MatchSidePlaylists/components/TeamsStats/index.tsx deleted file mode 100644 index 17a1dad5..00000000 --- a/src/features/MatchSidePlaylists/components/TeamsStats/index.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import { - Container, - Row, - TeamShortName, - ParamValueContainer, - ParamValue, - ParamTitle, -} from './styled' - -export const TeamsStats = () => ( - - - DIN - SPA - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - - - 90 - - Points - 123 - - -) diff --git a/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx new file mode 100644 index 00000000..d4698aea --- /dev/null +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx @@ -0,0 +1,31 @@ +import isNumber from 'lodash/isNumber' +import find from 'lodash/find' +import round from 'lodash/round' + +import type { Param } from 'requests' + +import { useMatchPageStore } from 'features/MatchPage/store' + +export const useTeamsStatsTable = () => { + const { profile, teamsStats } = useMatchPageStore() + + const getDisplayedValue = (val: any) => ( + isNumber(val) ? round(val, 2) : '-' + ) + + const getStatItemById = (paramId: number) => { + if (!profile) return null + + return find(teamsStats[profile?.team2.id], ({ param1 }) => param1.id === paramId) || null + } + + const isClickable = (param: Param) => ( + Boolean(param.val) && param.clickable + ) + + return { + getDisplayedValue, + getStatItemById, + isClickable, + } +} diff --git a/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx new file mode 100644 index 00000000..610d2c8c --- /dev/null +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx @@ -0,0 +1,93 @@ +import { Fragment } from 'react' + +import map from 'lodash/map' + +import { useMatchPageStore } from 'features/MatchPage/store' +import { useLexicsStore } from 'features/LexicsStore' + +import { useTeamsStatsTable } from './hooks' +import { + Container, + Row, + TeamShortName, + ParamValueContainer, + ParamValue, + StatItemTitle, + Divider, +} from './styled' + +export const TeamsStatsTable = () => { + const { profile, teamsStats } = useMatchPageStore() + const { + getDisplayedValue, + getStatItemById, + isClickable, + } = useTeamsStatsTable() + const { lang } = useLexicsStore() + + if (!profile) return null + + return ( + + + + + + + {map(teamsStats[profile.team1.id], (team1StatItem) => { + const team2StatItem = getStatItemById(team1StatItem.param1.id) + const statItemTitle = team1StatItem[`name_${lang === 'ru' ? 'ru' : 'en'}`] + + return ( + + + + {getDisplayedValue(team1StatItem.param1.val)} + + {team1StatItem.param2 && ( + + / + + {getDisplayedValue(team1StatItem.param2.val)} + + + )} + + + {statItemTitle} + + {team2StatItem && ( + + + {getDisplayedValue(team2StatItem.param1.val)} + + {team2StatItem.param2 && ( + + / + + {getDisplayedValue(team2StatItem.param2.val)} + + + )} + + )} + + ) + })} + + ) +} diff --git a/src/features/MatchSidePlaylists/components/TeamsStats/styled.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx similarity index 68% rename from src/features/MatchSidePlaylists/components/TeamsStats/styled.tsx rename to src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx index 222b9918..39b3a3c7 100644 --- a/src/features/MatchSidePlaylists/components/TeamsStats/styled.tsx +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx @@ -1,16 +1,16 @@ import styled, { css } from 'styled-components/macro' -import { customScrollbar } from 'features/Common' +import { Name } from 'features/Name' export const Container = styled.div` width: 100%; font-size: 11px; + overflow: hidden; + border-radius: 5px; background-color: #333333; - - ${customScrollbar} ` -export const TeamShortName = styled.span` +export const TeamShortName = styled(Name)` color: ${({ theme }) => theme.colors.white}; letter-spacing: -0.078px; text-transform: uppercase; @@ -37,7 +37,10 @@ type TParamValue = { clickable?: boolean, } -export const ParamValue = styled.span` +export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({ + ...clickable && { tabIndex: 0 }, +}))` + font-weight: 600; color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; ${({ clickable }) => (clickable @@ -47,10 +50,16 @@ export const ParamValue = styled.span` : '')} ` -export const ParamTitle = styled.span` +export const StatItemTitle = styled.span` color: ${({ theme }) => theme.colors.white}; letter-spacing: -0.078px; text-transform: uppercase; font-weight: 600; opacity: 0.5; ` + +export const Divider = styled.span` + color: ${({ theme }) => theme.colors.white}; + opacity: 0.5; + font-weight: 600; +` diff --git a/src/features/MatchSidePlaylists/hooks.tsx b/src/features/MatchSidePlaylists/hooks.tsx index 689c6fe5..17e889c5 100644 --- a/src/features/MatchSidePlaylists/hooks.tsx +++ b/src/features/MatchSidePlaylists/hooks.tsx @@ -5,6 +5,8 @@ import { } from 'react' import reduce from 'lodash/reduce' +import isEmpty from 'lodash/isEmpty' +import compact from 'lodash/compact' import { useMatchPageStore } from 'features/MatchPage/store' @@ -14,26 +16,54 @@ export const useMatchSidePlaylists = () => { const { closePopup, events, + isEmptyPlayersStats, matchPlaylists: playlists, + profile: matchProfile, + teamsStats, + tournamentData, } = useMatchPageStore() const [selectedTab, setSelectedTab] = useState(Tabs.WATCH) - const isWatchTabVisible = useMemo(() => { - const playListFilter = reduce( - playlists.match, - (acc, item) => { - let result = acc - if (item.duration) result++ - return result - }, - 0, - ) - return playListFilter > 1 - }, [playlists]) + + const playListFilter = useMemo(() => reduce( + playlists.match, + (acc, item) => { + let result = acc + if (item.duration) result++ + return result + }, + 0, + ), [playlists.match]) + + const isWatchTabVisible = useMemo(() => ( + playListFilter > 1 || tournamentData.matchDates.length > 1 + ), [playListFilter, tournamentData.matchDates.length]) const isEventTabVisible = useMemo(() => ( events.length > 0 ), [events]) + const isPlayersTabVisible = useMemo(() => ( + !isEmpty(playlists.players.team1) + ), [playlists.players.team1]) + + const isStatsTabVisible = useMemo(() => ( + !isEmpty(teamsStats) + || (matchProfile?.team1.id && !isEmptyPlayersStats(matchProfile.team1.id)) + || (matchProfile?.team2.id && !isEmptyPlayersStats(matchProfile.team2.id)) + ), [ + isEmptyPlayersStats, + matchProfile?.team1.id, + matchProfile?.team2.id, + teamsStats, + ]) + + const hasLessThanFourTabs = compact([ + isWatchTabVisible, + isEventTabVisible, + isPlayersTabVisible, + // isStatsTabVisible, + ]).length < 4 + useEffect(() => { switch (true) { case isWatchTabVisible: @@ -42,18 +72,32 @@ export const useMatchSidePlaylists = () => { case isEventTabVisible: setSelectedTab(Tabs.EVENTS) break + case isPlayersTabVisible: + setSelectedTab(Tabs.PLAYERS) + break + // case isStatsTabVisible: + // setSelectedTab(Tabs.STATS) + // break } - }, [isEventTabVisible, isWatchTabVisible]) + }, [ + isEventTabVisible, + isPlayersTabVisible, + // isStatsTabVisible, + isWatchTabVisible, + ]) useEffect(() => { if (selectedTab !== Tabs.EVENTS) closePopup() }, [selectedTab, closePopup]) return { + hasLessThanFourTabs, isEventTabVisible, - isStatsTabVisible: true, + isPlayersTabVisible, + isStatsTabVisible, isWatchTabVisible, onTabClick: setSelectedTab, + playListFilter, selectedTab, } } diff --git a/src/features/MatchSidePlaylists/index.tsx b/src/features/MatchSidePlaylists/index.tsx index 25b08a12..e50cafe2 100644 --- a/src/features/MatchSidePlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/index.tsx @@ -42,8 +42,6 @@ type Props = { setCircleAnimation?: TSetCircleAnimation, } -const hasLessThanFourTabs = false - export const MatchSidePlaylists = ({ circleAnimation, onSelect, @@ -59,10 +57,13 @@ export const MatchSidePlaylists = ({ } = useMatchPageStore() const { + hasLessThanFourTabs, isEventTabVisible, - isStatsTabVisible, + isPlayersTabVisible, + // isStatsTabVisible, isWatchTabVisible, onTabClick, + playListFilter, selectedTab, } = useMatchSidePlaylists() @@ -108,7 +109,7 @@ export const MatchSidePlaylists = ({ {isWatchTabVisible ? ( onTabClick(Tabs.WATCH)} > @@ -117,37 +118,38 @@ export const MatchSidePlaylists = ({ ) : null} {isEventTabVisible ? ( onTabClick(Tabs.EVENTS)} > ) : null} - {isStatsTabVisible ? ( + {isPlayersTabVisible ? ( onTabClick(Tabs.PLAYERS)} > - + ) : null} - {isStatsTabVisible ? ( + {/* {isStatsTabVisible ? ( onTabClick(Tabs.STATS)} > - ) : null} + ) : null} */} diff --git a/src/features/MatchSidePlaylists/styled.tsx b/src/features/MatchSidePlaylists/styled.tsx index 1a8a9e36..0ba4d49a 100644 --- a/src/features/MatchSidePlaylists/styled.tsx +++ b/src/features/MatchSidePlaylists/styled.tsx @@ -43,7 +43,7 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })` height: 40px; ${Tab} { - justify-content: initial; + justify-content: center; flex-direction: row; gap: 5px; } @@ -56,24 +56,30 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })` : ''}; ` -type TabProps = { - selected?: boolean, -} +export const TabTitle = styled(T9n)` + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + color: ${({ theme }) => theme.colors.white}; +` -export const Tab = styled.div.attrs(({ selected }: TabProps) => ({ - 'aria-pressed': selected, - role: 'tab', -}))` +export const Tab = styled.button.attrs({ role: 'tab' })` display: flex; flex-direction: column; justify-content: space-between; align-items: center; flex: 1; - opacity: ${({ selected }) => (selected ? '1' : '0.4')}; + opacity: 0.4; cursor: pointer; + border: none; + background: none; - :hover { + &[aria-pressed="true"], :hover { opacity: 1; + + ${TabTitle} { + font-weight: 600; + } } ` @@ -82,8 +88,8 @@ type TabIconProps = { } export const TabIcon = styled.div` - width: 20px; - height: 20px; + width: 22px; + height: 22px; background-image: url(/images/matchTabs/${({ icon }) => `${icon}.svg`}); background-repeat: no-repeat; background-position: center; @@ -96,28 +102,22 @@ export const TabIcon = styled.div` : '')} ` -export const TabTitle = styled(T9n)` - font-size: 8px; - text-transform: uppercase; - color: ${({ theme }) => theme.colors.white}; -` - type TContainer = { + forWatchTab?: boolean, hasScroll: boolean, } export const Container = styled.div` width: 320px; - margin-top: 23px; + margin-top: 14px; max-height: calc(100vh - 130px); - overflow-y: ${({ forVideoTab }) => (forVideoTab ? 'hidden' : 'auto')}; - padding-right: ${({ forVideoTab }) => (forVideoTab ? '0' : '')}; + overflow-y: ${({ forWatchTab }) => (forWatchTab ? 'hidden' : 'auto')}; + padding-right: ${({ forWatchTab }) => (forWatchTab ? '0' : '')}; padding-left: 14px; padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')}; ${customScrollbar} - @media ${devices.tablet} { margin-top: 15px; } diff --git a/src/features/MultiSourcePlayer/hooks/index.tsx b/src/features/MultiSourcePlayer/hooks/index.tsx index 22022292..c4e596f8 100644 --- a/src/features/MultiSourcePlayer/hooks/index.tsx +++ b/src/features/MultiSourcePlayer/hooks/index.tsx @@ -56,6 +56,7 @@ export type Props = { chapters: Chapters, isOpenPopup?: boolean, onError?: () => void, + onPlayerProgressChange?: (ms: number) => void, onPlayingChange: (playing: boolean) => void, profile: MatchInfo, setCircleAnimation: TSetCircleAnimation, @@ -64,6 +65,7 @@ export type Props = { export const useMultiSourcePlayer = ({ chapters, onError, + onPlayerProgressChange, onPlayingChange, setCircleAnimation, }: Props) => { @@ -201,6 +203,7 @@ export const useMultiSourcePlayer = ({ timeForStatistics.current = (value + chapter.startMs) / 1000 setPlayerState({ playedProgress: value }) + onPlayerProgressChange?.(playedMs + chapter.startMs) } const onEnded = () => { diff --git a/src/features/StreamPlayer/components/YoutubePlayer/index.tsx b/src/features/StreamPlayer/components/YoutubePlayer/index.tsx index 75c2b2bd..99815ad5 100644 --- a/src/features/StreamPlayer/components/YoutubePlayer/index.tsx +++ b/src/features/StreamPlayer/components/YoutubePlayer/index.tsx @@ -7,7 +7,7 @@ import { PlayerWrapper } from '../../styled' import { useVideoPlayer, Props } from '../../hooks' export const YoutubePlayer = (props: Props) => { - const { isOpenPopup, profile } = useMatchPageStore() + const { isOpenFiltersPopup, profile } = useMatchPageStore() const { onMouseMove, @@ -34,7 +34,7 @@ export const YoutubePlayer = (props: Props) => { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {isOpenPopup && } + {isOpenFiltersPopup && } { - const { isOpenPopup, profile } = useMatchPageStore() + const { isOpenFiltersPopup, profile } = useMatchPageStore() const { user } = useAuthStore() const { @@ -96,7 +96,7 @@ export const StreamPlayer = (props: Props) => { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {isOpenPopup && } + {isOpenFiltersPopup && } diff --git a/src/helpers/getTeamAbbr/index.tsx b/src/helpers/getTeamAbbr/index.tsx new file mode 100644 index 00000000..62a1eb51 --- /dev/null +++ b/src/helpers/getTeamAbbr/index.tsx @@ -0,0 +1,25 @@ +import toUpper from 'lodash/toUpper' +import split from 'lodash/split' +import size from 'lodash/size' + +import pipe from 'lodash/fp/pipe' +import take from 'lodash/fp/take' +import join from 'lodash/fp/join' +import map from 'lodash/fp/map' + +export const getTeamAbbr = (teamName: string) => { + const nameParts = split(teamName, ' ') + + return size(nameParts) > 1 + ? pipe( + map(take(1)), + join(''), + toUpper, + )(nameParts) + + : pipe( + take(3), + join(''), + toUpper, + )(nameParts[0]) +} diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 734a5130..0577a810 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -8,3 +8,4 @@ export * from './secondsToHms' export * from './redirectToUrl' export * from './getRandomString' export * from './selectedApi' +export * from './getTeamAbbr' diff --git a/src/hooks/usePageParams.tsx b/src/hooks/usePageParams.tsx index 4810663c..4d1df1c7 100644 --- a/src/hooks/usePageParams.tsx +++ b/src/hooks/usePageParams.tsx @@ -22,6 +22,7 @@ export const usePageParams = () => { return { profileId: Number(pageId), profileType: ProfileTypes[toUpper(profileName) as keyof typeof ProfileTypes], + sportName, sportType: SportTypes[toUpper(sportName) as keyof typeof SportTypes], } } diff --git a/src/requests/getMatchParticipants.tsx b/src/requests/getMatchParticipants.tsx new file mode 100644 index 00000000..645c3c03 --- /dev/null +++ b/src/requests/getMatchParticipants.tsx @@ -0,0 +1,65 @@ +import isUndefined from 'lodash/isUndefined' + +import { SportTypes } from 'config' + +import { callApi } from 'helpers' + +export type Player = { + birthday: string | null, + c_country: number, + c_gender: number, + club_f_team: number, + club_shirt_num: number, + firstname_eng: string, + firstname_national: string | null, + firstname_rus: string, + height: number | null, + id: number, + is_gk: boolean, + lastname_eng: string, + lastname_national: string | null, + lastname_rus: string, + national_f_team: number | null, + national_shirt_num: number, + nickname_eng: string | null, + nickname_rus: string | null, + weight: number | null, +} + +type DataItem = { + players: Array, + team_id: number, +} + +type Response = { + data?: Array, + error?: { + code: string, + message: string, + }, +} + +type GetMatchParticipantsArgs = { + matchId: number, + second?: number, + sportType: SportTypes, +} + +export const getMatchParticipants = async ({ + matchId, + second, + sportType, +}: GetMatchParticipantsArgs) => { + const config = { + method: 'GET', + } + + const response: Response = await callApi({ + config, + url: `http://136.243.17.103:8888/ask/participants?sport_id=${sportType}&match_id=${matchId}${isUndefined(second) ? '' : `&second=${second}`}`, + }) + + if (response.error) Promise.reject(response) + + return Promise.resolve(response.data || []) +} diff --git a/src/requests/getPlayersStats.tsx b/src/requests/getPlayersStats.tsx new file mode 100644 index 00000000..3fb5d93f --- /dev/null +++ b/src/requests/getPlayersStats.tsx @@ -0,0 +1,54 @@ +import isUndefined from 'lodash/isUndefined' + +import { callApi } from 'helpers' + +export type PlayerParam = { + clickable: boolean, + data_type: string, + id: number, + lexic: number, + lexica_short: number | null, + markers: Array | null, + name_en: string, + name_ru: string, + val: number | null, +} + +export type PlayersStats = { + [playerId: string]: { + [paramId: string]: PlayerParam, + }, +} + +type Response = { + data?: PlayersStats, + error?: string, + message?: string, +} + +type GetPlayersStatsArgs = { + matchId: number, + second?: number, + sportName: string, + teamId: number, +} + +export const getPlayersStats = async ({ + matchId, + second, + sportName, + teamId, +}: GetPlayersStatsArgs) => { + const config = { + method: 'GET', + } + + const response: Response = await callApi({ + config, + url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/${teamId}/players/stats${isUndefined(second) ? '' : `?second=${second}`}`, + }) + + if (response.error) Promise.reject(response) + + return Promise.resolve(response.data || {}) +} diff --git a/src/requests/getTeamsStats.tsx b/src/requests/getTeamsStats.tsx new file mode 100644 index 00000000..acdf40bd --- /dev/null +++ b/src/requests/getTeamsStats.tsx @@ -0,0 +1,56 @@ +import isUndefined from 'lodash/isUndefined' + +import { callApi } from 'helpers' + +export type Param = { + clickable: boolean, + data_type: string, + id: number, + lexic: number, + markers: Array, + name_en: string, + name_ru: string, + val: number | null, +} + +export type TeamStatItem = { + lexic: number, + name_en: string, + name_ru: string, + order: number, + param1: Param, + param2: Param | null, +} + +type Response = { + data?: { + [teamId: string]: Array, + }, + error?: string, + message?: string, +} + +type GetTeamsStatsArgs = { + matchId: number, + second?: number, + sportName: string, +} + +export const getTeamsStats = async ({ + matchId, + second, + sportName, +}: GetTeamsStatsArgs) => { + const config = { + method: 'GET', + } + + const response: Response = await callApi({ + config, + url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}`}`, + }) + + if (response.error) Promise.reject(response) + + return Promise.resolve(response.data || {}) +} diff --git a/src/requests/index.tsx b/src/requests/index.tsx index b8074c36..2e54b0ae 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -25,3 +25,6 @@ export * from './getPlayerPlaylists' export * from './getSubscriptions' export * from './buySubscription' export * from './saveMatchStats' +export * from './getTeamsStats' +export * from './getPlayersStats' +export * from './getMatchParticipants'