|
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 { |
||||
WATCH, |
||||
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 || {}) |
||||
} |
||||