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. 29
      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' lang === 'ru' ? 'rus' : 'eng'
) )
export const getShortSuffix = (lang: string) => (
lang === 'ru' ? 'ru' : 'en'
)
export const getLexicIds = (ids: Array<LexicsId> | LexicsConfig) => ( export const getLexicIds = (ids: Array<LexicsId> | LexicsConfig) => (
uniq(map(ids, (id) => Number(id))) uniq(map(ids, (id) => Number(id)))
) )

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

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

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

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

@ -4,9 +4,25 @@ import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/confi
export const useStatsTab = () => { export const useStatsTab = () => {
const [statsType, setStatsType] = useState<StatsType>(StatsType.FINAL_STATS) 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 { return {
setStatsType, isPlayersStatsFetching,
isTeamsStatsFetching,
setIsPlayersStatsFetching,
setIsTeamsStatsFetching,
statsType, statsType,
toggleStatsType,
} }
} }

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

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

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

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

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

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

@ -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>

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

Loading…
Cancel
Save