feat(in-141): players stats #6

Merged
andrey.dekterev merged 2 commits from players_stats into in-142 3 years ago
  1. 10
      src/features/MatchPage/store/hooks/index.tsx
  2. 24
      src/features/MatchPage/store/hooks/useMatchData.tsx
  3. 159
      src/features/MatchPage/store/hooks/usePlayersStats.tsx
  4. 12
      src/features/MatchPage/store/hooks/useStatsTab.tsx
  5. 15
      src/features/MatchPage/store/hooks/useTeamsStats.tsx
  6. 7
      src/features/MatchSidePlaylists/components/PlayersTable/config.tsx
  7. 61
      src/features/MatchSidePlaylists/components/PlayersTable/hooks.tsx
  8. 58
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx
  9. 83
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx
  10. 171
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx
  11. 1812
      src/features/MatchSidePlaylists/components/PlayersTable/index.tsx
  12. 204
      src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx
  13. 8
      src/features/MatchSidePlaylists/components/PlayersTable/types.tsx
  14. 41
      src/features/MatchSidePlaylists/components/TabStats/hooks.tsx
  15. 42
      src/features/MatchSidePlaylists/components/TabStats/index.tsx
  16. 3
      src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx
  17. 21
      src/features/MatchSidePlaylists/hooks.tsx
  18. 6
      src/features/MatchSidePlaylists/index.tsx
  19. 25
      src/helpers/getTeamAbbr/index.tsx
  20. 1
      src/helpers/index.tsx
  21. 1
      src/hooks/index.tsx
  22. 65
      src/requests/getMatchParticipants.tsx
  23. 54
      src/requests/getPlayersStats.tsx
  24. 2
      src/requests/getTeamsStats.tsx
  25. 2
      src/requests/index.tsx

@ -65,10 +65,11 @@ export const useMatchPage = () => {
const { const {
events, events,
fetchTeamsStats,
handlePlaylistClick, handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists, matchPlaylists,
playingProgress, playersData,
playersStats,
selectedPlaylist, selectedPlaylist,
setFullMatchPlaylistDuration, setFullMatchPlaylistDuration,
setPlayingProgress, setPlayingProgress,
@ -162,11 +163,11 @@ export const useMatchPage = () => {
countOfFilters, countOfFilters,
disablePlayingEpisodes, disablePlayingEpisodes,
events, events,
fetchTeamsStats,
filteredEvents, filteredEvents,
handlePlaylistClick, handlePlaylistClick,
hideProfileCard, hideProfileCard,
isEmptyFilters, isEmptyFilters,
isEmptyPlayersStats,
isLiveMatch, isLiveMatch,
isOpenFiltersPopup, isOpenFiltersPopup,
isPlayFilterEpisodes, isPlayFilterEpisodes,
@ -177,7 +178,8 @@ export const useMatchPage = () => {
plaingOrder, plaingOrder,
playEpisodes, playEpisodes,
playNextEpisode, playNextEpisode,
playingProgress, playersData,
playersStats,
profile, profile,
profileCardShown, profileCardShown,
resetEvents, resetEvents,

@ -17,6 +17,8 @@ import { useMatchPopupStore } from 'features/MatchPopup'
import { useMatchPlaylists } from './useMatchPlaylists' import { useMatchPlaylists } from './useMatchPlaylists'
import { useEvents } from './useEvents' import { useEvents } from './useEvents'
import { useTeamsStats } from './useTeamsStats' import { useTeamsStats } from './useTeamsStats'
import { useStatsTab } from './useStatsTab'
import { usePlayersStats } from './usePlayersStats'
const MATCH_DATA_POLL_INTERVAL = 60000 const MATCH_DATA_POLL_INTERVAL = 60000
const MATCH_PLAYLISTS_DELAY = 5000 const MATCH_PLAYLISTS_DELAY = 5000
@ -35,12 +37,21 @@ export const useMatchData = (profile: MatchInfo) => {
setSelectedPlaylist, setSelectedPlaylist,
} = useMatchPlaylists(profile) } = useMatchPlaylists(profile)
const { events, fetchMatchEvents } = useEvents() const { events, fetchMatchEvents } = useEvents()
const { setStatsType, statsType } = useStatsTab()
const { teamsStats } = useTeamsStats({
matchProfile: profile,
playingProgress,
statsType,
})
const { const {
fetchTeamsStats, isEmptyPlayersStats,
setStatsType, playersData,
playersStats,
} = usePlayersStats({
matchProfile: profile,
playingProgress,
statsType, statsType,
teamsStats, })
} = useTeamsStats(profile, playingProgress)
const fetchPlaylistsDebounced = useMemo( const fetchPlaylistsDebounced = useMemo(
() => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY), () => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY),
@ -100,10 +111,11 @@ export const useMatchData = (profile: MatchInfo) => {
return { return {
events, events,
fetchTeamsStats,
handlePlaylistClick, handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists, matchPlaylists,
playingProgress, playersData,
playersStats,
selectedPlaylist, selectedPlaylist,
setFullMatchPlaylistDuration, setFullMatchPlaylistDuration,
setPlayingProgress, setPlayingProgress,

@ -0,0 +1,159 @@
import {
useMemo,
useEffect,
useState,
} from 'react'
import throttle from 'lodash/throttle'
import isEmpty from 'lodash/isEmpty'
import every from 'lodash/every'
import find from 'lodash/find'
import type {
MatchInfo,
PlayersStats,
Player,
} from 'requests'
import { getPlayersStats, getMatchParticipants } from 'requests'
import { useObjectState, usePageParams } from 'hooks'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000
type UsePlayersStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
statsType: StatsType,
}
type PlayersData = {
team1: Array<Player>,
team2: Array<Player>,
}
export const usePlayersStats = ({
matchProfile,
playingProgress,
statsType,
}: UsePlayersStatsArgs) => {
const [playersStats, setPlayersStats] = useObjectState<Record<string, PlayersStats>>({})
const [playersData, setPlayersData] = useState<PlayersData>({ team1: [], team2: [] })
const {
profileId: matchId,
sportName,
sportType,
} = usePageParams()
const isCurrentStats = statsType === StatsType.CURRENT_STATS
const progressSec = Math.floor(playingProgress / 1000)
const isEmptyPlayersStats = (teamId: number) => (
isEmpty(playersStats[teamId])
|| every(playersStats[teamId], isEmpty)
|| isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2'])
)
const fetchPlayers = useMemo(() => throttle((second?: number) => {
if (!matchProfile?.team1.id || !matchProfile?.team1.id) return
try {
getMatchParticipants({
matchId,
second,
sportType,
}).then((data) => {
const team1Players = find(data, { team_id: matchProfile.team1.id })?.players || []
const team2Players = find(data, { team_id: matchProfile.team2.id })?.players || []
setPlayersData({
team1: team1Players,
team2: team2Players,
})
})
// eslint-disable-next-line no-empty
} catch (e) {}
}, REQUEST_DELAY), [
matchId,
matchProfile?.team1.id,
matchProfile?.team2.id,
sportType,
])
const fetchPlayersStats = useMemo(() => throttle((second?: number) => {
if (!sportName || !matchProfile?.team1.id || !matchProfile?.team2.id) return
try {
getPlayersStats({
matchId,
second,
sportName,
teamId: matchProfile.team1.id,
}).then((data) => setPlayersStats({ [matchProfile.team1.id]: data }))
getPlayersStats({
matchId,
second,
sportName,
teamId: matchProfile.team2.id,
}).then((data) => setPlayersStats({ [matchProfile?.team2.id]: data }))
// eslint-disable-next-line no-empty
} catch (e) {}
}, REQUEST_DELAY), [
matchId,
matchProfile?.team1.id,
matchProfile?.team2.id,
setPlayersStats,
sportName,
])
useEffect(() => {
let interval: NodeJS.Timeout
fetchPlayers()
if (!isCurrentStats) {
fetchPlayersStats()
}
if (matchProfile?.live) {
interval = setInterval(() => {
if (isCurrentStats) return
fetchPlayersStats()
fetchPlayers()
}, STATS_POLL_INTERVAL)
}
return () => clearInterval(interval)
}, [
fetchPlayersStats,
fetchPlayers,
isCurrentStats,
matchProfile?.live,
])
useEffect(() => {
if (isCurrentStats) {
fetchPlayersStats(progressSec)
fetchPlayers(progressSec)
}
}, [
fetchPlayersStats,
fetchPlayers,
progressSec,
isCurrentStats,
matchProfile?.live,
])
return {
isEmptyPlayersStats,
playersData,
playersStats,
}
}

@ -0,0 +1,12 @@
import { useState } from 'react'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
export const useStatsTab = () => {
const [statsType, setStatsType] = useState<StatsType>(StatsType.FINAL_STATS)
return {
setStatsType,
statsType,
}
}

@ -16,8 +16,17 @@ import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/confi
const REQUEST_DELAY = 3000 const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000 const STATS_POLL_INTERVAL = 30000
export const useTeamsStats = (matchProfile: MatchInfo, playingProgress: number) => { type UseTeamsStatsArgs = {
const [statsType, setStatsType] = useState<StatsType>(StatsType.FINAL_STATS) matchProfile: MatchInfo,
playingProgress: number,
statsType: StatsType,
}
export const useTeamsStats = ({
matchProfile,
playingProgress,
statsType,
}: UseTeamsStatsArgs) => {
const [teamsStats, setTeamsStats] = useState<{ const [teamsStats, setTeamsStats] = useState<{
[teamId: string]: Array<TeamStatItem>, [teamId: string]: Array<TeamStatItem>,
}>({}) }>({})
@ -63,8 +72,6 @@ export const useTeamsStats = (matchProfile: MatchInfo, playingProgress: number)
}, [fetchTeamsStats, progressSec, isCurrentStats]) }, [fetchTeamsStats, progressSec, isCurrentStats])
return { return {
fetchTeamsStats,
setStatsType,
statsType, statsType,
teamsStats, teamsStats,
} }

@ -1 +1,6 @@
export const CELL_WIDTH = 47 export const PARAM_COLUMN_WIDTH = 50
export const REQUEST_DELAY = 3000
export const STATS_POLL_INTERVAL = 30000
export const DISPLAYED_PARAMS_COLUMNS = 4
export const FIRST_COLUMN_WIDTH_DEFAULT = 100
export const SCROLLBAR_WIDTH = 8

@ -1,61 +0,0 @@
import type { SyntheticEvent } from 'react'
import {
useRef,
useState,
useEffect,
} from 'react'
import { isMobileDevice } from 'config/userAgent'
import { CELL_WIDTH } from './config'
export const usePlayersTable = () => {
const containerRef = useRef<HTMLDivElement>(null)
const tableWrapperRef = useRef<HTMLDivElement>(null)
const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRightArrow] = useState(false)
const cellWidth = isMobileDevice
? ((containerRef.current?.clientWidth || 0) - 98) / 4
: CELL_WIDTH
const slideLeft = () => tableWrapperRef.current!.scrollBy(-cellWidth, 0)
const slideRight = () => tableWrapperRef.current!.scrollBy(cellWidth, 0)
const handleScroll = (e: SyntheticEvent<HTMLDivElement>) => {
const {
clientWidth,
scrollLeft,
scrollWidth,
} = e.currentTarget
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowLeftArrow(scrollLeft > 1)
setShowRightArrow(scrollRight > 1)
}
useEffect(() => {
const {
clientWidth = 0,
scrollLeft = 0,
scrollWidth = 0,
} = tableWrapperRef.current || {}
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowRightArrow(scrollRight > 1)
}, [])
return {
cellWidth,
containerRef,
handleScroll,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
tableWrapperRef,
}
}

@ -0,0 +1,58 @@
import { useState } from 'react'
import type { SortCondition, PlayersTableProps } from '../types'
import { usePlayers } from './usePlayers'
import { useTable } from './useTable'
export const usePlayersTable = ({ teamId }: PlayersTableProps) => {
const [sortCondition, setSortCondition] = useState<SortCondition>({ dir: 'asc', paramId: null })
const {
getFullName,
getPlayerParams,
players,
} = usePlayers({ sortCondition, teamId })
const {
containerRef,
firstColumnWidth,
getDisplayedValue,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
showExpandButton,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
tableWrapperRef,
toggleIsExpanded,
} = useTable({
setSortCondition,
teamId,
})
return {
containerRef,
firstColumnWidth,
getDisplayedValue,
getFullName,
getPlayerParams,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
players,
showExpandButton,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
sortCondition,
tableWrapperRef,
toggleIsExpanded,
}
}

@ -0,0 +1,83 @@
import { useMemo, useCallback } from 'react'
import orderBy from 'lodash/orderBy'
import isNil from 'lodash/isNil'
import trim from 'lodash/trim'
import type { Player, PlayerParam } from 'requests'
import { useToggle } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import type { SortCondition } from '../types'
type UsePlayersArgs = {
sortCondition: SortCondition,
teamId: number,
}
export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => {
const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle()
const {
playersData,
playersStats,
profile: matchProfile,
} = useMatchPageStore()
const { suffix } = useLexicsStore()
const getPlayerParams = useCallback(
(playerId: number) => playersStats[teamId][playerId] || {},
[playersStats, teamId],
)
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : val)
const getFullName = useCallback((player: Player) => (
trim(`${player[`firstname_${suffix}`]} ${player[`lastname_${suffix}`]}`)
), [suffix])
const getParamValue = useCallback((playerId: number, paramId: number) => {
const playerParams = getPlayerParams(playerId)
const { val } = playerParams[paramId] || {}
return val
}, [getPlayerParams])
const sortedPlayers = useMemo(() => {
const players = playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2']
return isNil(sortCondition.paramId)
? orderBy(players, getFullName)
: orderBy(
players,
[
(player) => {
const paramValue = getParamValue(player.id, sortCondition.paramId!)
return isNil(paramValue) ? -1 : paramValue
},
getFullName,
],
sortCondition.dir,
)
}, [
getFullName,
getParamValue,
playersData,
matchProfile?.team1.id,
sortCondition.dir,
sortCondition.paramId,
teamId,
])
return {
getDisplayedValue,
getFullName,
getPlayerParams,
isExpanded,
players: sortedPlayers,
toggleIsExpanded,
}
}

@ -0,0 +1,171 @@
import type {
SyntheticEvent,
Dispatch,
SetStateAction,
} from 'react'
import {
useRef,
useState,
useEffect,
useMemo,
} from 'react'
import size from 'lodash/size'
import isNil from 'lodash/isNil'
import reduce from 'lodash/reduce'
import forEach from 'lodash/forEach'
import values from 'lodash/values'
import round from 'lodash/round'
import map from 'lodash/map'
import { isMobileDevice } from 'config'
import type { PlayerParam, PlayersStats } from 'requests'
import { useToggle } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsConfig } from 'features/LexicsStore'
import type { SortCondition } from '../types'
import {
PARAM_COLUMN_WIDTH,
DISPLAYED_PARAMS_COLUMNS,
FIRST_COLUMN_WIDTH_DEFAULT,
SCROLLBAR_WIDTH,
} from '../config'
type UseTableArgs = {
setSortCondition: Dispatch<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(false)
const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle()
const { playersStats } = useMatchPageStore()
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 = () => {
const rest = (
(containerRef.current?.clientWidth || 0) - FIRST_COLUMN_WIDTH_DEFAULT - SCROLLBAR_WIDTH
)
const desktopWith = PARAM_COLUMN_WIDTH
const mobileWidth = paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : rest / DISPLAYED_PARAMS_COLUMNS
return isMobileDevice ? mobileWidth : desktopWith
}
const getFirstColumnWidth = () => {
if (isExpanded) return 0
return paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : FIRST_COLUMN_WIDTH_DEFAULT
}
const paramColumnWidth = getParamColumnWidth()
const firstColumnWidth = getFirstColumnWidth()
const slideLeft = () => tableWrapperRef.current?.scrollBy(-paramColumnWidth, 0)
const slideRight = () => tableWrapperRef.current?.scrollBy(paramColumnWidth, 0)
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : round(val, 2))
const handleScroll = (e: SyntheticEvent<HTMLDivElement>) => {
const {
clientWidth,
scrollLeft,
scrollWidth,
} = e.currentTarget
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowLeftArrow(scrollLeft > 0)
setShowRightArrow(scrollRight > 0)
}
const handleSortClick = (paramId: number) => () => {
setSortCondition((curr) => ({
dir: curr.dir === 'asc' || curr.paramId !== paramId ? 'desc' : 'asc',
paramId,
}))
}
useEffect(() => {
const {
clientWidth = 0,
scrollLeft = 0,
scrollWidth = 0,
} = tableWrapperRef.current || {}
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowRightArrow(scrollRight > 0)
}, [isExpanded])
return {
containerRef,
firstColumnWidth,
getDisplayedValue,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
showExpandButton: !isMobileDevice && paramsCount > DISPLAYED_PARAMS_COLUMNS,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
tableWrapperRef,
toggleIsExpanded,
}
}

@ -1,109 +1,133 @@
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config'
import { customScrollbar } from 'features/Common' import { customScrollbar } from 'features/Common'
import { TooltipWrapper } from 'features/Tooltip'
import { import {
ArrowButton as ArrowButtonBase, ArrowButton as ArrowButtonBase,
Arrow as ArrowBase, Arrow as ArrowBase,
} from 'features/HeaderFilters/components/DateFilter/styled' } from 'features/HeaderFilters/components/DateFilter/styled'
import { T9n } from 'features/T9n'
import { CELL_WIDTH } from './config' type ContainerProps = {
isExpanded?: boolean,
}
export const Container = styled.div` export const Container = styled.div<ContainerProps>`
${({ isExpanded }) => (isExpanded
? ''
: css`
position: relative; position: relative;
`)}
` `
export const TableWrapper = styled.div` type TableWrapperProps = {
isExpanded?: boolean,
}
export const TableWrapper = styled.div<TableWrapperProps>`
display: flex;
max-width: 100%;
max-height: calc(100vh - 235px); max-height: calc(100vh - 235px);
border-radius: 5px; border-radius: 5px;
overflow-x: auto; overflow-x: auto;
scroll-behavior: smooth; scroll-behavior: smooth;
background-color: #333333;
z-index: 50;
${customScrollbar} ${customScrollbar}
${({ isExpanded }) => (isExpanded
? css`
position: absolute;
right: 14px;
`
: '')}
` `
export const Table = styled.table` export const Table = styled.div`
width: 100%; flex-grow: 1;
border-radius: 5px; border-radius: 5px;
border-collapse: collapse; border-collapse: collapse;
letter-spacing: -0.078px; letter-spacing: -0.078px;
table-layout: fixed;
` `
export const Thead = styled.thead` export const Tooltip = styled(TooltipWrapper)`
height: 45px; left: auto;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); padding: 2px 10px;
` border-radius: 6px;
transform: none;
type ThProps = {
sorted?: boolean,
width?: number,
}
export const Th = styled.th.attrs({ tabIndex: 0 })<ThProps>`
position: sticky;
left: 0;
top: 0;
width: ${({ width }) => width || CELL_WIDTH}px;
font-size: 11px; font-size: 11px;
color: ${({ theme }) => theme.colors.white}; line-height: 1;
text-transform: uppercase; color: ${({ theme }) => theme.colors.black};
cursor: pointer;
background-color: #333333;
z-index: 2;
:first-child { ::before {
width: 90px; display: none;
z-index: 3;
cursor: default;
} }
${({ sorted }) => (sorted
? ''
: css`
${ParamShortTitle} {
opacity: 0.5;
}
`)}
` `
export const ParamShortTitle = styled.span`` export const ParamShortTitle = styled(T9n)`
text-transform: uppercase;
export const Tbody = styled.tbody`` `
export const Tr = styled.tr` export const Row = styled.div`
display: flex;
width: 100%;
height: 45px; height: 45px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
:last-child { :first-child {
border-bottom: none; position: sticky;
left: 0;
top: 0;
z-index: 1;
} }
` `
type TdProps = { type TdProps = {
clickable?: boolean, clickable?: boolean,
columnWidth?: number,
headerCell?: boolean,
sorted?: boolean, sorted?: boolean,
} }
export const Td = styled.td.attrs(({ clickable }: TdProps) => ({ export const Cell = styled.div.attrs(({ clickable }: TdProps) => ({
...clickable && { tabIndex: 0 }, ...clickable && { tabIndex: 0 },
}))<TdProps>` }))<TdProps>`
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
font-size: 11px; font-size: 11px;
text-align: center; color: ${({
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; clickable,
text-overflow: ellipsis; headerCell,
overflow: hidden; theme,
}) => (clickable && !headerCell ? '#5EB2FF' : theme.colors.white)};
white-space: nowrap; white-space: nowrap;
background-color: #333333; background-color: #333333;
${Tooltip} {
top: 35px;
}
:hover {
${Tooltip} {
display: block;
}
}
${({ headerCell }) => (headerCell
? ''
: css`
:first-child { :first-child {
position: sticky; justify-content: unset;
left: 0;
z-index: 1;
padding-left: 13px; padding-left: 13px;
text-align: left;
color: ${({ theme }) => theme.colors.white}; color: ${({ theme }) => theme.colors.white};
} }
`)}
${({ sorted }) => (sorted ${({ sorted }) => (sorted
? css` ? css`
@ -111,21 +135,70 @@ export const Td = styled.td.attrs(({ clickable }: TdProps) => ({
` `
: '')} : '')}
${({ clickable }) => (clickable ${({ clickable, headerCell }) => (clickable || headerCell
? css` ? css`
cursor: pointer; cursor: pointer;
` `
: '')} : '')}
` `
type FirstColumnProps = {
columnWidth?: number,
}
export const FirstColumn = styled.div<FirstColumnProps>`
position: sticky;
left: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
`
export const PlayerNum = styled.span` export const PlayerNum = styled.span`
display: inline-block;
width: 20px;
flex-shrink: 0;
text-align: center;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
` `
type PlayerNameProps = {
columnWidth?: number,
}
export const PlayerName = styled.span<PlayerNameProps>`
display: inline-block;
margin-top: 2px;
text-overflow: ellipsis;
overflow: hidden;
${({ columnWidth }) => (columnWidth
? css`
max-width: calc(${columnWidth}px - 31px);
`
: css`
max-width: 110px;
`)}
`
export const PlayerNameWrapper = styled.span`
position: relative;
${Tooltip} {
top: 15px;
}
:hover {
${Tooltip} {
display: block;
}
}
`
const ArrowButton = styled(ArrowButtonBase)` const ArrowButton = styled(ArrowButtonBase)`
position: absolute; position: absolute;
width: 17px;
margin-top: 2px; margin-top: 2px;
z-index: 4; background-color: #333333;
z-index: 3;
${isMobileDevice ${isMobileDevice
? css` ? css`
@ -146,4 +219,23 @@ export const ArrowButtonLeft = styled(ArrowButton)`
export const Arrow = styled(ArrowBase)` export const Arrow = styled(ArrowBase)`
width: 10px; width: 10px;
height: 10px; height: 10px;
${isMobileDevice
? css`
border-color: ${({ theme }) => theme.colors.white};
`
: ''};
`
export const ExpandButton = styled(ArrowButton)`
left: 20px;
top: 0;
${Arrow} {
left: 0;
:last-child {
margin-left: 7px;
}
}
` `

@ -0,0 +1,8 @@
export type PlayersTableProps = {
teamId: number,
}
export type SortCondition = {
dir: 'asc' | 'desc',
paramId: number | null,
}

@ -1,4 +1,6 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import isEmpty from 'lodash/isEmpty'
import { useMatchPageStore } from 'features/MatchPage/store' import { useMatchPageStore } from 'features/MatchPage/store'
@ -7,21 +9,56 @@ import { StatsType, Tabs } from './config'
export const useTabStats = () => { export const useTabStats = () => {
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS) const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS)
const { setStatsType, statsType } = useMatchPageStore() const {
isEmptyPlayersStats,
profile: matchProfile,
setStatsType,
statsType,
teamsStats,
} = useMatchPageStore()
const isFinalStatsType = statsType === StatsType.FINAL_STATS const isFinalStatsType = statsType === StatsType.FINAL_STATS
const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats' const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats'
const tooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video' const tooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video'
const isVisibleTeamsTab = !isEmpty(teamsStats)
const isVisibleTeam1PlayersTab = Boolean(
matchProfile && !isEmptyPlayersStats(matchProfile.team1.id),
)
const isVisibleTeam2PlayersTab = Boolean(
matchProfile && !isEmptyPlayersStats(matchProfile.team2.id),
)
const toggleStatsType = () => { const toggleStatsType = () => {
const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS
setStatsType(newStatsType) setStatsType(newStatsType)
} }
useEffect(() => {
switch (true) {
case isVisibleTeamsTab:
setSelectedTab(Tabs.TEAMS)
break
case isVisibleTeam1PlayersTab:
setSelectedTab(Tabs.TEAM1)
break
case isVisibleTeam2PlayersTab:
setSelectedTab(Tabs.TEAM2)
break
default:
}
}, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab])
return { return {
isFinalStatsType, isFinalStatsType,
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
selectedTab, selectedTab,
setSelectedTab, setSelectedTab,
switchTitleLexic, switchTitleLexic,

@ -1,7 +1,11 @@
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'
import { getTeamAbbr } from 'helpers'
import { Tooltip } from 'features/Tooltip' import { Tooltip } from 'features/Tooltip'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Name } from 'features/Name'
import { Tabs } from './config' import { Tabs } from './config'
import { useTabStats } from './hooks' import { useTabStats } from './hooks'
@ -27,37 +31,59 @@ const tabPanes = {
export const TabStats = () => { export const TabStats = () => {
const { const {
isFinalStatsType, isFinalStatsType,
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
selectedTab, selectedTab,
setSelectedTab, setSelectedTab,
switchTitleLexic, switchTitleLexic,
toggleStatsType, toggleStatsType,
tooltipLexic, tooltipLexic,
} = useTabStats() } = useTabStats()
const { profile: matchProfile } = useMatchPageStore()
const TabPane = tabPanes[selectedTab] const TabPane = tabPanes[selectedTab]
if (!matchProfile) return null
const { team1, team2 } = matchProfile
return ( return (
<Container> <Container>
<Header> <Header>
<TabList> <TabList>
{isVisibleTeamsTab && (
<Tab <Tab
aria-pressed={selectedTab === Tabs.TEAMS} aria-pressed={selectedTab === Tabs.TEAMS}
onClick={() => setSelectedTab(Tabs.TEAMS)} onClick={() => setSelectedTab(Tabs.TEAMS)}
> >
<T9n t='team' /> <T9n t='team' />
</Tab> </Tab>
{/* <Tab )}
{isVisibleTeam1PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM1} aria-pressed={selectedTab === Tabs.TEAM1}
onClick={() => setSelectedTab(Tabs.TEAM1)} onClick={() => setSelectedTab(Tabs.TEAM1)}
> >
DIN <Name nameObj={{
</Tab> */} name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng),
{/* <Tab name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus),
}}
/>
</Tab>
)}
{isVisibleTeam2PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM2} aria-pressed={selectedTab === Tabs.TEAM2}
onClick={() => setSelectedTab(Tabs.TEAM2)} onClick={() => setSelectedTab(Tabs.TEAM2)}
> >
SPA <Name nameObj={{
</Tab> */} name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng),
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus),
}}
/>
</Tab>
)}
</TabList> </TabList>
<Switch> <Switch>
<SwitchTitle t={switchTitleLexic} /> <SwitchTitle t={switchTitleLexic} />
@ -69,7 +95,9 @@ export const TabStats = () => {
</SwitchButton> </SwitchButton>
</Switch> </Switch>
</Header> </Header>
<TabPane /> <TabPane
teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id}
/>
</Container> </Container>
) )
} }

@ -1,5 +1,6 @@
import isNumber from 'lodash/isNumber' import isNumber from 'lodash/isNumber'
import find from 'lodash/find' import find from 'lodash/find'
import round from 'lodash/round'
import type { Param } from 'requests' import type { Param } from 'requests'
@ -9,7 +10,7 @@ export const useTeamsStatsTable = () => {
const { profile, teamsStats } = useMatchPageStore() const { profile, teamsStats } = useMatchPageStore()
const getDisplayedValue = (val: any) => ( const getDisplayedValue = (val: any) => (
isNumber(val) ? val : '-' isNumber(val) ? round(val, 2) : '-'
) )
const getStatItemById = (paramId: number) => { const getStatItemById = (paramId: number) => {

@ -16,7 +16,9 @@ export const useMatchSidePlaylists = () => {
const { const {
closePopup, closePopup,
events, events,
isEmptyPlayersStats,
matchPlaylists: playlists, matchPlaylists: playlists,
profile: matchProfile,
teamsStats, teamsStats,
tournamentData, tournamentData,
} = useMatchPageStore() } = useMatchPageStore()
@ -46,13 +48,20 @@ export const useMatchSidePlaylists = () => {
const isStatsTabVisible = useMemo(() => ( const isStatsTabVisible = useMemo(() => (
!isEmpty(teamsStats) !isEmpty(teamsStats)
), [teamsStats]) || (matchProfile?.team1.id && !isEmptyPlayersStats(matchProfile.team1.id))
|| (matchProfile?.team2.id && !isEmptyPlayersStats(matchProfile.team2.id))
), [
isEmptyPlayersStats,
matchProfile?.team1.id,
matchProfile?.team2.id,
teamsStats,
])
const hasLessThanFourTabs = compact([ const hasLessThanFourTabs = compact([
isWatchTabVisible, isWatchTabVisible,
isEventTabVisible, isEventTabVisible,
isPlayersTabVisible, isPlayersTabVisible,
isStatsTabVisible, // isStatsTabVisible,
]).length < 4 ]).length < 4
useEffect(() => { useEffect(() => {
@ -66,14 +75,14 @@ export const useMatchSidePlaylists = () => {
case isPlayersTabVisible: case isPlayersTabVisible:
setSelectedTab(Tabs.PLAYERS) setSelectedTab(Tabs.PLAYERS)
break break
case isStatsTabVisible: // case isStatsTabVisible:
setSelectedTab(Tabs.STATS) // setSelectedTab(Tabs.STATS)
break // break
} }
}, [ }, [
isEventTabVisible, isEventTabVisible,
isPlayersTabVisible, isPlayersTabVisible,
isStatsTabVisible, // isStatsTabVisible,
isWatchTabVisible, isWatchTabVisible,
]) ])

@ -60,7 +60,7 @@ export const MatchSidePlaylists = ({
hasLessThanFourTabs, hasLessThanFourTabs,
isEventTabVisible, isEventTabVisible,
isPlayersTabVisible, isPlayersTabVisible,
isStatsTabVisible, // isStatsTabVisible,
isWatchTabVisible, isWatchTabVisible,
onTabClick, onTabClick,
playListFilter, playListFilter,
@ -134,7 +134,7 @@ export const MatchSidePlaylists = ({
<TabTitle t='players' /> <TabTitle t='players' />
</Tab> </Tab>
) : null} ) : null}
{isStatsTabVisible ? ( {/* {isStatsTabVisible ? (
<Tab <Tab
aria-pressed={selectedTab === Tabs.STATS} aria-pressed={selectedTab === Tabs.STATS}
onClick={() => onTabClick(Tabs.STATS)} onClick={() => onTabClick(Tabs.STATS)}
@ -142,7 +142,7 @@ export const MatchSidePlaylists = ({
<TabIcon icon='stats' /> <TabIcon icon='stats' />
<TabTitle t='stats' /> <TabTitle t='stats' />
</Tab> </Tab>
) : null} ) : null} */}
</TabsGroup> </TabsGroup>
</TabsWrapper> </TabsWrapper>

@ -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])
}

@ -8,3 +8,4 @@ export * from './secondsToHms'
export * from './redirectToUrl' export * from './redirectToUrl'
export * from './getRandomString' export * from './getRandomString'
export * from './selectedApi' export * from './selectedApi'
export * from './getTeamAbbr'

@ -4,3 +4,4 @@ export * from './useStorage'
export * from './useInterval' export * from './useInterval'
export * from './useEventListener' export * from './useEventListener'
export * from './useObjectState' export * from './useObjectState'
export * from './usePageParams'

@ -0,0 +1,65 @@
import isUndefined from 'lodash/isUndefined'
import { SportTypes } from 'config'
import { callApi } from 'helpers'
export type Player = {
birthday: string | null,
c_country: number,
c_gender: number,
club_f_team: number,
club_shirt_num: number,
firstname_eng: string,
firstname_national: string | null,
firstname_rus: string,
height: number | null,
id: number,
is_gk: boolean,
lastname_eng: string,
lastname_national: string | null,
lastname_rus: string,
national_f_team: number | null,
national_shirt_num: number,
nickname_eng: string | null,
nickname_rus: string | null,
weight: number | null,
}
type DataItem = {
players: Array<Player>,
team_id: number,
}
type Response = {
data?: Array<DataItem>,
error?: {
code: string,
message: string,
},
}
type GetMatchParticipantsArgs = {
matchId: number,
second?: number,
sportType: SportTypes,
}
export const getMatchParticipants = async ({
matchId,
second,
sportType,
}: GetMatchParticipantsArgs) => {
const config = {
method: 'GET',
}
const response: Response = await callApi({
config,
url: `http://136.243.17.103:8888/ask/participants?sport_id=${sportType}&match_id=${matchId}${isUndefined(second) ? '' : `&second=${second}`}`,
})
if (response.error) Promise.reject(response)
return Promise.resolve(response.data || [])
}

@ -0,0 +1,54 @@
import isUndefined from 'lodash/isUndefined'
import { callApi } from 'helpers'
export type PlayerParam = {
clickable: boolean,
data_type: string,
id: number,
lexic: number,
lexica_short: number | null,
markers: Array<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,
second?: number,
sportName: string,
teamId: number,
}
export const getPlayersStats = async ({
matchId,
second,
sportName,
teamId,
}: GetPlayersStatsArgs) => {
const config = {
method: 'GET',
}
const response: Response = await callApi({
config,
url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/${teamId}/players/stats${isUndefined(second) ? '' : `?second=${second}`}`,
})
if (response.error) Promise.reject(response)
return Promise.resolve(response.data || {})
}

@ -47,7 +47,7 @@ export const getTeamsStats = async ({
const response: Response = await callApi({ const response: Response = await callApi({
config, config,
url: `https://statistic.insports.tv/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}`}`, url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}`}`,
}) })
if (response.error) Promise.reject(response) if (response.error) Promise.reject(response)

@ -25,3 +25,5 @@ export * from './getPlayerPlaylists'
export * from './getSubscriptions' export * from './getSubscriptions'
export * from './buySubscription' export * from './buySubscription'
export * from './getTeamsStats' export * from './getTeamsStats'
export * from './getPlayersStats'
export * from './getMatchParticipants'

Loading…
Cancel
Save