|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 908 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 329 B |
@ -0,0 +1,3 @@ |
|||||||
|
export enum KEYBOARD_KEYS { |
||||||
|
Enter = 'Enter', |
||||||
|
} |
||||||
@ -1,11 +0,0 @@ |
|||||||
import find from 'lodash/find' |
|
||||||
|
|
||||||
import type { MatchInfo } from 'requests/getMatchInfo' |
|
||||||
|
|
||||||
import { FULL_MATCH_BOUNDARY } from 'features/MatchPage/components/LiveMatch/helpers' |
|
||||||
|
|
||||||
export const calculateDuration = (profile: MatchInfo) => { |
|
||||||
const bound = find(profile?.video_bounds, { h: FULL_MATCH_BOUNDARY }) |
|
||||||
if (!bound) return 0 |
|
||||||
return Number(bound.e) - Number(bound.s) |
|
||||||
} |
|
||||||
@ -0,0 +1,53 @@ |
|||||||
|
import head from 'lodash/head' |
||||||
|
import last from 'lodash/last' |
||||||
|
import inRange from 'lodash/inRange' |
||||||
|
|
||||||
|
import type { VideoBounds } from 'requests' |
||||||
|
|
||||||
|
export const getHalfTime = (videoBounds: VideoBounds, currentTime: number) => { |
||||||
|
const firstBound = head(videoBounds) |
||||||
|
const lastBound = last(videoBounds) |
||||||
|
|
||||||
|
const matchSecond = (Number(firstBound?.s) || 0) + currentTime |
||||||
|
|
||||||
|
let period = 1 |
||||||
|
let second = 1 |
||||||
|
|
||||||
|
if (firstBound === lastBound || matchSecond < (Number(videoBounds[1]?.s || 0))) { |
||||||
|
return { |
||||||
|
period, |
||||||
|
second, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (lastBound?.e && matchSecond > (Number(lastBound.e))) { |
||||||
|
return {} |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 1; i < videoBounds.length; i++) { |
||||||
|
const { e, s } = videoBounds[i] |
||||||
|
|
||||||
|
if (inRange( |
||||||
|
matchSecond, |
||||||
|
Number(s), |
||||||
|
Number(e || 1e5) + 1, |
||||||
|
)) { |
||||||
|
period = i |
||||||
|
second = matchSecond - Number(videoBounds[i].s) |
||||||
|
break |
||||||
|
} else if ( |
||||||
|
videoBounds[i + 1] && inRange( |
||||||
|
matchSecond, |
||||||
|
Number(e) + 1, |
||||||
|
Number(videoBounds[i + 1].s), |
||||||
|
)) { |
||||||
|
period = i + 1 |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
period, |
||||||
|
second, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,201 @@ |
|||||||
|
import type { Dispatch, SetStateAction } from 'react' |
||||||
|
import { |
||||||
|
useMemo, |
||||||
|
useEffect, |
||||||
|
useState, |
||||||
|
} from 'react' |
||||||
|
import { useQueryClient } from 'react-query' |
||||||
|
|
||||||
|
import throttle from 'lodash/throttle' |
||||||
|
import isEmpty from 'lodash/isEmpty' |
||||||
|
import every from 'lodash/every' |
||||||
|
import find from 'lodash/find' |
||||||
|
import isUndefined from 'lodash/isUndefined' |
||||||
|
|
||||||
|
import { querieKeys } from 'config' |
||||||
|
|
||||||
|
import type { MatchScore } from 'requests' |
||||||
|
import { |
||||||
|
MatchInfo, |
||||||
|
PlayersStats, |
||||||
|
Player, |
||||||
|
getPlayersStats, |
||||||
|
getMatchParticipants, |
||||||
|
} from 'requests' |
||||||
|
|
||||||
|
import { useObjectState, usePageParams } from 'hooks' |
||||||
|
|
||||||
|
import type{ PlaylistOption } from 'features/MatchPage/types' |
||||||
|
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' |
||||||
|
import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' |
||||||
|
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' |
||||||
|
|
||||||
|
const REQUEST_DELAY = 3000 |
||||||
|
const STATS_POLL_INTERVAL = 30000 |
||||||
|
|
||||||
|
type UsePlayersStatsArgs = { |
||||||
|
matchProfile: MatchInfo, |
||||||
|
playingProgress: number, |
||||||
|
selectedPlaylist?: PlaylistOption, |
||||||
|
setIsPlayersStatsFetching: Dispatch<SetStateAction<boolean>>, |
||||||
|
statsType: StatsType, |
||||||
|
} |
||||||
|
|
||||||
|
type PlayersData = { |
||||||
|
team1: Array<Player>, |
||||||
|
team2: Array<Player>, |
||||||
|
} |
||||||
|
|
||||||
|
export const usePlayersStats = ({ |
||||||
|
matchProfile, |
||||||
|
playingProgress, |
||||||
|
selectedPlaylist, |
||||||
|
setIsPlayersStatsFetching, |
||||||
|
statsType, |
||||||
|
}: UsePlayersStatsArgs) => { |
||||||
|
const [playersStats, setPlayersStats] = useObjectState<Record<string, PlayersStats>>({}) |
||||||
|
const [playersData, setPlayersData] = useState<PlayersData>({ team1: [], team2: [] }) |
||||||
|
|
||||||
|
const { |
||||||
|
profileId: matchId, |
||||||
|
sportName, |
||||||
|
sportType, |
||||||
|
} = usePageParams() |
||||||
|
|
||||||
|
const client = useQueryClient() |
||||||
|
|
||||||
|
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) |
||||||
|
|
||||||
|
const isCurrentStats = statsType === StatsType.CURRENT_STATS |
||||||
|
|
||||||
|
const isEmptyPlayersStats = (teamId: number) => ( |
||||||
|
isEmpty(playersStats[teamId]) |
||||||
|
|| every(playersStats[teamId], isEmpty) |
||||||
|
|| isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2']) |
||||||
|
) |
||||||
|
|
||||||
|
const fetchPlayers = useMemo(() => throttle(async (second?: number) => { |
||||||
|
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds |
||||||
|
|
||||||
|
if ( |
||||||
|
!matchProfile?.team1.id |
||||||
|
|| !matchProfile?.team2.id |
||||||
|
|| !videoBounds |
||||||
|
) return null |
||||||
|
|
||||||
|
try { |
||||||
|
return getMatchParticipants({ |
||||||
|
matchId, |
||||||
|
sportType, |
||||||
|
...(!isUndefined(second) && getHalfTime(videoBounds, second)), |
||||||
|
}) |
||||||
|
} catch (e) { |
||||||
|
return Promise.reject(e) |
||||||
|
} |
||||||
|
}, REQUEST_DELAY), [ |
||||||
|
matchId, |
||||||
|
matchProfile?.team1.id, |
||||||
|
matchProfile?.team2.id, |
||||||
|
matchProfile?.video_bounds, |
||||||
|
matchScore?.video_bounds, |
||||||
|
sportType, |
||||||
|
]) |
||||||
|
|
||||||
|
const fetchPlayersStats = useMemo(() => (async (team: 'team1' | 'team2', second?: number) => { |
||||||
|
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds |
||||||
|
|
||||||
|
if (!sportName || !matchProfile?.[team].id || !videoBounds) return null |
||||||
|
|
||||||
|
try { |
||||||
|
return getPlayersStats({ |
||||||
|
matchId, |
||||||
|
sportName, |
||||||
|
teamId: matchProfile[team].id, |
||||||
|
...(!isUndefined(second) && getHalfTime(videoBounds, second)), |
||||||
|
}) |
||||||
|
} catch (e) { |
||||||
|
return Promise.reject(e) |
||||||
|
} |
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}), [ |
||||||
|
matchId, |
||||||
|
sportName, |
||||||
|
matchProfile?.team1.id, |
||||||
|
matchProfile?.team2.id, |
||||||
|
]) |
||||||
|
|
||||||
|
const fetchData = useMemo(() => throttle(async (second?: number) => { |
||||||
|
if ( |
||||||
|
selectedPlaylist?.id !== FULL_GAME_KEY |
||||||
|
|| (matchProfile?.live && Number(matchProfile.c_match_calc_status) <= 1) |
||||||
|
) return |
||||||
|
|
||||||
|
const [res1, res2, res3] = await Promise.all([ |
||||||
|
fetchPlayers(second), |
||||||
|
fetchPlayersStats('team1', second), |
||||||
|
fetchPlayersStats('team2', second), |
||||||
|
]) |
||||||
|
|
||||||
|
const team1Players = find(res1, { team_id: matchProfile?.team1.id })?.players || [] |
||||||
|
const team2Players = find(res1, { team_id: matchProfile?.team2.id })?.players || [] |
||||||
|
|
||||||
|
setPlayersData({ |
||||||
|
team1: team1Players, |
||||||
|
team2: team2Players, |
||||||
|
}) |
||||||
|
|
||||||
|
setPlayersStats({ |
||||||
|
...(matchProfile?.team1.id && res2 && { [matchProfile.team1.id]: res2 }), |
||||||
|
...(matchProfile?.team2.id && res3 && { [matchProfile.team2.id]: res3 }), |
||||||
|
}) |
||||||
|
|
||||||
|
setIsPlayersStatsFetching(false) |
||||||
|
}, REQUEST_DELAY), [ |
||||||
|
selectedPlaylist?.id, |
||||||
|
fetchPlayers, |
||||||
|
fetchPlayersStats, |
||||||
|
setPlayersStats, |
||||||
|
matchProfile?.team1.id, |
||||||
|
matchProfile?.team2.id, |
||||||
|
matchProfile?.live, |
||||||
|
matchProfile?.c_match_calc_status, |
||||||
|
setIsPlayersStatsFetching, |
||||||
|
]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
let interval: NodeJS.Timeout |
||||||
|
|
||||||
|
if (!isCurrentStats) { |
||||||
|
fetchData() |
||||||
|
} |
||||||
|
|
||||||
|
if (matchProfile?.live && !isCurrentStats) { |
||||||
|
interval = setInterval(() => { |
||||||
|
fetchData() |
||||||
|
}, STATS_POLL_INTERVAL) |
||||||
|
} |
||||||
|
|
||||||
|
return () => clearInterval(interval) |
||||||
|
}, [ |
||||||
|
fetchData, |
||||||
|
isCurrentStats, |
||||||
|
matchProfile?.live, |
||||||
|
]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (isCurrentStats) { |
||||||
|
fetchData(playingProgress) |
||||||
|
} |
||||||
|
}, [ |
||||||
|
fetchData, |
||||||
|
playingProgress, |
||||||
|
isCurrentStats, |
||||||
|
matchProfile?.live, |
||||||
|
]) |
||||||
|
|
||||||
|
return { |
||||||
|
isEmptyPlayersStats, |
||||||
|
playersData, |
||||||
|
playersStats, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,144 @@ |
|||||||
|
import { useState, useEffect } from 'react' |
||||||
|
|
||||||
|
import map from 'lodash/map' |
||||||
|
import isEqual from 'lodash/isEqual' |
||||||
|
|
||||||
|
import type { |
||||||
|
Episode, |
||||||
|
Episodes, |
||||||
|
Events, |
||||||
|
MatchInfo, |
||||||
|
} from 'requests' |
||||||
|
|
||||||
|
import type { EventPlaylistOption, PlaylistOption } from 'features/MatchPage/types' |
||||||
|
import type { TCircleAnimation } from 'features/CircleAnimationBar' |
||||||
|
import { initialCircleAnimation } from 'features/CircleAnimationBar' |
||||||
|
import { PlaylistTypes } from 'features/MatchPage/types' |
||||||
|
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' |
||||||
|
|
||||||
|
type UseStatsTabArgs = { |
||||||
|
disablePlayingEpisodes: () => void, |
||||||
|
handlePlaylistClick: (playlist: PlaylistOption) => void, |
||||||
|
matchProfile: MatchInfo, |
||||||
|
selectedPlaylist?: PlaylistOption, |
||||||
|
} |
||||||
|
|
||||||
|
type PlayNextEpisodeArgs = { |
||||||
|
episodesToPlay?: Array<EventPlaylistOption>, |
||||||
|
order?: number, |
||||||
|
} |
||||||
|
|
||||||
|
const EPISODE_TIMESTAMP_OFFSET = 0.001 |
||||||
|
|
||||||
|
const addOffset = ({ |
||||||
|
e, |
||||||
|
h, |
||||||
|
s, |
||||||
|
}: Episode) => ({ |
||||||
|
e: e + EPISODE_TIMESTAMP_OFFSET, |
||||||
|
h, |
||||||
|
s: s + EPISODE_TIMESTAMP_OFFSET, |
||||||
|
}) |
||||||
|
|
||||||
|
export const useStatsTab = ({ |
||||||
|
disablePlayingEpisodes, |
||||||
|
handlePlaylistClick, |
||||||
|
matchProfile, |
||||||
|
selectedPlaylist, |
||||||
|
}: UseStatsTabArgs) => { |
||||||
|
const [statsType, setStatsType] = useState<StatsType>(StatsType.FINAL_STATS) |
||||||
|
const [isPlayersStatsFetching, setIsPlayersStatsFetching] = useState(false) |
||||||
|
const [isTeamsStatsFetching, setIsTeamsStatsFetching] = useState(false) |
||||||
|
const [stateEpisodesToPlay, setEpisodesToPlay] = useState<Array<EventPlaylistOption>>([]) |
||||||
|
const [filteredEvents, setFilteredEvents] = useState<Events>([]) |
||||||
|
const [plaingOrder, setPlaingOrder] = useState(0) |
||||||
|
const [isPlayFilterEpisodes, setIsPlayingFiltersEpisodes] = useState(false) |
||||||
|
const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false) |
||||||
|
const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation) |
||||||
|
|
||||||
|
const isFinalStatsType = statsType === StatsType.FINAL_STATS |
||||||
|
|
||||||
|
const toggleStatsType = () => { |
||||||
|
const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS |
||||||
|
|
||||||
|
setStatsType(newStatsType) |
||||||
|
setIsTeamsStatsFetching(true) |
||||||
|
setIsPlayersStatsFetching(true) |
||||||
|
} |
||||||
|
|
||||||
|
const getEpisodesToPlay = (episodes: Episodes) => map(episodes, (episode, i) => ({ |
||||||
|
episodes: [ |
||||||
|
/** При проигрывании нового эпизода с такими же e и s, как у текущего |
||||||
|
воспроизведение начинается не с начала, чтобы пофиксить это добавляем |
||||||
|
небольшой оффсет |
||||||
|
*/ |
||||||
|
isEqual(episode, selectedPlaylist?.episodes[0]) |
||||||
|
? addOffset(episode) |
||||||
|
: episode, |
||||||
|
], |
||||||
|
id: i, |
||||||
|
type: PlaylistTypes.EVENT, |
||||||
|
})) as Array<EventPlaylistOption> |
||||||
|
|
||||||
|
const playNextEpisode = ({ |
||||||
|
order, |
||||||
|
episodesToPlay = stateEpisodesToPlay, |
||||||
|
}: PlayNextEpisodeArgs = {}) => { |
||||||
|
const currentOrder = order === 0 ? order : plaingOrder |
||||||
|
const isLastEpisode = currentOrder === episodesToPlay.length |
||||||
|
|
||||||
|
if (isLastEpisode) { |
||||||
|
setPlaingOrder(0) |
||||||
|
setIsPlayingFiltersEpisodes(false) |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (currentOrder !== 0) { |
||||||
|
handlePlaylistClick(episodesToPlay[currentOrder]) |
||||||
|
} |
||||||
|
|
||||||
|
setPlaingOrder(currentOrder + 1) |
||||||
|
} |
||||||
|
|
||||||
|
const playEpisodes = (episodes: Episodes) => { |
||||||
|
disablePlayingEpisodes() |
||||||
|
|
||||||
|
const episodesToPlay = getEpisodesToPlay(episodes) |
||||||
|
|
||||||
|
setEpisodesToPlay(episodesToPlay) |
||||||
|
setFilteredEvents(episodes as Events) |
||||||
|
|
||||||
|
setWatchAllEpisodesTimer(true) |
||||||
|
setIsPlayingFiltersEpisodes(true) |
||||||
|
|
||||||
|
handlePlaylistClick(episodesToPlay[0]) |
||||||
|
playNextEpisode({ episodesToPlay, order: 0 }) |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (matchProfile?.live) { |
||||||
|
setStatsType(StatsType.CURRENT_STATS) |
||||||
|
} |
||||||
|
}, [matchProfile?.live]) |
||||||
|
|
||||||
|
return { |
||||||
|
circleAnimation, |
||||||
|
filteredEvents, |
||||||
|
isPlayFilterEpisodes, |
||||||
|
isPlayersStatsFetching, |
||||||
|
isTeamsStatsFetching, |
||||||
|
plaingOrder, |
||||||
|
playEpisodes, |
||||||
|
playNextEpisode, |
||||||
|
setCircleAnimation, |
||||||
|
setIsPlayersStatsFetching, |
||||||
|
setIsPlayingFiltersEpisodes, |
||||||
|
setIsTeamsStatsFetching, |
||||||
|
setPlaingOrder, |
||||||
|
setWatchAllEpisodesTimer, |
||||||
|
statsType, |
||||||
|
toggleStatsType, |
||||||
|
watchAllEpisodesTimer, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,113 @@ |
|||||||
|
import type { Dispatch, SetStateAction } from 'react' |
||||||
|
import { |
||||||
|
useEffect, |
||||||
|
useState, |
||||||
|
useMemo, |
||||||
|
} from 'react' |
||||||
|
import { useQueryClient } from 'react-query' |
||||||
|
|
||||||
|
import throttle from 'lodash/throttle' |
||||||
|
import isUndefined from 'lodash/isUndefined' |
||||||
|
|
||||||
|
import { querieKeys } from 'config' |
||||||
|
|
||||||
|
import type { MatchInfo, MatchScore } from 'requests' |
||||||
|
import { getTeamsStats, TeamStatItem } from 'requests' |
||||||
|
|
||||||
|
import { usePageParams } from 'hooks' |
||||||
|
|
||||||
|
import type { PlaylistOption } from 'features/MatchPage/types' |
||||||
|
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' |
||||||
|
import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' |
||||||
|
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' |
||||||
|
|
||||||
|
const REQUEST_DELAY = 3000 |
||||||
|
const STATS_POLL_INTERVAL = 30000 |
||||||
|
|
||||||
|
type UseTeamsStatsArgs = { |
||||||
|
matchProfile: MatchInfo, |
||||||
|
playingProgress: number, |
||||||
|
selectedPlaylist?: PlaylistOption, |
||||||
|
setIsTeamsStatsFetching: Dispatch<SetStateAction<boolean>>, |
||||||
|
statsType: StatsType, |
||||||
|
} |
||||||
|
|
||||||
|
export const useTeamsStats = ({ |
||||||
|
matchProfile, |
||||||
|
playingProgress, |
||||||
|
selectedPlaylist, |
||||||
|
setIsTeamsStatsFetching, |
||||||
|
statsType, |
||||||
|
}: UseTeamsStatsArgs) => { |
||||||
|
const [teamsStats, setTeamsStats] = useState<{ |
||||||
|
[teamId: string]: Array<TeamStatItem>, |
||||||
|
}>({}) |
||||||
|
|
||||||
|
const { profileId: matchId, sportName } = usePageParams() |
||||||
|
|
||||||
|
const client = useQueryClient() |
||||||
|
|
||||||
|
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) |
||||||
|
|
||||||
|
const isCurrentStats = statsType === StatsType.CURRENT_STATS |
||||||
|
|
||||||
|
const fetchTeamsStats = useMemo(() => throttle(async (second?: number) => { |
||||||
|
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds |
||||||
|
|
||||||
|
if ( |
||||||
|
!sportName |
||||||
|
|| selectedPlaylist?.id !== FULL_GAME_KEY |
||||||
|
|| !videoBounds |
||||||
|
|| (matchProfile?.live && Number(matchProfile.c_match_calc_status) <= 1) |
||||||
|
) return |
||||||
|
|
||||||
|
try { |
||||||
|
const data = await getTeamsStats({ |
||||||
|
matchId, |
||||||
|
sportName, |
||||||
|
...(!isUndefined(second) && getHalfTime(videoBounds, second)), |
||||||
|
}) |
||||||
|
|
||||||
|
setTeamsStats(data) |
||||||
|
setIsTeamsStatsFetching(false) |
||||||
|
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {} |
||||||
|
}, REQUEST_DELAY), [ |
||||||
|
matchProfile?.video_bounds, |
||||||
|
matchProfile?.c_match_calc_status, |
||||||
|
matchProfile?.live, |
||||||
|
matchScore?.video_bounds, |
||||||
|
selectedPlaylist?.id, |
||||||
|
matchId, |
||||||
|
setIsTeamsStatsFetching, |
||||||
|
sportName, |
||||||
|
]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
let interval: NodeJS.Timeout |
||||||
|
|
||||||
|
if (!isCurrentStats) { |
||||||
|
fetchTeamsStats() |
||||||
|
} |
||||||
|
|
||||||
|
if (matchProfile?.live && !isCurrentStats) { |
||||||
|
interval = setInterval(() => { |
||||||
|
fetchTeamsStats() |
||||||
|
}, STATS_POLL_INTERVAL) |
||||||
|
} |
||||||
|
|
||||||
|
return () => clearInterval(interval) |
||||||
|
}, [fetchTeamsStats, matchProfile?.live, isCurrentStats]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (isCurrentStats) { |
||||||
|
fetchTeamsStats(playingProgress) |
||||||
|
} |
||||||
|
}, [fetchTeamsStats, playingProgress, isCurrentStats]) |
||||||
|
|
||||||
|
return { |
||||||
|
statsType, |
||||||
|
teamsStats, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { CircleAnimationBar as CircleAnimationBarBase } from 'features/CircleAnimationBar' |
||||||
|
|
||||||
|
export const CircleAnimationBar = styled(CircleAnimationBarBase)` |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
|
||||||
|
circle { |
||||||
|
stroke: #4086C6; |
||||||
|
} |
||||||
|
|
||||||
|
text { |
||||||
|
fill: ${({ theme }) => theme.colors.white}; |
||||||
|
} |
||||||
|
` |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { customScrollbar } from 'features/Common' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
import { isMobileDevice } from '../../../../config/userAgent' |
||||||
|
|
||||||
|
type MatchesWrapperProps = { |
||||||
|
additionalScrollHeight: number, |
||||||
|
hasScroll?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const MatchesWrapper = styled.div<MatchesWrapperProps>` |
||||||
|
overflow-y: auto; |
||||||
|
max-height: calc(100vh - 200px - ${({ additionalScrollHeight }) => additionalScrollHeight}px); |
||||||
|
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')}; |
||||||
|
|
||||||
|
> * { |
||||||
|
:not(:last-child) { |
||||||
|
margin-bottom: 10px; |
||||||
|
}
|
||||||
|
} |
||||||
|
|
||||||
|
${customScrollbar} |
||||||
|
|
||||||
|
${isMobileDevice ? css` |
||||||
|
overflow: hidden; |
||||||
|
max-height: initial; |
||||||
|
` : ''}
|
||||||
|
` |
||||||
|
|
||||||
|
export const Title = styled(T9n)` |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
margin-bottom: 15px; |
||||||
|
font-size: 12px; |
||||||
|
font-weight: 600; |
||||||
|
text-transform: uppercase; |
||||||
|
color: rgba(255, 255, 255, 0.5); |
||||||
|
` |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
import type { PropsWithChildren, HTMLProps } from 'react' |
||||||
|
import { memo, useRef } from 'react' |
||||||
|
import { createPortal } from 'react-dom' |
||||||
|
|
||||||
|
import { isMobileDevice, KEYBOARD_KEYS } from 'config' |
||||||
|
|
||||||
|
import { |
||||||
|
useEventListener, |
||||||
|
useModalRoot, |
||||||
|
useTooltip, |
||||||
|
} from 'hooks' |
||||||
|
|
||||||
|
import { Tooltip } from '../TabStats/styled' |
||||||
|
import { CellContainer } from './styled' |
||||||
|
|
||||||
|
type CellProps = { |
||||||
|
anchorId?: string, |
||||||
|
as?: 'td' | 'th', |
||||||
|
clickable?: boolean, |
||||||
|
columnWidth?: number, |
||||||
|
hasValue?: boolean, |
||||||
|
sorted?: boolean, |
||||||
|
tooltipText?: string, |
||||||
|
} & HTMLProps<HTMLTableCellElement> |
||||||
|
|
||||||
|
const CellFC = ({ |
||||||
|
anchorId, |
||||||
|
as, |
||||||
|
children, |
||||||
|
clickable, |
||||||
|
columnWidth, |
||||||
|
hasValue, |
||||||
|
onClick, |
||||||
|
sorted, |
||||||
|
tooltipText, |
||||||
|
}: PropsWithChildren<CellProps>) => { |
||||||
|
const cellRef = useRef<HTMLTableCellElement | null>(null) |
||||||
|
|
||||||
|
const { |
||||||
|
isTooltipShown, |
||||||
|
onMouseLeave, |
||||||
|
onMouseOver, |
||||||
|
tooltipStyle, |
||||||
|
} = useTooltip() |
||||||
|
|
||||||
|
const modalRoot = useModalRoot() |
||||||
|
|
||||||
|
useEventListener({ |
||||||
|
callback: (e) => { |
||||||
|
if (e.key !== KEYBOARD_KEYS.Enter) return |
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick() |
||||||
|
}, |
||||||
|
event: 'keydown', |
||||||
|
target: cellRef, |
||||||
|
}) |
||||||
|
|
||||||
|
return ( |
||||||
|
<CellContainer |
||||||
|
ref={cellRef} |
||||||
|
as={as} |
||||||
|
onClick={onClick} |
||||||
|
clickable={clickable} |
||||||
|
columnWidth={columnWidth} |
||||||
|
sorted={sorted} |
||||||
|
hasValue={hasValue} |
||||||
|
onMouseOver={tooltipText && !isMobileDevice ? onMouseOver({ |
||||||
|
anchorId, |
||||||
|
horizontalPosition: 'right', |
||||||
|
tooltipText, |
||||||
|
verticalPosition: 'top', |
||||||
|
}) : undefined} |
||||||
|
onMouseLeave={tooltipText && !isMobileDevice ? onMouseLeave : undefined} |
||||||
|
> |
||||||
|
{children} |
||||||
|
{isTooltipShown && modalRoot.current && createPortal( |
||||||
|
<Tooltip style={tooltipStyle}> |
||||||
|
{tooltipText} |
||||||
|
</Tooltip>, |
||||||
|
modalRoot.current, |
||||||
|
)} |
||||||
|
</CellContainer> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const Cell = memo(CellFC) |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
export const PARAM_COLUMN_WIDTH_DEFAULT = 40 |
||||||
|
export const FIRST_COLUMN_WIDTH_DEFAULT = 105 |
||||||
|
export const FIRST_COLUMN_WIDTH_EXPANDED = 220 |
||||||
|
export const REQUEST_DELAY = 3000 |
||||||
|
export const STATS_POLL_INTERVAL = 30000 |
||||||
|
export const DISPLAYED_PARAMS_COLUMNS = 5 |
||||||
|
export const SCROLLBAR_WIDTH = 8 |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
import { useEffect, useState } from 'react' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
|
||||||
|
import type { SortCondition, PlayersTableProps } from '../types' |
||||||
|
import { usePlayers } from './usePlayers' |
||||||
|
import { useTable } from './useTable' |
||||||
|
|
||||||
|
export const usePlayersTable = ({ teamId }: PlayersTableProps) => { |
||||||
|
const [sortCondition, setSortCondition] = useState<SortCondition>({ |
||||||
|
clicksCount: 0, |
||||||
|
dir: 'asc', |
||||||
|
paramId: null, |
||||||
|
}) |
||||||
|
|
||||||
|
const { plaingOrder, setCircleAnimation } = useMatchPageStore() |
||||||
|
|
||||||
|
const { |
||||||
|
getPlayerName, |
||||||
|
getPlayerParams, |
||||||
|
players, |
||||||
|
} = usePlayers({ sortCondition, teamId }) |
||||||
|
|
||||||
|
const { |
||||||
|
containerRef, |
||||||
|
getDisplayedValue, |
||||||
|
handleParamClick, |
||||||
|
handleScroll, |
||||||
|
handleSortClick, |
||||||
|
isExpanded, |
||||||
|
paramColumnWidth, |
||||||
|
params, |
||||||
|
showExpandButton, |
||||||
|
showLeftArrow, |
||||||
|
showRightArrow, |
||||||
|
slideLeft, |
||||||
|
slideRight, |
||||||
|
tableWrapperRef, |
||||||
|
toggleIsExpanded, |
||||||
|
} = useTable({ |
||||||
|
setSortCondition, |
||||||
|
teamId, |
||||||
|
}) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setCircleAnimation((state) => ({ |
||||||
|
...state, |
||||||
|
plaingOrder, |
||||||
|
})) |
||||||
|
}, [setCircleAnimation, plaingOrder]) |
||||||
|
|
||||||
|
return { |
||||||
|
containerRef, |
||||||
|
getDisplayedValue, |
||||||
|
getPlayerName, |
||||||
|
getPlayerParams, |
||||||
|
handleParamClick, |
||||||
|
handleScroll, |
||||||
|
handleSortClick, |
||||||
|
isExpanded, |
||||||
|
paramColumnWidth, |
||||||
|
params, |
||||||
|
players, |
||||||
|
showExpandButton, |
||||||
|
showLeftArrow, |
||||||
|
showRightArrow, |
||||||
|
slideLeft, |
||||||
|
slideRight, |
||||||
|
sortCondition, |
||||||
|
tableWrapperRef, |
||||||
|
toggleIsExpanded, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
import { useMemo, useCallback } from 'react' |
||||||
|
|
||||||
|
import orderBy from 'lodash/orderBy' |
||||||
|
import isNil from 'lodash/isNil' |
||||||
|
import trim from 'lodash/trim' |
||||||
|
|
||||||
|
import type { Player } 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 getPlayerName = useCallback((player: Player) => ( |
||||||
|
trim(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, 'ord') |
||||||
|
: orderBy( |
||||||
|
players, |
||||||
|
[ |
||||||
|
(player) => { |
||||||
|
const paramValue = getParamValue(player.id, sortCondition.paramId!) |
||||||
|
|
||||||
|
return isNil(paramValue) ? -1 : paramValue |
||||||
|
}, |
||||||
|
'ord', |
||||||
|
], |
||||||
|
sortCondition.dir, |
||||||
|
) |
||||||
|
}, [ |
||||||
|
getParamValue, |
||||||
|
playersData, |
||||||
|
matchProfile?.team1.id, |
||||||
|
sortCondition.dir, |
||||||
|
sortCondition.paramId, |
||||||
|
teamId, |
||||||
|
]) |
||||||
|
|
||||||
|
return { |
||||||
|
getPlayerName, |
||||||
|
getPlayerParams, |
||||||
|
isExpanded, |
||||||
|
players: sortedPlayers, |
||||||
|
toggleIsExpanded, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,262 @@ |
|||||||
|
import type { |
||||||
|
SyntheticEvent, |
||||||
|
Dispatch, |
||||||
|
SetStateAction, |
||||||
|
} from 'react' |
||||||
|
import { |
||||||
|
useCallback, |
||||||
|
useRef, |
||||||
|
useState, |
||||||
|
useEffect, |
||||||
|
useLayoutEffect, |
||||||
|
useMemo, |
||||||
|
} from 'react' |
||||||
|
import { useQueryClient } from 'react-query' |
||||||
|
|
||||||
|
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 map from 'lodash/map' |
||||||
|
|
||||||
|
import { isMobileDevice, querieKeys } from 'config' |
||||||
|
|
||||||
|
import type { |
||||||
|
PlayerParam, |
||||||
|
PlayersStats, |
||||||
|
MatchScore, |
||||||
|
} from 'requests' |
||||||
|
import { getStatsEvents } from 'requests' |
||||||
|
|
||||||
|
import { usePageParams, useToggle } from 'hooks' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
import { useLexicsConfig } from 'features/LexicsStore' |
||||||
|
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' |
||||||
|
|
||||||
|
import type { SortCondition } from '../types' |
||||||
|
import { |
||||||
|
PARAM_COLUMN_WIDTH_DEFAULT, |
||||||
|
FIRST_COLUMN_WIDTH_DEFAULT, |
||||||
|
DISPLAYED_PARAMS_COLUMNS, |
||||||
|
SCROLLBAR_WIDTH, |
||||||
|
} from '../config' |
||||||
|
import { StatsType } from '../../TabStats/config' |
||||||
|
|
||||||
|
type UseTableArgs = { |
||||||
|
setSortCondition: Dispatch<SetStateAction<SortCondition>>, |
||||||
|
teamId: number, |
||||||
|
} |
||||||
|
|
||||||
|
type HeaderParam = Pick<PlayerParam, 'id' | 'lexica_short' | 'lexic'> |
||||||
|
|
||||||
|
export const useTable = ({ |
||||||
|
setSortCondition, |
||||||
|
teamId, |
||||||
|
}: UseTableArgs) => { |
||||||
|
const containerRef = useRef<HTMLDivElement>(null) |
||||||
|
const tableWrapperRef = useRef<HTMLDivElement>(null) |
||||||
|
|
||||||
|
const [showLeftArrow, setShowLeftArrow] = useState(false) |
||||||
|
const [showRightArrow, setShowRightArrow] = useState(true) |
||||||
|
const [paramColumnWidth, setParamColumnWidth] = useState(PARAM_COLUMN_WIDTH_DEFAULT) |
||||||
|
|
||||||
|
const { |
||||||
|
close: reduceTable, |
||||||
|
isOpen: isExpanded, |
||||||
|
toggle: toggleIsExpanded, |
||||||
|
} = useToggle() |
||||||
|
const { |
||||||
|
playersStats, |
||||||
|
playingProgress, |
||||||
|
playStatsEpisodes, |
||||||
|
profile, |
||||||
|
setIsPlayingFiltersEpisodes, |
||||||
|
setPlayingData, |
||||||
|
setWatchAllEpisodesTimer, |
||||||
|
statsType, |
||||||
|
} = useMatchPageStore() |
||||||
|
const { profileId, sportType } = usePageParams() |
||||||
|
|
||||||
|
const client = useQueryClient() |
||||||
|
|
||||||
|
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) |
||||||
|
|
||||||
|
const params = useMemo(() => ( |
||||||
|
reduce<PlayersStats, Record<string, HeaderParam>>( |
||||||
|
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<HeaderParam, Array<number>>( |
||||||
|
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 = useCallback(() => { |
||||||
|
const paramsTableWidth = ( |
||||||
|
(containerRef.current?.clientWidth || 0) |
||||||
|
- FIRST_COLUMN_WIDTH_DEFAULT |
||||||
|
- SCROLLBAR_WIDTH - 8 |
||||||
|
) |
||||||
|
return isExpanded |
||||||
|
? PARAM_COLUMN_WIDTH_DEFAULT |
||||||
|
: paramsTableWidth / DISPLAYED_PARAMS_COLUMNS |
||||||
|
}, [isExpanded]) |
||||||
|
|
||||||
|
const slideLeft = () => { |
||||||
|
const { |
||||||
|
clientHeight = 0, |
||||||
|
clientWidth = 0, |
||||||
|
scrollHeight = 0, |
||||||
|
scrollLeft = 0, |
||||||
|
scrollWidth = 0, |
||||||
|
} = tableWrapperRef.current || {} |
||||||
|
|
||||||
|
const hasVerticalScroll = scrollHeight > clientHeight |
||||||
|
const scrollRight = scrollWidth - (scrollLeft + clientWidth) |
||||||
|
|
||||||
|
const scrollBy = scrollRight === 0 |
||||||
|
? paramColumnWidth - (hasVerticalScroll ? SCROLLBAR_WIDTH : SCROLLBAR_WIDTH * 2) |
||||||
|
: paramColumnWidth |
||||||
|
|
||||||
|
tableWrapperRef.current?.scrollBy(-scrollBy, 0) |
||||||
|
} |
||||||
|
const slideRight = () => { |
||||||
|
tableWrapperRef.current?.scrollBy(paramColumnWidth, 0) |
||||||
|
} |
||||||
|
|
||||||
|
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : String(val)) |
||||||
|
|
||||||
|
const handleScroll = (e: SyntheticEvent<HTMLDivElement>) => { |
||||||
|
const { |
||||||
|
clientWidth, |
||||||
|
scrollLeft, |
||||||
|
scrollWidth, |
||||||
|
} = e.currentTarget |
||||||
|
|
||||||
|
const scrollRight = scrollWidth - (scrollLeft + clientWidth) |
||||||
|
|
||||||
|
setShowLeftArrow(scrollLeft > 0) |
||||||
|
setShowRightArrow(scrollRight > 0) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSortClick = (paramId: number) => () => { |
||||||
|
setSortCondition((curr) => { |
||||||
|
const clicksCount = curr.paramId === paramId || isNil(curr.paramId) |
||||||
|
? curr.clicksCount + 1 |
||||||
|
: 1 |
||||||
|
|
||||||
|
// При третьем клике сбрасываем счетчик клика и убираем сортировку по параметру
|
||||||
|
return { |
||||||
|
clicksCount: clicksCount === 3 ? 0 : clicksCount, |
||||||
|
dir: curr.dir === 'asc' || curr.paramId !== paramId ? 'desc' : 'asc', |
||||||
|
paramId: clicksCount === 3 ? null : paramId, |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleParamClick = async (paramId: number, playerId: number) => { |
||||||
|
setWatchAllEpisodesTimer(false) |
||||||
|
setIsPlayingFiltersEpisodes(false) |
||||||
|
|
||||||
|
const videoBounds = matchScore?.video_bounds || profile?.video_bounds |
||||||
|
|
||||||
|
setPlayingData({ |
||||||
|
player: { |
||||||
|
id: playerId, |
||||||
|
paramId, |
||||||
|
}, |
||||||
|
team: { |
||||||
|
id: null, |
||||||
|
paramId: null, |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
try { |
||||||
|
const events = await getStatsEvents({ |
||||||
|
matchId: profileId, |
||||||
|
paramId, |
||||||
|
playerId, |
||||||
|
sportType, |
||||||
|
teamId, |
||||||
|
...(statsType === StatsType.CURRENT_STATS && videoBounds && ( |
||||||
|
getHalfTime(videoBounds, playingProgress) |
||||||
|
)), |
||||||
|
}) |
||||||
|
|
||||||
|
playStatsEpisodes(events) |
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {} |
||||||
|
} |
||||||
|
|
||||||
|
useLayoutEffect(() => { |
||||||
|
setParamColumnWidth(getParamColumnWidth()) |
||||||
|
}, [getParamColumnWidth, containerRef.current?.clientWidth]) |
||||||
|
|
||||||
|
useLayoutEffect(() => { |
||||||
|
const { |
||||||
|
clientWidth = 0, |
||||||
|
scrollLeft = 0, |
||||||
|
scrollWidth = 0, |
||||||
|
} = tableWrapperRef.current || {} |
||||||
|
|
||||||
|
const scrollRight = scrollWidth - (scrollLeft + clientWidth) |
||||||
|
|
||||||
|
setShowRightArrow(scrollRight > 0) |
||||||
|
}, [isExpanded]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (isExpanded && paramsCount <= DISPLAYED_PARAMS_COLUMNS) { |
||||||
|
reduceTable() |
||||||
|
} |
||||||
|
}, [isExpanded, paramsCount, reduceTable]) |
||||||
|
|
||||||
|
return { |
||||||
|
containerRef, |
||||||
|
getDisplayedValue, |
||||||
|
handleParamClick, |
||||||
|
handleScroll, |
||||||
|
handleSortClick, |
||||||
|
isExpanded, |
||||||
|
paramColumnWidth, |
||||||
|
params, |
||||||
|
showExpandButton: !isMobileDevice && paramsCount > DISPLAYED_PARAMS_COLUMNS, |
||||||
|
showLeftArrow, |
||||||
|
showRightArrow, |
||||||
|
slideLeft, |
||||||
|
slideRight, |
||||||
|
tableWrapperRef, |
||||||
|
toggleIsExpanded, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,201 @@ |
|||||||
|
import { Fragment } from 'react' |
||||||
|
|
||||||
|
import map from 'lodash/map' |
||||||
|
import includes from 'lodash/includes' |
||||||
|
|
||||||
|
import { PlayerParam } from 'requests' |
||||||
|
|
||||||
|
import { usePageParams } from 'hooks' |
||||||
|
|
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
import { Loader } from 'features/Loader' |
||||||
|
import { defaultTheme } from 'features/Theme/config' |
||||||
|
|
||||||
|
import type { PlayersTableProps } from './types' |
||||||
|
import { FIRST_COLUMN_WIDTH_DEFAULT, FIRST_COLUMN_WIDTH_EXPANDED } from './config' |
||||||
|
import { usePlayersTable } from './hooks' |
||||||
|
import { Cell } from './Cell' |
||||||
|
import { |
||||||
|
Container, |
||||||
|
TableWrapper, |
||||||
|
Table, |
||||||
|
Header, |
||||||
|
Row, |
||||||
|
PlayerNum, |
||||||
|
PlayerName, |
||||||
|
ParamShortTitle, |
||||||
|
ArrowButtonRight, |
||||||
|
ArrowButtonLeft, |
||||||
|
Arrow, |
||||||
|
ExpandButton, |
||||||
|
} from './styled' |
||||||
|
import { CircleAnimationBar } from '../CircleAnimationBar' |
||||||
|
|
||||||
|
export const PlayersTable = (props: PlayersTableProps) => { |
||||||
|
const { |
||||||
|
containerRef, |
||||||
|
getDisplayedValue, |
||||||
|
getPlayerName, |
||||||
|
getPlayerParams, |
||||||
|
handleParamClick, |
||||||
|
handleScroll, |
||||||
|
handleSortClick, |
||||||
|
isExpanded, |
||||||
|
paramColumnWidth, |
||||||
|
params, |
||||||
|
players, |
||||||
|
showExpandButton, |
||||||
|
showLeftArrow, |
||||||
|
showRightArrow, |
||||||
|
slideLeft, |
||||||
|
slideRight, |
||||||
|
sortCondition, |
||||||
|
tableWrapperRef, |
||||||
|
toggleIsExpanded, |
||||||
|
} = usePlayersTable(props) |
||||||
|
const { translate } = useLexicsStore() |
||||||
|
const { sportName } = usePageParams() |
||||||
|
const { |
||||||
|
isPlayersStatsFetching, |
||||||
|
playingData, |
||||||
|
watchAllEpisodesTimer, |
||||||
|
} = useMatchPageStore() |
||||||
|
|
||||||
|
const firstColumnWidth = isExpanded ? FIRST_COLUMN_WIDTH_EXPANDED : FIRST_COLUMN_WIDTH_DEFAULT |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container |
||||||
|
ref={containerRef} |
||||||
|
isExpanded={isExpanded} |
||||||
|
> |
||||||
|
{isPlayersStatsFetching |
||||||
|
? <Loader color={defaultTheme.colors.white} /> |
||||||
|
: ( |
||||||
|
<TableWrapper |
||||||
|
ref={tableWrapperRef} |
||||||
|
isExpanded={isExpanded} |
||||||
|
onScroll={handleScroll} |
||||||
|
> |
||||||
|
{!isExpanded && ( |
||||||
|
<Fragment> |
||||||
|
{showRightArrow && ( |
||||||
|
<ArrowButtonRight |
||||||
|
aria-label='Scroll to right' |
||||||
|
onClick={slideRight} |
||||||
|
> |
||||||
|
<Arrow direction='right' /> |
||||||
|
</ArrowButtonRight> |
||||||
|
)} |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
<Table role='marquee' aria-live='off'> |
||||||
|
<Header> |
||||||
|
<Row> |
||||||
|
<Cell |
||||||
|
as='th' |
||||||
|
columnWidth={firstColumnWidth} |
||||||
|
> |
||||||
|
{showLeftArrow && ( |
||||||
|
<ArrowButtonLeft |
||||||
|
aria-label='Scroll to left' |
||||||
|
onClick={slideLeft} |
||||||
|
> |
||||||
|
<Arrow direction='left' /> |
||||||
|
</ArrowButtonLeft> |
||||||
|
)} |
||||||
|
{showExpandButton && ( |
||||||
|
<ExpandButton |
||||||
|
isExpanded={isExpanded} |
||||||
|
aria-label={isExpanded ? 'Reduce' : 'Expand'} |
||||||
|
onClick={toggleIsExpanded} |
||||||
|
> |
||||||
|
<Arrow direction={isExpanded ? 'right' : 'left'} /> |
||||||
|
<Arrow direction={isExpanded ? 'right' : 'left'} /> |
||||||
|
</ExpandButton> |
||||||
|
)} |
||||||
|
</Cell> |
||||||
|
{map(params, ({ |
||||||
|
id, |
||||||
|
lexic, |
||||||
|
lexica_short, |
||||||
|
}) => ( |
||||||
|
<Cell |
||||||
|
as='th' |
||||||
|
key={id} |
||||||
|
columnWidth={paramColumnWidth} |
||||||
|
onClick={handleSortClick(id)} |
||||||
|
sorted={sortCondition.paramId === id} |
||||||
|
tooltipText={translate(lexic)} |
||||||
|
anchorId={`param_${id}`} |
||||||
|
> |
||||||
|
<ParamShortTitle |
||||||
|
id={`param_${id}`} |
||||||
|
t={lexica_short || ''} |
||||||
|
sorted={sortCondition.paramId === id} |
||||||
|
sortDirection={sortCondition.dir} |
||||||
|
showLeftArrow={showLeftArrow} |
||||||
|
/> |
||||||
|
</Cell> |
||||||
|
))} |
||||||
|
</Row> |
||||||
|
</Header> |
||||||
|
|
||||||
|
<tbody> |
||||||
|
{map(players, (player) => { |
||||||
|
const playerName = getPlayerName(player) |
||||||
|
const playerNum = player.num ?? player.club_shirt_num |
||||||
|
const playerProfileUrl = `/${sportName}/players/${player.id}` |
||||||
|
|
||||||
|
return ( |
||||||
|
<Row key={player.id}> |
||||||
|
<Cell columnWidth={firstColumnWidth}> |
||||||
|
<PlayerNum>{playerNum}</PlayerNum>{' '} |
||||||
|
<PlayerName to={playerProfileUrl}> |
||||||
|
{playerName} |
||||||
|
</PlayerName> |
||||||
|
</Cell> |
||||||
|
{map(params, (param) => { |
||||||
|
const playerParam = getPlayerParams(player.id)[ |
||||||
|
param.id |
||||||
|
] as PlayerParam | undefined |
||||||
|
const value = playerParam ? getDisplayedValue(playerParam) : '-' |
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
const clickable = Boolean(playerParam?.clickable) && !includes([0, '-'], value) |
||||||
|
const sorted = sortCondition.paramId === param.id |
||||||
|
const onClick = () => { |
||||||
|
clickable && handleParamClick(param.id, player.id) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Cell |
||||||
|
columnWidth={paramColumnWidth} |
||||||
|
key={param.id} |
||||||
|
clickable={clickable} |
||||||
|
sorted={sorted} |
||||||
|
onClick={onClick} |
||||||
|
hasValue={value !== '-'} |
||||||
|
> |
||||||
|
{watchAllEpisodesTimer |
||||||
|
&& param.id === playingData.player.paramId |
||||||
|
&& player.id === playingData.player.id |
||||||
|
? ( |
||||||
|
<CircleAnimationBar |
||||||
|
text={value} |
||||||
|
size={20} |
||||||
|
/> |
||||||
|
) |
||||||
|
: value} |
||||||
|
</Cell> |
||||||
|
) |
||||||
|
})} |
||||||
|
</Row> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tbody> |
||||||
|
</Table> |
||||||
|
</TableWrapper> |
||||||
|
)} |
||||||
|
</Container> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,283 @@ |
|||||||
|
import { Link } from 'react-router-dom' |
||||||
|
|
||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isIOS, isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import { customScrollbar } from 'features/Common' |
||||||
|
import { |
||||||
|
ArrowButton as ArrowButtonBase, |
||||||
|
Arrow as ArrowBase, |
||||||
|
} from 'features/HeaderFilters/components/DateFilter/styled' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
type ContainerProps = { |
||||||
|
isExpanded?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Container = styled.div<ContainerProps>` |
||||||
|
--bgColor: #333; |
||||||
|
|
||||||
|
${({ isExpanded }) => (isExpanded |
||||||
|
? css` |
||||||
|
--bgColor: rgba(51, 51, 51, 0.7); |
||||||
|
` |
||||||
|
: css` |
||||||
|
position: relative; |
||||||
|
`)}
|
||||||
|
` |
||||||
|
|
||||||
|
type TableWrapperProps = { |
||||||
|
isExpanded?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const TableWrapper = styled.div<TableWrapperProps>` |
||||||
|
max-width: 100%; |
||||||
|
clip-path: inset(0 0 0 0 round 5px); |
||||||
|
overflow-x: auto; |
||||||
|
scroll-behavior: smooth; |
||||||
|
background:
|
||||||
|
linear-gradient(180deg, #292929 44px, var(--bgColor) 44px), |
||||||
|
linear-gradient(-90deg, #333 8px, var(--bgColor) 8px); |
||||||
|
z-index: 50; |
||||||
|
|
||||||
|
${customScrollbar} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:vertical { |
||||||
|
background: linear-gradient(180deg, transparent 44px, #3F3F3F 44px); |
||||||
|
} |
||||||
|
|
||||||
|
${({ isExpanded }) => (isExpanded |
||||||
|
? css` |
||||||
|
position: absolute; |
||||||
|
right: 14px; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
max-height: calc(100vh - 203px); |
||||||
|
`};
|
||||||
|
|
||||||
|
${isIOS |
||||||
|
? css` |
||||||
|
overscroll-behavior: none; |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const Table = styled.table` |
||||||
|
border-radius: 5px; |
||||||
|
border-spacing: 0; |
||||||
|
border-collapse: collapse;
|
||||||
|
letter-spacing: -0.078px; |
||||||
|
table-layout: fixed; |
||||||
|
` |
||||||
|
|
||||||
|
type ParamShortTitleProps = { |
||||||
|
showLeftArrow?: boolean, |
||||||
|
sortDirection: 'asc' | 'desc', |
||||||
|
sorted?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const ParamShortTitle = styled(T9n)<ParamShortTitleProps>` |
||||||
|
position: relative; |
||||||
|
text-transform: uppercase; |
||||||
|
|
||||||
|
::before { |
||||||
|
position: absolute; |
||||||
|
content: ''; |
||||||
|
top: 50%; |
||||||
|
left: -9px; |
||||||
|
translate: 0 -50%; |
||||||
|
rotate: ${({ sortDirection }) => (sortDirection === 'asc' ? 0 : 180)}deg; |
||||||
|
width: 7px; |
||||||
|
height: 7px; |
||||||
|
background-image: url(/images/sortUp.svg); |
||||||
|
background-size: cover; |
||||||
|
|
||||||
|
${({ sorted }) => (sorted |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
display: none; |
||||||
|
`)}
|
||||||
|
|
||||||
|
${({ showLeftArrow }) => (showLeftArrow |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
z-index: 1; |
||||||
|
`)}
|
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const PlayerNum = styled.span` |
||||||
|
display: inline-block; |
||||||
|
width: 17px; |
||||||
|
flex-shrink: 0; |
||||||
|
color: rgba(255, 255, 255, 0.5); |
||||||
|
` |
||||||
|
|
||||||
|
export const PlayerName = styled(Link)` |
||||||
|
display: inline-block; |
||||||
|
vertical-align: middle; |
||||||
|
text-overflow: ellipsis; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
overflow: hidden; |
||||||
|
` |
||||||
|
|
||||||
|
type CellContainerProps = { |
||||||
|
as?: 'td' | 'th', |
||||||
|
clickable?: boolean, |
||||||
|
columnWidth?: number, |
||||||
|
hasValue?: boolean, |
||||||
|
sorted?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const CellContainer = styled.td.attrs(({ clickable }: CellContainerProps) => ({ |
||||||
|
...clickable && { tabIndex: 0 }, |
||||||
|
}))<CellContainerProps>` |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
flex-shrink: 0; |
||||||
|
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')}; |
||||||
|
min-width: 30px; |
||||||
|
font-size: 11px; |
||||||
|
font-weight: ${({ clickable }) => (clickable ? 700 : 400)}; |
||||||
|
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; |
||||||
|
white-space: nowrap; |
||||||
|
background-color: var(--bgColor); |
||||||
|
|
||||||
|
:first-child { |
||||||
|
position: sticky; |
||||||
|
left: 0; |
||||||
|
justify-content: unset; |
||||||
|
padding-left: 10px; |
||||||
|
text-align: left; |
||||||
|
cursor: unset; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
|
||||||
|
${({ clickable }) => (clickable |
||||||
|
? css` |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
|
||||||
|
${({ as, sorted }) => (as === 'th' |
||||||
|
? css` |
||||||
|
font-weight: ${sorted ? '700' : '600'}; |
||||||
|
font-size: ${sorted ? 13 : 11}px; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
|
||||||
|
${({ hasValue }) => (!hasValue |
||||||
|
? css` |
||||||
|
color: rgba(255, 255, 255, 0.5); |
||||||
|
` |
||||||
|
: '')} |
||||||
|
` |
||||||
|
|
||||||
|
export const Row = styled.tr` |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
width: 100%; |
||||||
|
height: 45px; |
||||||
|
border-bottom: 0.5px solid #5C5C5C; |
||||||
|
z-index: 1; |
||||||
|
|
||||||
|
:last-child:not(:first-child) { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
|
||||||
|
:hover { |
||||||
|
${CellContainer}:not(th) { |
||||||
|
background-color: #484848; |
||||||
|
} |
||||||
|
|
||||||
|
${PlayerName} { |
||||||
|
text-decoration: underline; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Header = styled.thead` |
||||||
|
position: sticky; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
z-index: 2; |
||||||
|
|
||||||
|
${Row} { |
||||||
|
border-bottom-color: ${({ theme }) => theme.colors.secondary}; |
||||||
|
} |
||||||
|
|
||||||
|
${CellContainer} { |
||||||
|
background-color: #292929; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
${CellContainer}:first-child { |
||||||
|
cursor: unset; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Arrow = styled(ArrowBase)` |
||||||
|
width: 10px; |
||||||
|
height: 10px; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
border-color: ${({ theme }) => theme.colors.white}; |
||||||
|
` |
||||||
|
: ''};
|
||||||
|
` |
||||||
|
|
||||||
|
const ArrowButton = styled(ArrowButtonBase)` |
||||||
|
position: absolute; |
||||||
|
width: 20px; |
||||||
|
height: 44px; |
||||||
|
margin-top: 0; |
||||||
|
z-index: 3; |
||||||
|
background-color: #292929; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
margin-top: 0; |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const ArrowButtonRight = styled(ArrowButton)` |
||||||
|
right: 0; |
||||||
|
border-top-right-radius: 5px; |
||||||
|
|
||||||
|
${Arrow} { |
||||||
|
left: auto; |
||||||
|
right: 7px; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const ArrowButtonLeft = styled(ArrowButton)` |
||||||
|
right: -5px; |
||||||
|
` |
||||||
|
|
||||||
|
type ExpandButtonProps = { |
||||||
|
isExpanded?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const ExpandButton = styled(ArrowButton)<ExpandButtonProps>` |
||||||
|
left: 20px; |
||||||
|
top: 0; |
||||||
|
|
||||||
|
${Arrow} { |
||||||
|
left: ${({ isExpanded }) => (isExpanded ? -6 : -2)}px; |
||||||
|
|
||||||
|
:last-child { |
||||||
|
margin-left: 7px; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
export type PlayersTableProps = { |
||||||
|
teamId: number, |
||||||
|
} |
||||||
|
|
||||||
|
export type SortCondition = { |
||||||
|
clicksCount: number, |
||||||
|
dir: 'asc' | 'desc', |
||||||
|
paramId: number | null, |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
import type { Playlists, PlaylistOption } from 'features/MatchPage/types' |
||||||
|
import type { MatchInfo } from 'requests' |
||||||
|
|
||||||
|
import { PlayersPlaylists } from '../PlayersPlaylists' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
onSelect: (option: PlaylistOption) => void, |
||||||
|
playlists: Playlists, |
||||||
|
profile: MatchInfo, |
||||||
|
selectedPlaylist?: PlaylistOption, |
||||||
|
} |
||||||
|
|
||||||
|
export const TabPlayers = ({ |
||||||
|
onSelect, |
||||||
|
playlists, |
||||||
|
profile, |
||||||
|
selectedPlaylist, |
||||||
|
}: Props) => ( |
||||||
|
<PlayersPlaylists |
||||||
|
profile={profile} |
||||||
|
players={playlists.players} |
||||||
|
selectedMathPlaylist={selectedPlaylist} |
||||||
|
onSelect={onSelect} |
||||||
|
/> |
||||||
|
) |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
export enum Tabs { |
||||||
|
TEAMS, |
||||||
|
TEAM1, |
||||||
|
TEAM2, |
||||||
|
} |
||||||
|
|
||||||
|
export enum StatsType { |
||||||
|
FINAL_STATS, |
||||||
|
CURRENT_STATS, |
||||||
|
} |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
import { useEffect, useState } from 'react' |
||||||
|
|
||||||
|
import isEmpty from 'lodash/isEmpty' |
||||||
|
|
||||||
|
import { useTooltip } from 'hooks' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
|
||||||
|
import { StatsType, Tabs } from './config' |
||||||
|
|
||||||
|
export const useTabStats = () => { |
||||||
|
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS) |
||||||
|
|
||||||
|
const { |
||||||
|
isEmptyPlayersStats, |
||||||
|
profile: matchProfile, |
||||||
|
statsType, |
||||||
|
teamsStats, |
||||||
|
toggleStatsType, |
||||||
|
} = useMatchPageStore() |
||||||
|
|
||||||
|
const { |
||||||
|
isTooltipShown, |
||||||
|
onMouseLeave, |
||||||
|
onMouseOver, |
||||||
|
tooltipStyle, |
||||||
|
tooltipText, |
||||||
|
} = useTooltip() |
||||||
|
|
||||||
|
const isFinalStatsType = statsType === StatsType.FINAL_STATS |
||||||
|
|
||||||
|
const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats' |
||||||
|
const switchButtonTooltipLexic = 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), |
||||||
|
) |
||||||
|
|
||||||
|
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, |
||||||
|
isTooltipShown, |
||||||
|
isVisibleTeam1PlayersTab, |
||||||
|
isVisibleTeam2PlayersTab, |
||||||
|
isVisibleTeamsTab, |
||||||
|
onMouseLeave, |
||||||
|
onMouseOver, |
||||||
|
selectedTab, |
||||||
|
setSelectedTab, |
||||||
|
switchButtonTooltipLexic, |
||||||
|
switchTitleLexic, |
||||||
|
toggleStatsType, |
||||||
|
tooltipStyle, |
||||||
|
tooltipText, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,165 @@ |
|||||||
|
import type { ComponentProps } from 'react' |
||||||
|
import { createPortal } from 'react-dom' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import { getTeamAbbr } from 'helpers' |
||||||
|
|
||||||
|
import { useModalRoot } from 'hooks' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
import { Name } from 'features/Name' |
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
|
||||||
|
import { Tabs } from './config' |
||||||
|
import { useTabStats } from './hooks' |
||||||
|
import { PlayersTable } from '../PlayersTable' |
||||||
|
import { TeamsStatsTable } from '../TeamsStatsTable' |
||||||
|
|
||||||
|
import { |
||||||
|
Container, |
||||||
|
Header, |
||||||
|
TabList, |
||||||
|
Tab, |
||||||
|
Switch, |
||||||
|
SwitchTitle, |
||||||
|
SwitchButton, |
||||||
|
Tooltip, |
||||||
|
TabTitle, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
const tabPanes = { |
||||||
|
[Tabs.TEAMS]: TeamsStatsTable, |
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
[Tabs.TEAM1]: (props: ComponentProps<typeof PlayersTable>) => <PlayersTable {...props} />, |
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
[Tabs.TEAM2]: (props: ComponentProps<typeof PlayersTable>) => <PlayersTable {...props} />, |
||||||
|
} |
||||||
|
|
||||||
|
export const TabStats = () => { |
||||||
|
const { |
||||||
|
isFinalStatsType, |
||||||
|
isTooltipShown, |
||||||
|
isVisibleTeam1PlayersTab, |
||||||
|
isVisibleTeam2PlayersTab, |
||||||
|
isVisibleTeamsTab, |
||||||
|
onMouseLeave, |
||||||
|
onMouseOver, |
||||||
|
selectedTab, |
||||||
|
setSelectedTab, |
||||||
|
switchButtonTooltipLexic, |
||||||
|
switchTitleLexic, |
||||||
|
toggleStatsType, |
||||||
|
tooltipStyle, |
||||||
|
tooltipText, |
||||||
|
} = useTabStats() |
||||||
|
const { profile: matchProfile } = useMatchPageStore() |
||||||
|
const { suffix, translate } = useLexicsStore() |
||||||
|
|
||||||
|
const modalRoot = useModalRoot() |
||||||
|
|
||||||
|
const TabPane = tabPanes[selectedTab] |
||||||
|
|
||||||
|
if (!matchProfile) return null |
||||||
|
|
||||||
|
const { team1, team2 } = matchProfile |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container> |
||||||
|
<Header> |
||||||
|
<TabList> |
||||||
|
{isVisibleTeamsTab && ( |
||||||
|
<Tab |
||||||
|
aria-pressed={selectedTab === Tabs.TEAMS} |
||||||
|
onClick={() => setSelectedTab(Tabs.TEAMS)} |
||||||
|
> |
||||||
|
<TabTitle> |
||||||
|
<T9n t='team' /> |
||||||
|
</TabTitle> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
{isVisibleTeam1PlayersTab && ( |
||||||
|
<Tab |
||||||
|
aria-pressed={selectedTab === Tabs.TEAM1} |
||||||
|
onClick={() => setSelectedTab(Tabs.TEAM1)} |
||||||
|
> |
||||||
|
<TabTitle |
||||||
|
teamColor={team1.shirt_color} |
||||||
|
onMouseOver={isMobileDevice |
||||||
|
? undefined |
||||||
|
: onMouseOver({ |
||||||
|
anchorId: 'team1Tab', |
||||||
|
horizontalPosition: 'left', |
||||||
|
indent: 25, |
||||||
|
tooltipText: team1[`name_${suffix}`], |
||||||
|
})} |
||||||
|
onMouseLeave={isMobileDevice ? undefined : onMouseLeave} |
||||||
|
> |
||||||
|
<Name |
||||||
|
id='team1Tab' |
||||||
|
nameObj={{ |
||||||
|
name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng), |
||||||
|
name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus), |
||||||
|
}} |
||||||
|
/> |
||||||
|
</TabTitle> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
{isVisibleTeam2PlayersTab && ( |
||||||
|
<Tab |
||||||
|
aria-pressed={selectedTab === Tabs.TEAM2} |
||||||
|
onClick={() => setSelectedTab(Tabs.TEAM2)} |
||||||
|
> |
||||||
|
<TabTitle |
||||||
|
teamColor={team2.shirt_color} |
||||||
|
onMouseOver={isMobileDevice |
||||||
|
? undefined |
||||||
|
: onMouseOver({ |
||||||
|
anchorId: 'team2Tab', |
||||||
|
horizontalPosition: 'left', |
||||||
|
indent: 25, |
||||||
|
tooltipText: team2[`name_${suffix}`], |
||||||
|
})} |
||||||
|
onMouseLeave={isMobileDevice ? undefined : onMouseLeave} |
||||||
|
> |
||||||
|
<Name |
||||||
|
id='team2Tab' |
||||||
|
nameObj={{ |
||||||
|
name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng), |
||||||
|
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus), |
||||||
|
}} |
||||||
|
/> |
||||||
|
</TabTitle> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
</TabList> |
||||||
|
<Switch> |
||||||
|
<SwitchTitle t={switchTitleLexic} /> |
||||||
|
<SwitchButton |
||||||
|
id='switchButton' |
||||||
|
isFinalStatsType={isFinalStatsType} |
||||||
|
onClick={toggleStatsType} |
||||||
|
onMouseOver={isMobileDevice |
||||||
|
? undefined |
||||||
|
: onMouseOver({ |
||||||
|
anchorId: 'switchButton', |
||||||
|
horizontalPosition: 'right', |
||||||
|
tooltipText: translate(switchButtonTooltipLexic), |
||||||
|
})} |
||||||
|
onMouseLeave={isMobileDevice ? undefined : onMouseLeave} |
||||||
|
/> |
||||||
|
</Switch> |
||||||
|
</Header> |
||||||
|
<TabPane |
||||||
|
teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id} |
||||||
|
/> |
||||||
|
{isTooltipShown && modalRoot.current && createPortal( |
||||||
|
<Tooltip style={tooltipStyle}> |
||||||
|
{tooltipText} |
||||||
|
</Tooltip>, |
||||||
|
modalRoot.current, |
||||||
|
)} |
||||||
|
</Container> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,134 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { TooltipWrapper } from 'features/Tooltip' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
export const Container = styled.div`` |
||||||
|
|
||||||
|
export const Header = styled.div` |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
margin-bottom: 6px; |
||||||
|
` |
||||||
|
|
||||||
|
export const TabList = styled.div.attrs({ role: 'tablist' })` |
||||||
|
display: flex; |
||||||
|
` |
||||||
|
|
||||||
|
export const Tooltip = styled(TooltipWrapper)` |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
height: 17px; |
||||||
|
padding: 0 10px; |
||||||
|
border-radius: 6px; |
||||||
|
transform: none; |
||||||
|
font-size: 11px; |
||||||
|
line-height: 1; |
||||||
|
color: ${({ theme }) => theme.colors.black}; |
||||||
|
|
||||||
|
::before { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
type TabTitleProps = { |
||||||
|
teamColor?: string | null, |
||||||
|
} |
||||||
|
|
||||||
|
export const TabTitle = styled.span<TabTitleProps>` |
||||||
|
position: relative; |
||||||
|
color: rgba(255, 255, 255, 0.5); |
||||||
|
|
||||||
|
${({ teamColor, theme }) => (teamColor |
||||||
|
? css` |
||||||
|
::before { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
left: -8px; |
||||||
|
top: 50%; |
||||||
|
translate: 0 -50%; |
||||||
|
width: 5px; |
||||||
|
height: 5px; |
||||||
|
outline: ${teamColor.toUpperCase() === theme.colors.white ? 'none' : `0.5px solid ${theme.colors.white}`}; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: ${teamColor}; |
||||||
|
} |
||||||
|
` |
||||||
|
: '' |
||||||
|
)} |
||||||
|
` |
||||||
|
|
||||||
|
export const Tab = styled.button.attrs({ role: 'tab' })` |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
padding: 0 10px 10px; |
||||||
|
font-size: 12px; |
||||||
|
cursor: pointer; |
||||||
|
border: none; |
||||||
|
background: none; |
||||||
|
border-bottom: 2px solid transparent; |
||||||
|
|
||||||
|
&[aria-pressed="true"] { |
||||||
|
border-color: ${({ theme }) => theme.colors.white}; |
||||||
|
|
||||||
|
${TabTitle} { |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Switch = styled.div` |
||||||
|
display: flex; |
||||||
|
` |
||||||
|
|
||||||
|
export const SwitchTitle = styled(T9n)` |
||||||
|
font-size: 12px; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
white-space: nowrap; |
||||||
|
` |
||||||
|
|
||||||
|
type SwitchButtonProps = { |
||||||
|
isFinalStatsType: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const SwitchButton = styled.button<SwitchButtonProps>` |
||||||
|
width: 20px; |
||||||
|
height: 7px; |
||||||
|
margin-left: 5px; |
||||||
|
margin-top: 5px; |
||||||
|
border-radius: 2px; |
||||||
|
border: none; |
||||||
|
border: 1px solid ${({ theme }) => theme.colors.white}; |
||||||
|
cursor: pointer; |
||||||
|
|
||||||
|
${({ isFinalStatsType, theme }) => (!isFinalStatsType |
||||||
|
? css` |
||||||
|
background-image: linear-gradient( |
||||||
|
to right, |
||||||
|
${theme.colors.white} 33.333%, |
||||||
|
${theme.colors.black} 33.333%, |
||||||
|
${theme.colors.black} 66.666%, |
||||||
|
${theme.colors.white} 66.666%, |
||||||
|
${theme.colors.white} 72%, |
||||||
|
${theme.colors.black} 72%, |
||||||
|
${theme.colors.black} 100%) |
||||||
|
` |
||||||
|
: css` |
||||||
|
border-color: transparent; |
||||||
|
background-image: linear-gradient( |
||||||
|
to right, |
||||||
|
${theme.colors.white} 33.333%, |
||||||
|
${theme.colors.black} 33.333%, |
||||||
|
${theme.colors.black} 38%, |
||||||
|
${theme.colors.white} 38%, |
||||||
|
${theme.colors.white} 66.666%, |
||||||
|
${theme.colors.black} 66.666%, |
||||||
|
${theme.colors.black} 72%, |
||||||
|
${theme.colors.white} 72%, |
||||||
|
${theme.colors.white} 100%) |
||||||
|
` |
||||||
|
)} |
||||||
|
` |
||||||
@ -1,92 +0,0 @@ |
|||||||
import { useCallback, useMemo } from 'react' |
|
||||||
|
|
||||||
import { format } from 'date-fns' |
|
||||||
|
|
||||||
import { parseDate } from 'helpers/parseDate' |
|
||||||
|
|
||||||
import { WeekDay, Wrapper } from './styled' |
|
||||||
|
|
||||||
export type Props = { |
|
||||||
isInitialDateHidden: boolean, |
|
||||||
matchDates: Array<string>, |
|
||||||
onDateClick: (date: string) => void, |
|
||||||
profileDate: string, |
|
||||||
selectedDate: string, |
|
||||||
} |
|
||||||
|
|
||||||
export const VideoDate = (props: Props) => { |
|
||||||
const { |
|
||||||
isInitialDateHidden, |
|
||||||
matchDates, |
|
||||||
onDateClick, |
|
||||||
profileDate, |
|
||||||
selectedDate, |
|
||||||
} = props |
|
||||||
|
|
||||||
const selectedDateIndex = useMemo(() => ( |
|
||||||
matchDates.findIndex((date) => date === selectedDate) |
|
||||||
), [matchDates, selectedDate]) |
|
||||||
|
|
||||||
const lastDateIndex = matchDates.length - 1 |
|
||||||
|
|
||||||
const initialDateIndex = useMemo(() => ( |
|
||||||
matchDates.findIndex((date) => date === profileDate) |
|
||||||
), [matchDates, profileDate]) |
|
||||||
|
|
||||||
const currentDay = useMemo(() => ( |
|
||||||
matchDates.length && !(isInitialDateHidden && selectedDateIndex === initialDateIndex) |
|
||||||
? matchDates[selectedDateIndex] |
|
||||||
: null |
|
||||||
), [initialDateIndex, isInitialDateHidden, matchDates, selectedDateIndex]) |
|
||||||
|
|
||||||
const previousDay = useMemo(() => { |
|
||||||
if (selectedDateIndex !== 0) { |
|
||||||
if (isInitialDateHidden && selectedDateIndex - 1 === initialDateIndex) { |
|
||||||
return selectedDateIndex - 1 !== lastDateIndex ? matchDates[selectedDateIndex - 2] : null |
|
||||||
} |
|
||||||
return matchDates[selectedDateIndex - 1] |
|
||||||
} |
|
||||||
return null |
|
||||||
}, [initialDateIndex, isInitialDateHidden, lastDateIndex, matchDates, selectedDateIndex]) |
|
||||||
|
|
||||||
const nextDay = useMemo(() => { |
|
||||||
if (selectedDateIndex !== lastDateIndex) { |
|
||||||
if (isInitialDateHidden && selectedDateIndex + 1 === initialDateIndex) { |
|
||||||
return selectedDateIndex + 1 !== lastDateIndex ? matchDates[selectedDateIndex + 2] : null |
|
||||||
} |
|
||||||
return matchDates[selectedDateIndex + 1] |
|
||||||
} |
|
||||||
return null |
|
||||||
}, [initialDateIndex, isInitialDateHidden, lastDateIndex, matchDates, selectedDateIndex]) |
|
||||||
|
|
||||||
const onDayClick = (date: string) => { |
|
||||||
onDateClick?.(date) |
|
||||||
} |
|
||||||
|
|
||||||
const formatDate = useCallback((date: string) => ( |
|
||||||
format(parseDate(date, 'yyyy-MM-dd'), 'MMM dd, EE') |
|
||||||
), []) |
|
||||||
|
|
||||||
return ( |
|
||||||
<Wrapper> |
|
||||||
{previousDay && ( |
|
||||||
<WeekDay |
|
||||||
onClick={() => onDayClick(previousDay)} |
|
||||||
>{formatDate(previousDay)} |
|
||||||
</WeekDay> |
|
||||||
)} |
|
||||||
{currentDay && ( |
|
||||||
<WeekDay |
|
||||||
isActive |
|
||||||
>{formatDate(currentDay)} |
|
||||||
</WeekDay> |
|
||||||
)} |
|
||||||
{nextDay && ( |
|
||||||
<WeekDay |
|
||||||
onClick={() => onDayClick(nextDay)} |
|
||||||
>{formatDate(nextDay)} |
|
||||||
</WeekDay> |
|
||||||
)} |
|
||||||
</Wrapper> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,52 +0,0 @@ |
|||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
|
|
||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
|
|
||||||
export const Wrapper = styled.div` |
|
||||||
color: #FFFFFF; |
|
||||||
display: flex; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
margin-bottom: 10px; |
|
||||||
|
|
||||||
> :not(:last-child) { |
|
||||||
margin-right: 20px; |
|
||||||
} |
|
||||||
|
|
||||||
${isMobileDevice ? css` |
|
||||||
@media screen and (orientation: landscape){
|
|
||||||
> :not(:last-child) { |
|
||||||
margin-right: 3px; |
|
||||||
} |
|
||||||
} |
|
||||||
` : ''}
|
|
||||||
` |
|
||||||
|
|
||||||
export const WeekDay = styled.div.attrs(() => ({ |
|
||||||
'aria-hidden': true, |
|
||||||
}))<{isActive?: boolean}>` |
|
||||||
position: relative; |
|
||||||
color: rgba(255, 255, 255, 0.5); |
|
||||||
font-size: 12px; |
|
||||||
white-space: nowrap; |
|
||||||
padding: 5px; |
|
||||||
cursor: pointer; |
|
||||||
|
|
||||||
${({ isActive }) => ( |
|
||||||
isActive |
|
||||||
? css` |
|
||||||
color: #FFFFFF; |
|
||||||
cursor: default; |
|
||||||
|
|
||||||
:after { |
|
||||||
position: absolute; |
|
||||||
bottom: 0; |
|
||||||
left: 0; |
|
||||||
content: ''; |
|
||||||
width: 100%; |
|
||||||
height: 2px; |
|
||||||
background-color: #FFFFFF; |
|
||||||
} |
|
||||||
` |
|
||||||
: '')} |
|
||||||
` |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
import { customScrollbar } from 'features/Common' |
|
||||||
import { isMobileDevice } from '../../../../config/userAgent' |
|
||||||
|
|
||||||
export const MatchesWrapper = styled.div<{hasScroll?: boolean}>` |
|
||||||
overflow-y: auto; |
|
||||||
max-height: calc(100vh - 170px); |
|
||||||
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')}; |
|
||||||
|
|
||||||
> * { |
|
||||||
:not(:last-child) { |
|
||||||
margin-bottom: 10px; |
|
||||||
}
|
|
||||||
} |
|
||||||
|
|
||||||
${customScrollbar} |
|
||||||
|
|
||||||
${isMobileDevice ? css` |
|
||||||
overflow: hidden; |
|
||||||
max-height: initial; |
|
||||||
` : ''}
|
|
||||||
` |
|
||||||
@ -0,0 +1,176 @@ |
|||||||
|
import { Fragment, useRef } from 'react' |
||||||
|
import { useQueryClient } from 'react-query' |
||||||
|
|
||||||
|
import isNumber from 'lodash/isNumber' |
||||||
|
|
||||||
|
import { KEYBOARD_KEYS, querieKeys } from 'config' |
||||||
|
|
||||||
|
import type { |
||||||
|
Param, |
||||||
|
TeamStatItem, |
||||||
|
MatchScore, |
||||||
|
} from 'requests' |
||||||
|
import { getStatsEvents } from 'requests' |
||||||
|
|
||||||
|
import { usePageParams, useEventListener } from 'hooks' |
||||||
|
|
||||||
|
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' |
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
|
||||||
|
import { StatsType } from '../TabStats/config' |
||||||
|
import { CircleAnimationBar } from '../CircleAnimationBar' |
||||||
|
|
||||||
|
import { |
||||||
|
CellContainer, |
||||||
|
ParamValueContainer, |
||||||
|
ParamValue, |
||||||
|
Divider, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type CellProps = { |
||||||
|
teamId: number, |
||||||
|
teamStatItem: TeamStatItem | null, |
||||||
|
} |
||||||
|
|
||||||
|
export const Cell = ({ |
||||||
|
teamId, |
||||||
|
teamStatItem, |
||||||
|
}: CellProps) => { |
||||||
|
const paramValueContainerRef = useRef(null) |
||||||
|
|
||||||
|
const { profileId, sportType } = usePageParams() |
||||||
|
|
||||||
|
const { |
||||||
|
playingData, |
||||||
|
playingProgress, |
||||||
|
playStatsEpisodes, |
||||||
|
profile, |
||||||
|
setIsPlayingFiltersEpisodes, |
||||||
|
setPlayingData, |
||||||
|
setWatchAllEpisodesTimer, |
||||||
|
statsType, |
||||||
|
watchAllEpisodesTimer, |
||||||
|
} = useMatchPageStore() |
||||||
|
|
||||||
|
const client = useQueryClient() |
||||||
|
|
||||||
|
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) |
||||||
|
|
||||||
|
const isClickable = (param: Param) => ( |
||||||
|
Boolean(param.val) && param.clickable |
||||||
|
) |
||||||
|
|
||||||
|
const getDisplayedValue = (val: number | null) => ( |
||||||
|
isNumber(val) ? String(val) : '-' |
||||||
|
) |
||||||
|
|
||||||
|
const onParamClick = async (param: Param) => { |
||||||
|
if (!isClickable(param)) return |
||||||
|
|
||||||
|
const videoBounds = matchScore?.video_bounds || profile?.video_bounds |
||||||
|
|
||||||
|
setWatchAllEpisodesTimer(false) |
||||||
|
setIsPlayingFiltersEpisodes(false) |
||||||
|
|
||||||
|
setPlayingData({ |
||||||
|
player: { |
||||||
|
id: null, |
||||||
|
paramId: null, |
||||||
|
}, |
||||||
|
team: { |
||||||
|
id: teamId, |
||||||
|
paramId: param.id, |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
try { |
||||||
|
const events = await getStatsEvents({ |
||||||
|
matchId: profileId, |
||||||
|
paramId: param.id, |
||||||
|
sportType, |
||||||
|
teamId, |
||||||
|
...(statsType === StatsType.CURRENT_STATS && videoBounds && ( |
||||||
|
getHalfTime(videoBounds, playingProgress) |
||||||
|
)), |
||||||
|
}) |
||||||
|
|
||||||
|
playStatsEpisodes(events) |
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {} |
||||||
|
} |
||||||
|
|
||||||
|
useEventListener({ |
||||||
|
callback: (e) => { |
||||||
|
if (e.key !== KEYBOARD_KEYS.Enter || !teamStatItem) return |
||||||
|
|
||||||
|
const paramId = Number((e.target as HTMLElement).dataset.paramId) |
||||||
|
|
||||||
|
const param = paramId && (teamStatItem.param1.id === paramId |
||||||
|
? teamStatItem.param1 |
||||||
|
: teamStatItem.param2) |
||||||
|
|
||||||
|
param && onParamClick(param) |
||||||
|
}, |
||||||
|
event: 'keydown', |
||||||
|
target: paramValueContainerRef, |
||||||
|
}) |
||||||
|
|
||||||
|
if (!teamStatItem) return null |
||||||
|
|
||||||
|
return ( |
||||||
|
<CellContainer> |
||||||
|
<ParamValueContainer ref={paramValueContainerRef}> |
||||||
|
{watchAllEpisodesTimer |
||||||
|
&& playingData.team.paramId === teamStatItem.param1.id |
||||||
|
&& playingData.team.id === teamId |
||||||
|
? ( |
||||||
|
<ParamValue> |
||||||
|
<CircleAnimationBar |
||||||
|
text={getDisplayedValue(teamStatItem.param1.val)} |
||||||
|
size={20} |
||||||
|
/> |
||||||
|
</ParamValue> |
||||||
|
) |
||||||
|
: ( |
||||||
|
<ParamValue |
||||||
|
clickable={isClickable(teamStatItem.param1)} |
||||||
|
onClick={() => onParamClick(teamStatItem.param1)} |
||||||
|
data-param-id={teamStatItem.param1.id} |
||||||
|
hasValue={Boolean(teamStatItem.param1.val)} |
||||||
|
> |
||||||
|
{getDisplayedValue(teamStatItem.param1.val)} |
||||||
|
</ParamValue> |
||||||
|
)} |
||||||
|
|
||||||
|
{teamStatItem.param2 && ( |
||||||
|
<Fragment> |
||||||
|
{watchAllEpisodesTimer |
||||||
|
&& playingData.team.paramId === teamStatItem.param2.id |
||||||
|
&& playingData.team.id === teamId |
||||||
|
? ( |
||||||
|
<ParamValue> |
||||||
|
<CircleAnimationBar |
||||||
|
text={getDisplayedValue(teamStatItem.param2.val)} |
||||||
|
size={20} |
||||||
|
/> |
||||||
|
</ParamValue> |
||||||
|
) |
||||||
|
: ( |
||||||
|
<Fragment> |
||||||
|
<Divider>/</Divider> |
||||||
|
<ParamValue |
||||||
|
clickable={isClickable(teamStatItem.param2)} |
||||||
|
onClick={() => onParamClick(teamStatItem.param2!)} |
||||||
|
data-param-id={teamStatItem.param2.id} |
||||||
|
hasValue={Boolean(teamStatItem.param2.val)} |
||||||
|
> |
||||||
|
{getDisplayedValue(teamStatItem.param2.val)} |
||||||
|
</ParamValue> |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
</ParamValueContainer> |
||||||
|
</CellContainer> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import { useEffect } from 'react' |
||||||
|
|
||||||
|
import find from 'lodash/find' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
|
||||||
|
export const useTeamsStatsTable = () => { |
||||||
|
const { |
||||||
|
plaingOrder, |
||||||
|
profile, |
||||||
|
setCircleAnimation, |
||||||
|
teamsStats, |
||||||
|
} = useMatchPageStore() |
||||||
|
|
||||||
|
const getStatItemById = (paramId: number) => { |
||||||
|
if (!profile) return null |
||||||
|
|
||||||
|
return find(teamsStats[profile?.team2.id], ({ param1 }) => param1.id === paramId) || null |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setCircleAnimation((state) => ({ |
||||||
|
...state, |
||||||
|
plaingOrder, |
||||||
|
})) |
||||||
|
}, [setCircleAnimation, plaingOrder]) |
||||||
|
|
||||||
|
return { |
||||||
|
getStatItemById, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
import map from 'lodash/map' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
import { Loader } from 'features/Loader' |
||||||
|
import { defaultTheme } from 'features/Theme/config' |
||||||
|
|
||||||
|
import { useTeamsStatsTable } from './hooks' |
||||||
|
import { Cell } from './Cell' |
||||||
|
import { |
||||||
|
Container, |
||||||
|
TableWrapper, |
||||||
|
Table, |
||||||
|
Header, |
||||||
|
Row, |
||||||
|
CellContainer, |
||||||
|
TeamShortName, |
||||||
|
StatItemTitle, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export const TeamsStatsTable = () => { |
||||||
|
const { |
||||||
|
isTeamsStatsFetching, |
||||||
|
profile, |
||||||
|
teamsStats, |
||||||
|
} = useMatchPageStore() |
||||||
|
|
||||||
|
const { getStatItemById } = useTeamsStatsTable() |
||||||
|
|
||||||
|
const { shortSuffix } = useLexicsStore() |
||||||
|
|
||||||
|
if (!profile) return null |
||||||
|
|
||||||
|
if (isTeamsStatsFetching) { |
||||||
|
return ( |
||||||
|
<Loader color={defaultTheme.colors.white} /> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container> |
||||||
|
<TableWrapper> |
||||||
|
<Table role='marquee' aria-live='off'> |
||||||
|
<Header> |
||||||
|
<Row> |
||||||
|
<CellContainer as='th'> |
||||||
|
<TeamShortName |
||||||
|
nameObj={profile.team1} |
||||||
|
prefix='abbrev_' |
||||||
|
/> |
||||||
|
</CellContainer> |
||||||
|
<CellContainer as='th' /> |
||||||
|
<CellContainer as='th'> |
||||||
|
<TeamShortName |
||||||
|
nameObj={profile.team2} |
||||||
|
prefix='abbrev_' |
||||||
|
/> |
||||||
|
</CellContainer> |
||||||
|
</Row> |
||||||
|
</Header> |
||||||
|
|
||||||
|
<tbody> |
||||||
|
{map(teamsStats[profile.team1.id], (team1StatItem) => { |
||||||
|
const team2StatItem = getStatItemById(team1StatItem.param1.id) |
||||||
|
const statItemTitle = team1StatItem[`name_${shortSuffix}`] |
||||||
|
|
||||||
|
return ( |
||||||
|
<Row key={team1StatItem.param1.id}> |
||||||
|
<Cell |
||||||
|
teamStatItem={team1StatItem} |
||||||
|
teamId={profile.team1.id} |
||||||
|
/> |
||||||
|
|
||||||
|
<CellContainer> |
||||||
|
<StatItemTitle>{statItemTitle}</StatItemTitle> |
||||||
|
</CellContainer> |
||||||
|
|
||||||
|
<Cell |
||||||
|
teamStatItem={team2StatItem} |
||||||
|
teamId={profile.team2.id} |
||||||
|
/> |
||||||
|
</Row> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tbody> |
||||||
|
</Table> |
||||||
|
</TableWrapper> |
||||||
|
</Container> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,126 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import { Name } from 'features/Name' |
||||||
|
import { customScrollbar } from 'features/Common' |
||||||
|
|
||||||
|
export const Container = styled.div`` |
||||||
|
|
||||||
|
export const TableWrapper = styled.div` |
||||||
|
width: 100%; |
||||||
|
overflow: auto; |
||||||
|
font-size: 11px; |
||||||
|
clip-path: inset(0 0 0 0 round 5px); |
||||||
|
background-color: #333333; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
max-height: calc(100vh - 203px); |
||||||
|
`};
|
||||||
|
|
||||||
|
${customScrollbar} |
||||||
|
` |
||||||
|
|
||||||
|
export const Table = styled.table` |
||||||
|
width: 100%; |
||||||
|
border-spacing: 0; |
||||||
|
border-collapse: collapse;
|
||||||
|
letter-spacing: -0.078px; |
||||||
|
table-layout: fixed; |
||||||
|
` |
||||||
|
|
||||||
|
export const TeamShortName = styled(Name)` |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
letter-spacing: -0.078px; |
||||||
|
text-transform: uppercase; |
||||||
|
font-weight: 600; |
||||||
|
` |
||||||
|
|
||||||
|
export const CellContainer = styled.td` |
||||||
|
height: 45px; |
||||||
|
border-bottom: 0.5px solid #5C5C5C; |
||||||
|
background-color: #333333; |
||||||
|
|
||||||
|
:nth-child(2) { |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
:first-child, :last-child { |
||||||
|
width: 32px; |
||||||
|
} |
||||||
|
|
||||||
|
:first-child { |
||||||
|
padding-left: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
:last-child { |
||||||
|
text-align: right; |
||||||
|
padding-right: 12px; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Row = styled.tr` |
||||||
|
:last-child:not(:first-child) { |
||||||
|
${CellContainer} { |
||||||
|
border-bottom: none; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Header = styled.thead` |
||||||
|
position: sticky; |
||||||
|
top: 0; |
||||||
|
z-index: 1; |
||||||
|
|
||||||
|
${CellContainer} { |
||||||
|
background-color: #292929; |
||||||
|
border-bottom-color: ${({ theme }) => theme.colors.secondary}; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const ParamValueContainer = styled.div`` |
||||||
|
|
||||||
|
type TParamValue = { |
||||||
|
clickable?: boolean, |
||||||
|
hasValue?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({ |
||||||
|
...clickable && { tabIndex: 0 }, |
||||||
|
}))<TParamValue>` |
||||||
|
display: inline-block; |
||||||
|
width: 15px; |
||||||
|
height: 15px; |
||||||
|
text-align: center; |
||||||
|
position: relative; |
||||||
|
font-weight: ${({ clickable }) => (clickable ? 700 : 400)}; |
||||||
|
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; |
||||||
|
|
||||||
|
${({ clickable }) => (clickable |
||||||
|
? css` |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
|
||||||
|
${({ hasValue }) => (!hasValue |
||||||
|
? css` |
||||||
|
color: rgba(255, 255, 255, 0.5); |
||||||
|
` |
||||||
|
: '')} |
||||||
|
` |
||||||
|
|
||||||
|
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; |
||||||
|
` |
||||||
@ -1,5 +1,6 @@ |
|||||||
export enum Tabs { |
export enum Tabs { |
||||||
WATCH, |
WATCH, |
||||||
EVENTS, |
EVENTS, |
||||||
VIDEO |
STATS, |
||||||
|
PLAYERS, |
||||||
} |
} |
||||||
|
|||||||
@ -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]) |
||||||
|
} |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
import { useRef } from 'react' |
||||||
|
|
||||||
|
export const MODAL_ROOT_ID = 'modal-root' |
||||||
|
|
||||||
|
export const useModalRoot = () => useRef(document.getElementById(MODAL_ROOT_ID)) |
||||||
@ -0,0 +1,89 @@ |
|||||||
|
import type { CSSProperties, MouseEvent } from 'react' |
||||||
|
import { useState } from 'react' |
||||||
|
|
||||||
|
import isUndefined from 'lodash/isUndefined' |
||||||
|
|
||||||
|
import { useToggle } from './useToggle' |
||||||
|
|
||||||
|
type TooltipParams = { |
||||||
|
anchorId?: string, |
||||||
|
horizontalPosition?: 'left' | 'center' | 'right', |
||||||
|
indent?: number, |
||||||
|
tooltipText: string, |
||||||
|
verticalPosition?: 'top' | 'bottom', |
||||||
|
} |
||||||
|
|
||||||
|
export const useTooltip = () => { |
||||||
|
const [stateTooltipStyle, setTooltipStyle] = useState<CSSProperties>({}) |
||||||
|
const [stateAnchorId, setAnchorId] = useState<string | null>(null) |
||||||
|
const [stateTooltipText, setTooltipText] = useState('') |
||||||
|
|
||||||
|
const { |
||||||
|
close: hideTooltip, |
||||||
|
isOpen: isTooltipShown, |
||||||
|
open: showTooltip, |
||||||
|
} = useToggle() |
||||||
|
|
||||||
|
const onMouseOver = ({ |
||||||
|
anchorId, |
||||||
|
horizontalPosition = 'center', |
||||||
|
indent = 10, |
||||||
|
tooltipText, |
||||||
|
verticalPosition = 'bottom', |
||||||
|
}: TooltipParams) => (e: MouseEvent<HTMLElement>) => { |
||||||
|
const target = e.target as HTMLElement |
||||||
|
|
||||||
|
if (anchorId && target.id !== anchorId) return |
||||||
|
|
||||||
|
const { |
||||||
|
left, |
||||||
|
right, |
||||||
|
top, |
||||||
|
} = target.getBoundingClientRect() |
||||||
|
|
||||||
|
const coords: Partial<DOMRect> = { |
||||||
|
top: verticalPosition === 'bottom' |
||||||
|
? top + target.clientHeight + indent |
||||||
|
: top - target.clientHeight - indent, |
||||||
|
|
||||||
|
...(horizontalPosition === 'center' && { left: left + target.clientWidth / 2 }), |
||||||
|
...(horizontalPosition === 'left' && { left }), |
||||||
|
...(horizontalPosition === 'right' && { right }), |
||||||
|
} |
||||||
|
|
||||||
|
const tooltipStyle: CSSProperties = { |
||||||
|
left: !isUndefined(coords.left) ? `${coords.left}px` : 'auto', |
||||||
|
position: 'fixed', |
||||||
|
right: !isUndefined(coords.right) ? `${window.screen.width - coords.right}px` : 'auto', |
||||||
|
top: `${coords.top}px`, |
||||||
|
zIndex: 999, |
||||||
|
|
||||||
|
...(horizontalPosition === 'center' && { transform: 'translateX: (-50%)' }), |
||||||
|
...(verticalPosition === 'top' && { transform: 'translateY: (-50%)' }), |
||||||
|
...(horizontalPosition === 'center' && verticalPosition === 'top' && { transform: 'translate: (-50%, -50%)' }), |
||||||
|
} |
||||||
|
|
||||||
|
if (anchorId) { |
||||||
|
setAnchorId(anchorId) |
||||||
|
} |
||||||
|
|
||||||
|
setTooltipStyle(tooltipStyle) |
||||||
|
showTooltip() |
||||||
|
setTooltipText(tooltipText) |
||||||
|
} |
||||||
|
|
||||||
|
const onMouseLeave = () => { |
||||||
|
hideTooltip() |
||||||
|
setAnchorId(null) |
||||||
|
setTooltipStyle({}) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
anchorId: stateAnchorId, |
||||||
|
isTooltipShown, |
||||||
|
onMouseLeave, |
||||||
|
onMouseOver, |
||||||
|
tooltipStyle: stateTooltipStyle, |
||||||
|
tooltipText: stateTooltipText, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
import isUndefined from 'lodash/isUndefined' |
||||||
|
|
||||||
|
import { SportTypes, STATS_API_URL } 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, |
||||||
|
num: number | null, |
||||||
|
ord: number, |
||||||
|
weight: number | null, |
||||||
|
} |
||||||
|
|
||||||
|
type DataItem = { |
||||||
|
players: Array<Player>, |
||||||
|
team_id: number, |
||||||
|
} |
||||||
|
|
||||||
|
type Response = { |
||||||
|
data?: Array<DataItem>, |
||||||
|
error?: { |
||||||
|
code: string, |
||||||
|
message: string, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
type GetMatchParticipantsArgs = { |
||||||
|
matchId: number, |
||||||
|
period?: number, |
||||||
|
second?: number, |
||||||
|
sportType: SportTypes, |
||||||
|
} |
||||||
|
|
||||||
|
export const getMatchParticipants = async ({ |
||||||
|
matchId, |
||||||
|
period, |
||||||
|
second, |
||||||
|
sportType, |
||||||
|
}: GetMatchParticipantsArgs) => { |
||||||
|
const config = { |
||||||
|
method: 'GET', |
||||||
|
} |
||||||
|
|
||||||
|
const response: Response = await callApi({ |
||||||
|
config, |
||||||
|
url: `${STATS_API_URL}/ask/participants?sport_id=${sportType}&match_id=${matchId}${isUndefined(second) ? '' : `&second=${second}&half=${period}`}`, |
||||||
|
}) |
||||||
|
|
||||||
|
if (response.error) Promise.reject(response) |
||||||
|
|
||||||
|
return Promise.resolve(response.data || []) |
||||||
|
} |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
import isUndefined from 'lodash/isUndefined' |
||||||
|
|
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
import { STATS_API_URL } from 'config' |
||||||
|
|
||||||
|
export type PlayerParam = { |
||||||
|
clickable: boolean, |
||||||
|
data_type: string, |
||||||
|
id: number, |
||||||
|
lexic: number, |
||||||
|
lexica_short: number | null, |
||||||
|
markers: Array<number> | 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, |
||||||
|
period?: number, |
||||||
|
second?: number, |
||||||
|
sportName: string, |
||||||
|
teamId: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const getPlayersStats = async ({ |
||||||
|
matchId, |
||||||
|
period, |
||||||
|
second, |
||||||
|
sportName, |
||||||
|
teamId, |
||||||
|
}: GetPlayersStatsArgs) => { |
||||||
|
const config = { |
||||||
|
method: 'GET', |
||||||
|
} |
||||||
|
|
||||||
|
const response: Response = await callApi({ |
||||||
|
config, |
||||||
|
url: `${STATS_API_URL}/${sportName}/matches/${matchId}/teams/${teamId}/players/stats${isUndefined(second) ? '' : `?second=${second}&half=${period}`}`, |
||||||
|
}) |
||||||
|
|
||||||
|
if (response.error) Promise.reject(response) |
||||||
|
|
||||||
|
return Promise.resolve(response.data || {}) |
||||||
|
} |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
import { SportTypes, STATS_API_URL } from 'config' |
||||||
|
|
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
import { Episodes } from './getMatchPlaylists' |
||||||
|
|
||||||
|
type Response = { |
||||||
|
data?: Episodes, |
||||||
|
error?: { |
||||||
|
code: string, |
||||||
|
message: string, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
type GetStatsEventsArgs = { |
||||||
|
matchId: number, |
||||||
|
paramId: number, |
||||||
|
period?: number, |
||||||
|
playerId?: number, |
||||||
|
second?: number, |
||||||
|
sportType: SportTypes, |
||||||
|
teamId: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const getStatsEvents = async ({ |
||||||
|
matchId, |
||||||
|
paramId, |
||||||
|
period, |
||||||
|
playerId, |
||||||
|
second, |
||||||
|
sportType, |
||||||
|
teamId, |
||||||
|
}: GetStatsEventsArgs) => { |
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
half: period, |
||||||
|
match_id: matchId, |
||||||
|
match_second: second, |
||||||
|
offset_end: 6, |
||||||
|
offset_start: 6, |
||||||
|
option_id: 0, |
||||||
|
param_id: paramId, |
||||||
|
player_id: playerId, |
||||||
|
sport_id: sportType, |
||||||
|
team_id: teamId, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
const response: Response = await callApi({ |
||||||
|
config, |
||||||
|
url: `${STATS_API_URL}/video`, |
||||||
|
}) |
||||||
|
|
||||||
|
if (response.error) Promise.reject(response) |
||||||
|
|
||||||
|
return Promise.resolve(response.data || []) |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
import isUndefined from 'lodash/isUndefined' |
||||||
|
|
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
import { STATS_API_URL } from 'config' |
||||||
|
|
||||||
|
export type Param = { |
||||||
|
clickable: boolean, |
||||||
|
data_type: string, |
||||||
|
id: number, |
||||||
|
lexic: number, |
||||||
|
markers: Array<number>, |
||||||
|
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<TeamStatItem>, |
||||||
|
}, |
||||||
|
error?: string, |
||||||
|
message?: string, |
||||||
|
} |
||||||
|
|
||||||
|
type GetTeamsStatsArgs = { |
||||||
|
matchId: number, |
||||||
|
period?: number, |
||||||
|
second?: number, |
||||||
|
sportName: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const getTeamsStats = async ({ |
||||||
|
matchId, |
||||||
|
period, |
||||||
|
second, |
||||||
|
sportName, |
||||||
|
}: GetTeamsStatsArgs) => { |
||||||
|
const config = { |
||||||
|
method: 'GET', |
||||||
|
} |
||||||
|
|
||||||
|
const response: Response = await callApi({ |
||||||
|
config, |
||||||
|
url: `${STATS_API_URL}/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}&half=${period}`}`, |
||||||
|
}) |
||||||
|
|
||||||
|
if (response.error) Promise.reject(response) |
||||||
|
|
||||||
|
return Promise.resolve(response.data || {}) |
||||||
|
} |
||||||