feat(in-141): players stats #22

Merged
andrey.dekterev merged 1 commits from in-141 into in-142 3 years ago
  1. 4
      src/features/LexicsStore/helpers/index.tsx
  2. 2
      src/features/LexicsStore/hooks/index.tsx
  3. 8
      src/features/MatchPage/store/hooks/index.tsx
  4. 19
      src/features/MatchPage/store/hooks/useMatchData.tsx
  5. 99
      src/features/MatchPage/store/hooks/usePlayersStats.tsx
  6. 18
      src/features/MatchPage/store/hooks/useStatsTab.tsx
  7. 34
      src/features/MatchPage/store/hooks/useTeamsStats.tsx
  8. 16
      src/features/MatchSidePlaylists/components/CircleAnimationBar/index.tsx
  9. 65
      src/features/MatchSidePlaylists/components/PlayersTable/Cell.tsx
  10. 7
      src/features/MatchSidePlaylists/components/PlayersTable/config.tsx
  11. 12
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx
  12. 87
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx
  13. 194
      src/features/MatchSidePlaylists/components/PlayersTable/index.tsx
  14. 247
      src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx
  15. 160
      src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx
  16. 6
      src/features/MatchSidePlaylists/index.tsx
  17. 31
      src/features/MatchSidePlaylists/styled.tsx
  18. 5
      src/hooks/useModalRoot.tsx
  19. 88
      src/hooks/useTooltip.tsx
  20. 4
      src/requests/getTeamsStats.tsx

@ -12,6 +12,10 @@ export const getSuffix = (lang: string) => (
lang === 'ru' ? 'rus' : 'eng'
)
export const getShortSuffix = (lang: string) => (
lang === 'ru' ? 'ru' : 'en'
)
export const getLexicIds = (ids: Array<LexicsId> | LexicsConfig) => (
uniq(map(ids, (id) => Number(id)))
)

@ -8,6 +8,7 @@ import {
getLexicIds,
mapTranslationsToLocalKeys,
getSuffix,
getShortSuffix,
} from 'features/LexicsStore/helpers'
import { useLang } from './useLang'
@ -57,6 +58,7 @@ export const useLexics = (initialLanguage?: string) => {
changeLang,
lang,
languageList,
shortSuffix: getShortSuffix(lang),
suffix: getSuffix(lang),
translate,
} as const

@ -79,15 +79,17 @@ export const useMatchPage = () => {
events,
handlePlaylistClick,
isEmptyPlayersStats,
isPlayersStatsFetching,
isTeamsStatsFetching,
matchPlaylists,
playersData,
playersStats,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
setStatsType,
statsType,
teamsStats,
toggleStatsType,
} = useMatchData(matchProfile)
const profile = matchProfile
@ -183,7 +185,9 @@ export const useMatchPage = () => {
isLiveMatch,
isOpenFiltersPopup,
isPlayFilterEpisodes,
isPlayersStatsFetching,
isStarted,
isTeamsStatsFetching,
likeImage,
likeToggle,
matchPlaylists,
@ -203,7 +207,6 @@ export const useMatchPage = () => {
setPlaingOrder,
setPlayingProgress,
setReversed,
setStatsType,
setUnreversed,
setWatchAllEpisodesTimer,
showProfileCard,
@ -212,6 +215,7 @@ export const useMatchPage = () => {
toggleActiveEvents,
toggleActivePlayers,
togglePopup,
toggleStatsType,
tournamentData,
watchAllEpisodesTimer,
}

@ -36,13 +36,25 @@ export const useMatchData = (profile: MatchInfo) => {
setFullMatchPlaylistDuration,
setSelectedPlaylist,
} = useMatchPlaylists(profile)
const { events, fetchMatchEvents } = useEvents()
const { setStatsType, statsType } = useStatsTab()
const {
isPlayersStatsFetching,
isTeamsStatsFetching,
setIsPlayersStatsFetching,
setIsTeamsStatsFetching,
statsType,
toggleStatsType,
} = useStatsTab()
const { teamsStats } = useTeamsStats({
matchProfile: profile,
playingProgress,
setIsTeamsStatsFetching,
statsType,
})
const {
isEmptyPlayersStats,
playersData,
@ -50,6 +62,7 @@ export const useMatchData = (profile: MatchInfo) => {
} = usePlayersStats({
matchProfile: profile,
playingProgress,
setIsPlayersStatsFetching,
statsType,
})
@ -113,14 +126,16 @@ export const useMatchData = (profile: MatchInfo) => {
events,
handlePlaylistClick,
isEmptyPlayersStats,
isPlayersStatsFetching,
isTeamsStatsFetching,
matchPlaylists,
playersData,
playersStats,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
setStatsType,
statsType,
teamsStats,
toggleStatsType,
}
}

@ -1,3 +1,4 @@
import type { Dispatch, SetStateAction } from 'react'
import {
useMemo,
useEffect,
@ -26,6 +27,7 @@ const STATS_POLL_INTERVAL = 30000
type UsePlayersStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
setIsPlayersStatsFetching: Dispatch<SetStateAction<boolean>>,
statsType: StatsType,
}
@ -37,6 +39,7 @@ type PlayersData = {
export const usePlayersStats = ({
matchProfile,
playingProgress,
setIsPlayersStatsFetching,
statsType,
}: UsePlayersStatsArgs) => {
const [playersStats, setPlayersStats] = useObjectState<Record<string, PlayersStats>>({})
@ -58,26 +61,18 @@ export const usePlayersStats = ({
|| isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2'])
)
const fetchPlayers = useMemo(() => throttle((second?: number) => {
if (!matchProfile?.team1.id || !matchProfile?.team1.id) return
const fetchPlayers = useMemo(() => throttle(async (second?: number) => {
if (!matchProfile?.team1.id || !matchProfile?.team2.id) return null
try {
getMatchParticipants({
return 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) {}
} catch (e) {
return Promise.reject(e)
}
}, REQUEST_DELAY), [
matchId,
matchProfile?.team1.id,
@ -85,67 +80,83 @@ export const usePlayersStats = ({
sportType,
])
const fetchPlayersStats = useMemo(() => throttle((second?: number) => {
if (!sportName || !matchProfile?.team1.id || !matchProfile?.team2.id) return
const fetchPlayersStats = useMemo(() => (async (team: 'team1' | 'team2', second?: number) => {
if (!sportName || !matchProfile?.[team].id) return null
try {
getPlayersStats({
return 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), [
teamId: matchProfile[team].id,
})
} 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) => {
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), [
fetchPlayers,
fetchPlayersStats,
setPlayersStats,
sportName,
matchProfile?.team1.id,
matchProfile?.team2.id,
setIsPlayersStatsFetching,
])
useEffect(() => {
let interval: NodeJS.Timeout
fetchPlayers()
if (!isCurrentStats) {
fetchPlayersStats()
fetchData()
}
if (matchProfile?.live) {
if (matchProfile?.live && !isCurrentStats) {
interval = setInterval(() => {
if (isCurrentStats) return
fetchPlayersStats()
fetchPlayers()
fetchData()
}, STATS_POLL_INTERVAL)
}
return () => clearInterval(interval)
}, [
fetchPlayersStats,
fetchPlayers,
fetchData,
isCurrentStats,
matchProfile?.live,
])
useEffect(() => {
if (isCurrentStats) {
fetchPlayersStats(progressSec)
fetchPlayers(progressSec)
fetchData(progressSec)
}
}, [
fetchPlayersStats,
fetchPlayers,
fetchData,
progressSec,
isCurrentStats,
matchProfile?.live,

@ -4,9 +4,25 @@ import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/confi
export const useStatsTab = () => {
const [statsType, setStatsType] = useState<StatsType>(StatsType.FINAL_STATS)
const [isPlayersStatsFetching, setIsPlayersStatsFetching] = useState(false)
const [isTeamsStatsFetching, setIsTeamsStatsFetching] = useState(false)
const isFinalStatsType = statsType === StatsType.FINAL_STATS
const toggleStatsType = () => {
const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS
setStatsType(newStatsType)
setIsTeamsStatsFetching(true)
setIsPlayersStatsFetching(true)
}
return {
setStatsType,
isPlayersStatsFetching,
isTeamsStatsFetching,
setIsPlayersStatsFetching,
setIsTeamsStatsFetching,
statsType,
toggleStatsType,
}
}

@ -1,3 +1,4 @@
import type { Dispatch, SetStateAction } from 'react'
import {
useEffect,
useState,
@ -19,12 +20,14 @@ const STATS_POLL_INTERVAL = 30000
type UseTeamsStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
setIsTeamsStatsFetching: Dispatch<SetStateAction<boolean>>,
statsType: StatsType,
}
export const useTeamsStats = ({
matchProfile,
playingProgress,
setIsTeamsStatsFetching,
statsType,
}: UseTeamsStatsArgs) => {
const [teamsStats, setTeamsStats] = useState<{
@ -37,32 +40,37 @@ export const useTeamsStats = ({
const isCurrentStats = statsType === StatsType.CURRENT_STATS
const fetchTeamsStats = useMemo(() => throttle((second?: number) => {
const fetchTeamsStats = useMemo(() => throttle(async (second?: number) => {
if (!sportName) return
getTeamsStats({
matchId,
second,
sportName,
}).then(setTeamsStats)
}, REQUEST_DELAY), [matchId, sportName])
try {
const data = await getTeamsStats({
matchId,
second,
sportName,
})
setTeamsStats(data)
setIsTeamsStatsFetching(false)
// eslint-disable-next-line no-empty
} catch (e) {}
}, REQUEST_DELAY), [matchId, setIsTeamsStatsFetching, sportName])
useEffect(() => {
let timer: ReturnType<typeof setInterval>
let interval: NodeJS.Timeout
if (!isCurrentStats) {
fetchTeamsStats()
}
if (matchProfile?.live) {
timer = setInterval(() => {
if (isCurrentStats) return
if (matchProfile?.live && !isCurrentStats) {
interval = setInterval(() => {
fetchTeamsStats()
}, STATS_POLL_INTERVAL)
}
return () => clearInterval(timer)
return () => clearInterval(interval)
}, [fetchTeamsStats, matchProfile?.live, isCurrentStats])
useEffect(() => {

@ -0,0 +1,16 @@
import styled from 'styled-components/macro'
import { CircleAnimationBar as CircleAnimationBarBase } from 'features/CircleAnimationBar'
export const CircleAnimationBar = styled(CircleAnimationBarBase)`
position: absolute;
transform: translateY(-50%);
circle {
stroke: #4086C6;
}
text {
fill: ${({ theme }) => theme.colors.white};
}
`

@ -0,0 +1,65 @@
import type { PropsWithChildren, HTMLProps } from 'react'
import { memo } from 'react'
import { createPortal } from 'react-dom'
import { isMobileDevice } from 'config'
import { useModalRoot, useTooltip } from 'hooks'
import { Tooltip, CellContainer } from './styled'
type CellProps = {
anchorId?: string,
as?: 'td' | 'th',
clickable?: boolean,
columnWidth?: number,
sorted?: boolean,
tooltipText?: string,
} & HTMLProps<HTMLTableCellElement>
const CellFC = ({
anchorId,
as,
children,
clickable,
columnWidth,
onClick,
sorted,
tooltipText,
}: PropsWithChildren<CellProps>) => {
const {
isTooltipShown,
onMouseLeave,
onMouseOver,
tooltipStyle,
} = useTooltip()
const modalRoot = useModalRoot()
return (
<CellContainer
as={as}
onClick={onClick}
clickable={clickable}
columnWidth={columnWidth}
sorted={sorted}
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)

@ -1,6 +1,7 @@
export const PARAM_COLUMN_WIDTH = 50
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 = 4
export const FIRST_COLUMN_WIDTH_DEFAULT = 100
export const DISPLAYED_PARAMS_COLUMNS = 5
export const SCROLLBAR_WIDTH = 8

@ -34,8 +34,8 @@ export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => {
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : val)
const getFullName = useCallback((player: Player) => (
trim(`${player[`firstname_${suffix}`]} ${player[`lastname_${suffix}`]}`)
const getPlayerName = useCallback((player: Player) => (
trim(player[`lastname_${suffix}`] || '')
), [suffix])
const getParamValue = useCallback((playerId: number, paramId: number) => {
@ -49,7 +49,7 @@ export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => {
const players = playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2']
return isNil(sortCondition.paramId)
? orderBy(players, getFullName)
? orderBy(players, getPlayerName)
: orderBy(
players,
[
@ -58,12 +58,12 @@ export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => {
return isNil(paramValue) ? -1 : paramValue
},
getFullName,
getPlayerName,
],
sortCondition.dir,
)
}, [
getFullName,
getPlayerName,
getParamValue,
playersData,
matchProfile?.team1.id,
@ -74,7 +74,7 @@ export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => {
return {
getDisplayedValue,
getFullName,
getPlayerName,
getPlayerParams,
isExpanded,
players: sortedPlayers,

@ -4,9 +4,11 @@ import type {
SetStateAction,
} from 'react'
import {
useCallback,
useRef,
useState,
useEffect,
useLayoutEffect,
useMemo,
} from 'react'
@ -29,9 +31,9 @@ import { useLexicsConfig } from 'features/LexicsStore'
import type { SortCondition } from '../types'
import {
PARAM_COLUMN_WIDTH,
DISPLAYED_PARAMS_COLUMNS,
PARAM_COLUMN_WIDTH_DEFAULT,
FIRST_COLUMN_WIDTH_DEFAULT,
DISPLAYED_PARAMS_COLUMNS,
SCROLLBAR_WIDTH,
} from '../config'
@ -51,8 +53,13 @@ export const useTable = ({
const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRightArrow] = useState(false)
const [paramColumnWidth, setParamColumnWidth] = useState(PARAM_COLUMN_WIDTH_DEFAULT)
const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle()
const {
close: reduceTable,
isOpen: isExpanded,
toggle: toggleIsExpanded,
} = useToggle()
const { playersStats } = useMatchPageStore()
const params = useMemo(() => (
@ -95,27 +102,38 @@ export const useTable = ({
const paramsCount = size(params)
const getParamColumnWidth = () => {
const rest = (
(containerRef.current?.clientWidth || 0) - FIRST_COLUMN_WIDTH_DEFAULT - SCROLLBAR_WIDTH
const getParamColumnWidth = useCallback(() => {
const paramsTableWidth = (
(containerRef.current?.clientWidth || 0)
- FIRST_COLUMN_WIDTH_DEFAULT
- SCROLLBAR_WIDTH - 8
)
const desktopWith = PARAM_COLUMN_WIDTH
const mobileWidth = paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : rest / DISPLAYED_PARAMS_COLUMNS
return isMobileDevice ? mobileWidth : desktopWith
}
return isExpanded
? PARAM_COLUMN_WIDTH_DEFAULT
: paramsTableWidth / DISPLAYED_PARAMS_COLUMNS
}, [isExpanded])
const getFirstColumnWidth = () => {
if (isExpanded) return 0
const slideLeft = () => {
const {
clientHeight = 0,
clientWidth = 0,
scrollHeight = 0,
scrollLeft = 0,
scrollWidth = 0,
} = tableWrapperRef.current || {}
return paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : FIRST_COLUMN_WIDTH_DEFAULT
}
const hasVerticalScroll = scrollHeight > clientHeight
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
const paramColumnWidth = getParamColumnWidth()
const firstColumnWidth = getFirstColumnWidth()
const scrollBy = scrollRight === 0
? paramColumnWidth - (hasVerticalScroll ? SCROLLBAR_WIDTH : SCROLLBAR_WIDTH * 2)
: paramColumnWidth
const slideLeft = () => tableWrapperRef.current?.scrollBy(-paramColumnWidth, 0)
const slideRight = () => tableWrapperRef.current?.scrollBy(paramColumnWidth, 0)
tableWrapperRef.current?.scrollBy(-scrollBy, 0)
}
const slideRight = () => {
tableWrapperRef.current?.scrollBy(paramColumnWidth, 0)
}
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : round(val, 2))
@ -133,13 +151,21 @@ export const useTable = ({
}
const handleSortClick = (paramId: number) => () => {
setSortCondition((curr) => ({
dir: curr.dir === 'asc' || curr.paramId !== paramId ? 'desc' : 'asc',
paramId,
}))
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,
}
})
}
useEffect(() => {
useLayoutEffect(() => {
const {
clientWidth = 0,
scrollLeft = 0,
@ -149,11 +175,20 @@ export const useTable = ({
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowRightArrow(scrollRight > 0)
}, [isExpanded])
}, [isExpanded, tableWrapperRef.current?.clientWidth, paramsCount])
useLayoutEffect(() => {
setParamColumnWidth(getParamColumnWidth())
}, [getParamColumnWidth, tableWrapperRef.current?.clientWidth])
useEffect(() => {
if (isExpanded && paramsCount <= DISPLAYED_PARAMS_COLUMNS) {
reduceTable()
}
}, [isExpanded, paramsCount, reduceTable])
return {
containerRef,
firstColumnWidth,
getDisplayedValue,
handleScroll,
handleSortClick,

@ -1,38 +1,41 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import includes from 'lodash/includes'
// import includes from 'lodash/includes'
import { PlayerParam } from 'requests'
import { T9n } from 'features/T9n'
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,
FirstColumn,
Cell,
Header,
Row,
PlayerNum,
PlayerNameWrapper,
PlayerName,
ParamShortTitle,
ArrowButtonRight,
ArrowButtonLeft,
Arrow,
ExpandButton,
Tooltip,
} from './styled'
export const PlayersTable = (props: PlayersTableProps) => {
const {
containerRef,
firstColumnWidth,
getDisplayedValue,
getFullName,
getPlayerName,
getPlayerParams,
handleScroll,
handleSortClick,
@ -49,6 +52,17 @@ export const PlayersTable = (props: PlayersTableProps) => {
tableWrapperRef,
toggleIsExpanded,
} = usePlayersTable(props)
const { translate } = useLexicsStore()
const { sportName } = usePageParams()
const { isPlayersStatsFetching } = useMatchPageStore()
if (isPlayersStatsFetching) {
return (
<Loader color={defaultTheme.colors.white} />
)
}
const firstColumnWidth = isExpanded ? FIRST_COLUMN_WIDTH_EXPANDED : FIRST_COLUMN_WIDTH_DEFAULT
return (
<Container
@ -62,14 +76,6 @@ export const PlayersTable = (props: PlayersTableProps) => {
>
{!isExpanded && (
<Fragment>
{showLeftArrow && (
<ArrowButtonLeft
aria-label='Scroll to left'
onClick={slideLeft}
>
<Arrow direction='left' />
</ArrowButtonLeft>
)}
{showRightArrow && (
<ArrowButtonRight
aria-label='Scroll to right'
@ -80,85 +86,95 @@ export const PlayersTable = (props: PlayersTableProps) => {
)}
</Fragment>
)}
<FirstColumn columnWidth={firstColumnWidth}>
<Row>
<Cell>
{showExpandButton && (
<ExpandButton
aria-label={isExpanded ? 'Reduce' : 'Expand'}
onClick={toggleIsExpanded}
>
<Arrow direction={isExpanded ? 'right' : 'left'} />
<Arrow direction={isExpanded ? 'right' : 'left'} />
</ExpandButton>
)}
</Cell>
</Row>
{map(players, (player) => {
const fullName = getFullName(player)
return (
<Row key={player.id}>
<Cell>
<PlayerNum>
{player.club_shirt_num}
</PlayerNum>{' '}
<PlayerNameWrapper>
<PlayerName columnWidth={firstColumnWidth}>
{fullName}
</PlayerName>
<Tooltip>
<PlayerName>{fullName}</PlayerName>
</Tooltip>
</PlayerNameWrapper>
</Cell>
</Row>
)
})}
</FirstColumn>
<Table>
<Row>
{map(params, ({
id,
lexic,
lexica_short,
}) => (
<Table role='marquee' aria-live='off'>
<Header>
<Row>
<Cell
key={id}
columnWidth={paramColumnWidth}
onClick={handleSortClick(id)}
sorted={sortCondition.paramId === id}
headerCell
as='th'
columnWidth={firstColumnWidth}
>
<ParamShortTitle t={lexica_short || ''} />
<Tooltip>
<T9n t={lexic} />
</Tooltip>
{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>
))}
</Row>
{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>
{map(players, (player) => (
<Row key={player.id}>
{map(params, ({ id }) => {
const playerParam = getPlayerParams(player.id)[id] as PlayerParam | undefined
const value = playerParam ? getDisplayedValue(playerParam) : '-'
const clickable = Boolean(playerParam?.clickable) && !includes([0, '-'], value)
const sorted = sortCondition.paramId === id
<tbody>
{map(players, (player) => {
const playerName = getPlayerName(player)
const playerNum = player.num ?? player.club_shirt_num
const playerProfileUrl = `/${sportName}/players/${player.id}`
return (
<Cell
columnWidth={paramColumnWidth}
key={id}
clickable={clickable}
sorted={sorted}
>
{value}
return (
<Row key={player.id}>
<Cell columnWidth={firstColumnWidth}>
<PlayerNum>{playerNum}</PlayerNum>{' '}
<PlayerName to={playerProfileUrl}>
{playerName}
</PlayerName>
</Cell>
)
})}
</Row>
))}
{map(params, ({ id }) => {
const playerParam = getPlayerParams(player.id)[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 === id
return (
<Cell
columnWidth={paramColumnWidth}
key={id}
// clickable={clickable}
clickable={false}
sorted={sorted}
>
{value}
</Cell>
)
})}
</Row>
)
})}
</tbody>
</Table>
</TableWrapper>
</Container>

@ -1,3 +1,5 @@
import { Link } from 'react-router-dom'
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
@ -15,8 +17,12 @@ type ContainerProps = {
}
export const Container = styled.div<ContainerProps>`
--bgColor: #333;
${({ isExpanded }) => (isExpanded
? ''
? css`
--bgColor: rgba(51, 51, 51, 0.7);
`
: css`
position: relative;
`)}
@ -27,15 +33,21 @@ type TableWrapperProps = {
}
export const TableWrapper = styled.div<TableWrapperProps>`
display: flex;
max-width: 100%;
max-height: calc(100vh - 235px);
max-height: calc(100vh - 203px);
border-radius: 5px;
overflow-x: auto;
scroll-behavior: smooth;
background-color: #333333;
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`
@ -45,194 +57,217 @@ export const TableWrapper = styled.div<TableWrapperProps>`
: '')}
`
export const Table = styled.div`
flex-grow: 1;
export const Table = styled.table`
border-radius: 5px;
border-spacing: 0;
border-collapse: collapse;
letter-spacing: -0.078px;
table-layout: fixed;
`
export const Tooltip = styled(TooltipWrapper)`
left: auto;
display: block;
padding: 2px 10px;
border-radius: 6px;
transform: none;
font-size: 11px;
line-height: 1;
color: ${({ theme }) => theme.colors.black};
z-index: 999;
::before {
display: none;
}
`
export const ParamShortTitle = styled(T9n)`
type ParamShortTitleProps = {
showLeftArrow?: boolean,
sortDirection: 'asc' | 'desc',
sorted?: boolean,
}
export const ParamShortTitle = styled(T9n)<ParamShortTitleProps>`
position: relative;
text-transform: uppercase;
`
export const Row = styled.div`
display: flex;
width: 100%;
height: 45px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
::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;
`)}
:first-child {
position: sticky;
left: 0;
top: 0;
z-index: 1;
${({ showLeftArrow }) => (showLeftArrow
? ''
: css`
z-index: 1;
`)}
}
`
type TdProps = {
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,
headerCell?: boolean,
sorted?: boolean,
}
export const Cell = styled.div.attrs(({ clickable }: TdProps) => ({
export const CellContainer = styled.td.attrs(({ clickable }: CellContainerProps) => ({
...clickable && { tabIndex: 0 },
}))<TdProps>`
position: relative;
}))<CellContainerProps>`
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
min-width: 30px;
font-size: 11px;
color: ${({
clickable,
headerCell,
theme,
}) => (clickable && !headerCell ? '#5EB2FF' : theme.colors.white)};
font-weight: ${({ sorted }) => (sorted ? 'bold' : 'normal')};
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)};
white-space: nowrap;
background-color: #333333;
background-color: var(--bgColor);
${Tooltip} {
top: 35px;
}
:hover {
${Tooltip} {
display: block;
}
:first-child {
position: sticky;
left: 0;
justify-content: unset;
padding-left: 10px;
text-align: left;
cursor: unset;
z-index: 1;
}
${({ headerCell }) => (headerCell
? ''
: css`
:first-child {
justify-content: unset;
padding-left: 13px;
color: ${({ theme }) => theme.colors.white};
}
`)}
${({ sorted }) => (sorted
${({ clickable }) => (clickable
? css`
font-weight: bold;
cursor: pointer;
`
: '')}
${({ clickable, headerCell }) => (clickable || headerCell
${({ as, sorted }) => (as === 'th'
? css`
cursor: pointer;
font-weight: ${sorted ? '700' : '600'};
font-size: ${sorted ? 13 : 11}px;
`
: '')}
`
type FirstColumnProps = {
columnWidth?: number,
}
export const FirstColumn = styled.div<FirstColumnProps>`
export const Header = styled.thead`
position: sticky;
left: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
`
export const PlayerNum = styled.span`
display: inline-block;
width: 20px;
flex-shrink: 0;
text-align: center;
color: rgba(255, 255, 255, 0.5);
`
type PlayerNameProps = {
columnWidth?: number,
}
top: 0;
z-index: 2;
export const PlayerName = styled.span<PlayerNameProps>`
display: inline-block;
margin-top: 2px;
text-overflow: ellipsis;
overflow: hidden;
${CellContainer} {
background-color: #292929;
color: ${({ theme }) => theme.colors.white};
cursor: pointer;
}
${({ columnWidth }) => (columnWidth
? css`
max-width: calc(${columnWidth}px - 31px);
`
: css`
max-width: 110px;
`)}
${CellContainer}:first-child {
cursor: unset;
}
`
export const PlayerNameWrapper = styled.span`
export const Row = styled.tr`
position: relative;
display: flex;
width: 100%;
height: 45px;
border-bottom: 0.5px solid ${({ theme }) => theme.colors.secondary};
z-index: 1;
${Tooltip} {
top: 15px;
:last-child:not(:first-child) {
border: none;
}
:hover {
${Tooltip} {
display: block;
${CellContainer}:not(th) {
background-color: #484848;
}
${PlayerName} {
text-decoration: underline;
font-weight: 600;
}
}
`
export const Arrow = styled(ArrowBase)`
width: 10px;
height: 10px;
${isMobileDevice
? css`
border-color: ${({ theme }) => theme.colors.white};
`
: ''};
`
const ArrowButton = styled(ArrowButtonBase)`
position: absolute;
width: 17px;
margin-top: 2px;
background-color: #333333;
width: 20px;
height: 44px;
margin-top: 0;
z-index: 3;
background-color: #292929;
${isMobileDevice
? css`
height: 45px;
margin-top: 0;
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)`
left: 75px;
right: -5px;
`
export const Arrow = styled(ArrowBase)`
width: 10px;
height: 10px;
${isMobileDevice
? css`
border-color: ${({ theme }) => theme.colors.white};
`
: ''};
`
type ExpandButtonProps = {
isExpanded?: boolean,
}
export const ExpandButton = styled(ArrowButton)`
export const ExpandButton = styled(ArrowButton)<ExpandButtonProps>`
left: 20px;
top: 0;
${Arrow} {
left: 0;
left: ${({ isExpanded }) => (isExpanded ? -6 : -2)}px;
:last-child {
margin-left: 7px;

@ -4,11 +4,18 @@ 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 { Props } from './types'
import { useTeamsStatsTable } from './hooks'
import {
Container,
TableWrapper,
Table,
Header,
Row,
Cell,
TeamShortName,
ParamValueContainer,
ParamValue,
@ -16,78 +23,113 @@ import {
Divider,
} from './styled'
export const TeamsStatsTable = () => {
const { profile, teamsStats } = useMatchPageStore()
export const TeamsStatsTable = (props: Props) => {
const {
isTeamsStatsFetching,
profile,
teamsStats,
} = useMatchPageStore()
const {
getDisplayedValue,
getStatItemById,
isClickable,
// isClickable,
} = useTeamsStatsTable()
const { lang } = useLexicsStore()
const { shortSuffix } = useLexicsStore()
if (!profile) return null
if (isTeamsStatsFetching) {
return (
<Loader color={defaultTheme.colors.white} />
)
}
return (
<Container>
<Row>
<TeamShortName
nameObj={profile.team1}
prefix='abbrev_'
/>
<TeamShortName
nameObj={profile.team2}
prefix='abbrev_'
/>
</Row>
<TableWrapper>
<Table role='marquee' aria-live='off'>
<Header>
<Row>
<Cell as='th'>
<TeamShortName
nameObj={profile.team1}
prefix='abbrev_'
/>
</Cell>
<Cell as='th' />
<Cell as='th'>
<TeamShortName
nameObj={profile.team2}
prefix='abbrev_'
/>
</Cell>
</Row>
</Header>
{map(teamsStats[profile.team1.id], (team1StatItem) => {
const team2StatItem = getStatItemById(team1StatItem.param1.id)
const statItemTitle = team1StatItem[`name_${lang === 'ru' ? 'ru' : 'en'}`]
<tbody>
{map(teamsStats[profile.team1.id], (team1StatItem) => {
const team2StatItem = getStatItemById(team1StatItem.param1.id)
const statItemTitle = team1StatItem[`name_${shortSuffix}`]
return (
<Row key={team1StatItem.param1.id}>
<ParamValueContainer>
<ParamValue
clickable={isClickable(team1StatItem.param1)}
>
{getDisplayedValue(team1StatItem.param1.val)}
</ParamValue>
{team1StatItem.param2 && (
<Fragment>
<Divider>/</Divider>
<ParamValue
clickable={isClickable(team1StatItem.param2)}
>
{getDisplayedValue(team1StatItem.param2.val)}
</ParamValue>
</Fragment>
)}
</ParamValueContainer>
return (
<Row key={team1StatItem.param1.id}>
<Cell>
<ParamValueContainer>
<ParamValue
// clickable={isClickable(team1StatItem.param1)}
clickable={false}
>
{getDisplayedValue(team1StatItem.param1.val)}
</ParamValue>
{team1StatItem.param2 && (
<Fragment>
<Divider>/</Divider>
<ParamValue
// clickable={isClickable(team1StatItem.param2)}
clickable={false}
>
{getDisplayedValue(team1StatItem.param2.val)}
</ParamValue>
</Fragment>
)}
</ParamValueContainer>
</Cell>
<StatItemTitle>{statItemTitle}</StatItemTitle>
<Cell>
<StatItemTitle>{statItemTitle}</StatItemTitle>
</Cell>
{team2StatItem && (
<ParamValueContainer>
<ParamValue
clickable={isClickable(team2StatItem.param1)}
>
{getDisplayedValue(team2StatItem.param1.val)}
</ParamValue>
{team2StatItem.param2 && (
<Fragment>
<Divider>/</Divider>
<ParamValue
clickable={isClickable(team2StatItem.param2)}
>
{getDisplayedValue(team2StatItem.param2.val)}
</ParamValue>
</Fragment>
)}
</ParamValueContainer>
)}
</Row>
)
})}
<Cell>
{team2StatItem && (
<ParamValueContainer>
<ParamValue
// clickable={isClickable(team2StatItem.param1)}
clickable={false}
>
{getDisplayedValue(team2StatItem.param1.val)}
</ParamValue>
{team2StatItem.param2 && (
<Fragment>
<Divider>/</Divider>
<ParamValue
// clickable={isClickable(team2StatItem.param2)}
clickable={false}
>
{getDisplayedValue(team2StatItem.param2.val)}
</ParamValue>
</Fragment>
)}
</ParamValueContainer>
)}
</Cell>
</Row>
)
})}
</tbody>
</Table>
</TableWrapper>
</Container>
)
}

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

@ -13,21 +13,14 @@ export const Wrapper = styled.div`
? css`
overflow-y: auto;
width: 100%;
padding-right: 0;
${customScrollbar}
`
: ''};
`
export const TabsWrapper = styled.div`
padding: 0 30px;
${isMobileDevice
? css`
padding: 0 5px;
`
: ''};
`
export const TabsWrapper = styled.div``
type TabsGroupProps = {
hasLessThanFourTabs?: boolean,
@ -35,18 +28,22 @@ type TabsGroupProps = {
export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
display: flex;
height: 45px;
padding-top: 10px;
justify-content: center;
gap: 20px;
${({ hasLessThanFourTabs }) => (hasLessThanFourTabs
? css`
height: 40px;
padding-top: 10px;
${Tab} {
justify-content: center;
flex-direction: row;
gap: 5px;
}
${TabIcon} {
margin-bottom: 0;
}
`
: '')}
@ -68,7 +65,8 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
flex-direction: column;
justify-content: space-between;
align-items: center;
flex: 1;
padding-left: 0;
padding-right: 0;
opacity: 0.4;
cursor: pointer;
border: none;
@ -90,6 +88,8 @@ type TabIconProps = {
export const TabIcon = styled.div<TabIconProps>`
width: 22px;
height: 22px;
flex-shrink: 0;
margin-bottom: 5px;
background-image: url(/images/matchTabs/${({ icon }) => `${icon}.svg`});
background-repeat: no-repeat;
background-position: center;
@ -97,7 +97,7 @@ export const TabIcon = styled.div<TabIconProps>`
${({ icon }) => (icon === 'players'
? css`
background-size: 23px;
background-size: 25px;
`
: '')}
`
@ -125,6 +125,7 @@ export const Container = styled.div<TContainer>`
${isMobileDevice
? css`
padding: 0 5px;
padding-bottom: 20px;
overflow-y: hidden;
max-height: initial;

@ -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,88 @@
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`,
...(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,
}
}

@ -2,6 +2,8 @@ import isUndefined from 'lodash/isUndefined'
import { callApi } from 'helpers'
import { STATS_API_URL } from 'config'
export type Param = {
clickable: boolean,
data_type: string,
@ -47,7 +49,7 @@ export const getTeamsStats = async ({
const response: Response = await callApi({
config,
url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}`}`,
url: `${STATS_API_URL}/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}`}`,
})
if (response.error) Promise.reject(response)

Loading…
Cancel
Save