feat(in-142): match stats tab

pull/24/head
Ruslan Khayrullin 3 years ago committed by Andrei Dekterev
parent 6f694eeb9f
commit 269ddad403
  1. 5
      src/config/lexics/indexLexics.tsx
  2. 9
      src/features/MatchPage/components/FinishedMatch/index.tsx
  3. 8
      src/features/MatchPage/components/LiveMatch/hooks/index.tsx
  4. 18
      src/features/MatchPage/store/hooks/index.tsx
  5. 28
      src/features/MatchPage/store/hooks/useMatchData.tsx
  6. 159
      src/features/MatchPage/store/hooks/usePlayersStats.tsx
  7. 12
      src/features/MatchPage/store/hooks/useStatsTab.tsx
  8. 78
      src/features/MatchPage/store/hooks/useTeamsStats.tsx
  9. 39
      src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx
  10. 0
      src/features/MatchSidePlaylists/components/Matches/components/VideoDate/index.tsx
  11. 0
      src/features/MatchSidePlaylists/components/Matches/components/VideoDate/styled.tsx
  12. 12
      src/features/MatchSidePlaylists/components/Matches/index.tsx
  13. 9
      src/features/MatchSidePlaylists/components/Matches/styled.tsx
  14. 5
      src/features/MatchSidePlaylists/components/PlayersPlaylists/index.tsx
  15. 5
      src/features/MatchSidePlaylists/components/PlayersPlaylists/styled.tsx
  16. 6
      src/features/MatchSidePlaylists/components/PlayersTable/config.tsx
  17. 58
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx
  18. 83
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx
  19. 171
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx
  20. 397
      src/features/MatchSidePlaylists/components/PlayersTable/index.tsx
  21. 231
      src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx
  22. 8
      src/features/MatchSidePlaylists/components/PlayersTable/types.tsx
  23. 5
      src/features/MatchSidePlaylists/components/TabStats/config.tsx
  24. 68
      src/features/MatchSidePlaylists/components/TabStats/hooks.tsx
  25. 109
      src/features/MatchSidePlaylists/components/TabStats/index.tsx
  26. 40
      src/features/MatchSidePlaylists/components/TabStats/styled.tsx
  27. 77
      src/features/MatchSidePlaylists/components/TabWatch/index.tsx
  28. 276
      src/features/MatchSidePlaylists/components/TeamsStats/index.tsx
  29. 31
      src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx
  30. 93
      src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx
  31. 21
      src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx
  32. 72
      src/features/MatchSidePlaylists/hooks.tsx
  33. 25
      src/features/MatchSidePlaylists/index.tsx
  34. 44
      src/features/MatchSidePlaylists/styled.tsx
  35. 3
      src/features/MultiSourcePlayer/hooks/index.tsx
  36. 4
      src/features/StreamPlayer/components/YoutubePlayer/index.tsx
  37. 4
      src/features/StreamPlayer/index.tsx
  38. 25
      src/helpers/getTeamAbbr/index.tsx
  39. 1
      src/helpers/index.tsx
  40. 1
      src/hooks/usePageParams.tsx
  41. 65
      src/requests/getMatchParticipants.tsx
  42. 54
      src/requests/getPlayersStats.tsx
  43. 56
      src/requests/getTeamsStats.tsx
  44. 3
      src/requests/index.tsx

@ -9,8 +9,12 @@ const matchPopupLexics = {
apply: 13491,
choose_fav_team: 19776,
commentators: 15424,
current_stats: 19592,
display_all_stats: 19932,
display_stats_according_to_video: 19931,
episode_duration: 13410,
events: 1020,
final_stats: 19591,
from_end_match: 15396,
from_price: 3992,
from_start_match: 15395,
@ -160,6 +164,7 @@ export const indexLexics = {
no_match_access_body: 13419,
no_match_access_title: 13418,
player: 14975,
players: 164,
players_video: 13032,
privacy_policy_and_statement: 15404,
round_highilights: 13050,

@ -17,7 +17,11 @@ import { useMatchPageStore } from '../../store'
export const FinishedMatch = () => {
const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation)
const { isOpenPopup, profile } = useMatchPageStore()
const {
isOpenFiltersPopup,
profile,
setPlayingProgress,
} = useMatchPageStore()
const {
chapters,
closeSettingsPopup,
@ -48,9 +52,10 @@ export const FinishedMatch = () => {
<Fragment>
<MultiSourcePlayer
setCircleAnimation={setCircleAnimation}
isOpenPopup={isOpenPopup}
isOpenPopup={isOpenFiltersPopup}
chapters={chapters}
onPlayingChange={onPlayingChange}
onPlayerProgressChange={setPlayingProgress}
profile={profile}
/>
<MatchDescription />

@ -21,6 +21,7 @@ export const useLiveMatch = () => {
profile,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
} = useMatchPageStore()
const { profileId: matchId, sportType } = usePageParams()
const resume = useResumeUrlParam()
@ -45,7 +46,7 @@ export const useLiveMatch = () => {
} = usePlaylistLogger()
const {
onPlayerProgressChange,
onPlayerProgressChange: playerProgressChange,
onPlayingChange: notifyProgressLogger,
} = usePlayerProgressReporter()
@ -66,6 +67,11 @@ export const useLiveMatch = () => {
handlePlaylistClick(playlist, e)
}
const onPlayerProgressChange = (seconds: number, period = 0) => {
playerProgressChange(seconds, period)
setPlayingProgress(seconds * 1000)
}
return {
chapters,
isPlayFilterEpisodes,

@ -41,7 +41,7 @@ export const useMatchPage = () => {
countOfFilters,
filters,
isEmptyFilters,
isOpen: isOpenPopup,
isOpen: isOpenFiltersPopup,
resetEvents,
resetPlayers,
toggle: togglePopup,
@ -66,9 +66,16 @@ export const useMatchPage = () => {
const {
events,
handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists,
playersData,
playersStats,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
setStatsType,
statsType,
teamsStats,
} = useMatchData(matchProfile)
const profile = matchProfile
@ -160,8 +167,9 @@ export const useMatchPage = () => {
handlePlaylistClick,
hideProfileCard,
isEmptyFilters,
isEmptyPlayersStats,
isLiveMatch,
isOpenPopup,
isOpenFiltersPopup,
isPlayFilterEpisodes,
isStarted,
likeImage,
@ -170,6 +178,8 @@ export const useMatchPage = () => {
plaingOrder,
playEpisodes,
playNextEpisode,
playersData,
playersStats,
profile,
profileCardShown,
resetEvents,
@ -179,10 +189,14 @@ export const useMatchPage = () => {
setFullMatchPlaylistDuration,
setIsPlayinFiltersEpisodes,
setPlaingOrder,
setPlayingProgress,
setReversed,
setStatsType,
setUnreversed,
setWatchAllEpisodesTimer,
showProfileCard,
statsType,
teamsStats,
toggleActiveEvents,
toggleActivePlayers,
togglePopup,

@ -6,7 +6,7 @@ import {
import debounce from 'lodash/debounce'
import { MatchInfo } from 'requests/getMatchInfo'
import type { MatchInfo } from 'requests/getMatchInfo'
import { usePageParams } from 'hooks/usePageParams'
import { useInterval } from 'hooks/useInterval'
@ -16,6 +16,9 @@ import { useMatchPopupStore } from 'features/MatchPopup'
import { useMatchPlaylists } from './useMatchPlaylists'
import { useEvents } from './useEvents'
import { useTeamsStats } from './useTeamsStats'
import { useStatsTab } from './useStatsTab'
import { usePlayersStats } from './usePlayersStats'
const MATCH_DATA_POLL_INTERVAL = 60000
const MATCH_PLAYLISTS_DELAY = 5000
@ -24,6 +27,7 @@ export const useMatchData = (profile: MatchInfo) => {
const { profileId: matchId, sportType } = usePageParams()
const { chapters } = useMatchPopupStore()
const [matchDuration, setMatchDuration] = useState(0)
const [playingProgress, setPlayingProgress] = useState(0)
const {
fetchMatchPlaylists,
handlePlaylistClick,
@ -33,6 +37,21 @@ export const useMatchData = (profile: MatchInfo) => {
setSelectedPlaylist,
} = useMatchPlaylists(profile)
const { events, fetchMatchEvents } = useEvents()
const { setStatsType, statsType } = useStatsTab()
const { teamsStats } = useTeamsStats({
matchProfile: profile,
playingProgress,
statsType,
})
const {
isEmptyPlayersStats,
playersData,
playersStats,
} = usePlayersStats({
matchProfile: profile,
playingProgress,
statsType,
})
const fetchPlaylistsDebounced = useMemo(
() => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY),
@ -93,8 +112,15 @@ export const useMatchData = (profile: MatchInfo) => {
return {
events,
handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists,
playersData,
playersStats,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
setStatsType,
statsType,
teamsStats,
}
}

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

@ -0,0 +1,78 @@
import {
useEffect,
useState,
useMemo,
} from 'react'
import throttle from 'lodash/throttle'
import type { MatchInfo } from 'requests'
import { getTeamsStats, TeamStatItem } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000
type UseTeamsStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
statsType: StatsType,
}
export const useTeamsStats = ({
matchProfile,
playingProgress,
statsType,
}: UseTeamsStatsArgs) => {
const [teamsStats, setTeamsStats] = useState<{
[teamId: string]: Array<TeamStatItem>,
}>({})
const { profileId: matchId, sportName } = usePageParams()
const progressSec = Math.floor(playingProgress / 1000)
const isCurrentStats = statsType === StatsType.CURRENT_STATS
const fetchTeamsStats = useMemo(() => throttle((second?: number) => {
if (!sportName) return
getTeamsStats({
matchId,
second,
sportName,
}).then(setTeamsStats)
}, REQUEST_DELAY), [matchId, sportName])
useEffect(() => {
let timer: ReturnType<typeof setInterval>
if (!isCurrentStats) {
fetchTeamsStats()
}
if (matchProfile?.live) {
timer = setInterval(() => {
if (isCurrentStats) return
fetchTeamsStats()
}, STATS_POLL_INTERVAL)
}
return () => clearInterval(timer)
}, [fetchTeamsStats, matchProfile?.live, isCurrentStats])
useEffect(() => {
if (isCurrentStats) {
fetchTeamsStats(progressSec)
}
}, [fetchTeamsStats, progressSec, isCurrentStats])
return {
statsType,
teamsStats,
}
}

@ -1,8 +1,8 @@
import { useMemo } from 'react'
import type { ForwardedRef } from 'react'
import { forwardRef } from 'react'
import styled, { css } from 'styled-components/macro'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@ -15,6 +15,8 @@ import { T9n } from 'features/T9n'
import { PlayButton } from '../PlayButton'
export const LIST_ITEM_INDENT = 12
type Props = {
live?: boolean,
onSelect?: (selectedMathPlaylist: PlaylistOption) => void,
@ -25,7 +27,7 @@ type Props = {
const List = styled.ul``
const Item = styled.li`
margin-bottom: 12px;
margin-bottom: ${LIST_ITEM_INDENT}px;
width: 100%;
height: 36px;
${isMobileDevice
@ -36,24 +38,17 @@ const Item = styled.li`
: ''};
`
export const MatchPlaylists = ({
live,
onSelect,
playlists,
selectedMathPlaylist,
}: Props) => {
const filteredPlayListByDuration = useMemo(() => (
filter(playlists, (playlist) => (
live
? Boolean(playlist.duration) || (playlist.id === 'full_game')
: Boolean(playlist.duration)
))
), [playlists, live])
return (
<List>
export const MatchPlaylists = forwardRef(
({
live,
onSelect,
playlists,
selectedMathPlaylist,
}: Props,
ref: ForwardedRef<HTMLUListElement>) => (
<List ref={ref}>
{
map(filteredPlayListByDuration, (playlist) => (
map(playlists, (playlist) => (
<Item key={playlist.id}>
<PlayButton
duration={playlist.duration}
@ -68,5 +63,5 @@ export const MatchPlaylists = ({
))
}
</List>
)
}
),
)

@ -23,13 +23,15 @@ import { VideoDate } from './components/VideoDate'
import { MatchesWrapper } from './styled'
type Props = {
additionalScrollHeight: number,
profile: MatchInfo,
tournamentData: TournamentData,
}
const formatDate = (date: Date) => format(date, 'yyyy-MM-dd')
export const TabVideo = ({
export const Matches = ({
additionalScrollHeight,
profile,
tournamentData,
}: Props) => {
@ -75,7 +77,7 @@ export const TabVideo = ({
const hasScroll = scrollHeight > clientHeight
setOverflow(hasScroll)
}, [ref, selectedDate])
}, [ref.current?.clientHeight, selectedDate])
if (tournamentData.matches.length <= 1) return null
@ -88,7 +90,11 @@ export const TabVideo = ({
profileDate={profileDate}
onDateClick={setSelectedDate}
/>
<MatchesWrapper ref={ref} hasScroll={overflow}>
<MatchesWrapper
ref={ref}
hasScroll={overflow}
additionalScrollHeight={additionalScrollHeight}
>
{
map(sortBy(matches, ({ live }) => !live), (match) => (
<MatchCard

@ -2,9 +2,14 @@ import styled, { css } from 'styled-components/macro'
import { customScrollbar } from 'features/Common'
import { isMobileDevice } from '../../../../config/userAgent'
export const MatchesWrapper = styled.div<{hasScroll?: boolean}>`
type MatchesWrapperProps = {
additionalScrollHeight: number,
hasScroll?: boolean,
}
export const MatchesWrapper = styled.div<MatchesWrapperProps>`
overflow-y: auto;
max-height: calc(100vh - 170px);
max-height: calc(100vh - 165px - ${({ additionalScrollHeight }) => additionalScrollHeight}px);
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')};
> * {

@ -15,12 +15,10 @@ import type {
} from 'features/MatchPage/types'
import { Name } from 'features/Name'
import { T9n } from 'features/T9n'
import { isEqual } from '../../helpers'
import { PlayButton } from '../PlayButton'
import { BlockTitle } from '../../styled'
import {
Wrapper,
List,
@ -58,9 +56,6 @@ export const PlayersPlaylists = ({
return (
<Wrapper>
<BlockTitle>
<T9n t='players_episodes' />
</BlockTitle>
<Tabs>
<Tab
active={selectedTeam === Teams.TEAM1}

@ -28,9 +28,8 @@ export const PlayerAvatar = styled(ProfileLogo)`
`
export const Tabs = styled.div`
display: flex;
margin-top: 4px;
margin-bottom: 8px;
margin-top: -10px;
margin-bottom: 6px;
`
type TabProps = {

@ -0,0 +1,6 @@
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

@ -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,243 +1,166 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import includes from 'lodash/includes'
import { PlayerParam } from 'requests'
import { T9n } from 'features/T9n'
import type { PlayersTableProps } from './types'
import { usePlayersTable } from './hooks'
import {
Container,
TableWrapper,
Table,
Thead,
Th,
Tbody,
Tr,
Td,
FirstColumn,
Cell,
Row,
PlayerNum,
PlayerNameWrapper,
PlayerName,
ParamShortTitle,
ArrowButtonRight,
ArrowButtonLeft,
Arrow,
ExpandButton,
Tooltip,
} from './styled'
export const PlayersTable = () => (
<Table>
<Thead>
<Th />
<Th>
<ParamShortTitle>Min</ParamShortTitle>
</Th>
<Th sorted>
<ParamShortTitle>Pt</ParamShortTitle>
</Th>
<Th>
<ParamShortTitle>Reb</ParamShortTitle>
</Th>
<Th>
<ParamShortTitle>Ass</ParamShortTitle>
</Th>
<Th>
<ParamShortTitle>To</ParamShortTitle>
</Th>
</Thead>
<Tbody>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td>
<Td sorted>12</Td>
<Td>2</Td>
<Td>1</Td>
<Td>4</Td>
</Tr>
</Tbody>
</Table>
)
export const PlayersTable = (props: PlayersTableProps) => {
const {
containerRef,
firstColumnWidth,
getDisplayedValue,
getFullName,
getPlayerParams,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
players,
showExpandButton,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
sortCondition,
tableWrapperRef,
toggleIsExpanded,
} = usePlayersTable(props)
return (
<Container
ref={containerRef}
isExpanded={isExpanded}
>
<TableWrapper
ref={tableWrapperRef}
isExpanded={isExpanded}
onScroll={handleScroll}
>
{!isExpanded && (
<Fragment>
{showLeftArrow && (
<ArrowButtonLeft
aria-label='Scroll to left'
onClick={slideLeft}
>
<Arrow direction='left' />
</ArrowButtonLeft>
)}
{showRightArrow && (
<ArrowButtonRight
aria-label='Scroll to right'
onClick={slideRight}
>
<Arrow direction='right' />
</ArrowButtonRight>
)}
</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,
}) => (
<Cell
key={id}
columnWidth={paramColumnWidth}
onClick={handleSortClick(id)}
sorted={sortCondition.paramId === id}
headerCell
>
<ParamShortTitle t={lexica_short || ''} />
<Tooltip>
<T9n t={lexic} />
</Tooltip>
</Cell>
))}
</Row>
{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
return (
<Cell
columnWidth={paramColumnWidth}
key={id}
clickable={clickable}
sorted={sorted}
>
{value}
</Cell>
)
})}
</Row>
))}
</Table>
</TableWrapper>
</Container>
)
}

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

@ -3,3 +3,8 @@ export enum Tabs {
TEAM1,
TEAM2,
}
export enum StatsType {
FINAL_STATS,
CURRENT_STATS,
}

@ -0,0 +1,68 @@
import { useEffect, useState } from 'react'
import isEmpty from 'lodash/isEmpty'
import { useMatchPageStore } from 'features/MatchPage/store'
import { StatsType, Tabs } from './config'
export const useTabStats = () => {
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS)
const {
isEmptyPlayersStats,
profile: matchProfile,
setStatsType,
statsType,
teamsStats,
} = useMatchPageStore()
const isFinalStatsType = statsType === StatsType.FINAL_STATS
const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats'
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 newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS
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 {
isFinalStatsType,
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
selectedTab,
setSelectedTab,
switchTitleLexic,
toggleStatsType,
tooltipLexic,
}
}

@ -1,7 +1,16 @@
import { isMobileDevice } from 'config/userAgent'
import { getTeamAbbr } from 'helpers'
import { Tooltip } from 'features/Tooltip'
import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Name } from 'features/Name'
import { Tabs } from './config'
import { useTabStats } from './hooks'
import { PlayersTable } from '../PlayersTable'
import { TeamsStats } from '../TeamsStats'
import { TeamsStatsTable } from '../TeamsStatsTable'
import {
Container,
@ -13,22 +22,82 @@ import {
SwitchButton,
} from './styled'
export const TabStats = () => (
<Container>
<Header>
<TabList>
<Tab selected>Teams</Tab>
<Tab>DIN</Tab>
<Tab>SPA</Tab>
</TabList>
<Switch>
<SwitchTitle>Final Stats</SwitchTitle>
<SwitchButton>
<Tooltip lexic='others' />
</SwitchButton>
</Switch>
</Header>
<PlayersTable />
{/* <TeamsStats /> */}
</Container>
)
const tabPanes = {
[Tabs.TEAMS]: TeamsStatsTable,
[Tabs.TEAM1]: PlayersTable,
[Tabs.TEAM2]: PlayersTable,
}
export const TabStats = () => {
const {
isFinalStatsType,
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
selectedTab,
setSelectedTab,
switchTitleLexic,
toggleStatsType,
tooltipLexic,
} = useTabStats()
const { profile: matchProfile } = useMatchPageStore()
const TabPane = tabPanes[selectedTab]
if (!matchProfile) return null
const { team1, team2 } = matchProfile
return (
<Container>
<Header>
<TabList>
{isVisibleTeamsTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAMS}
onClick={() => setSelectedTab(Tabs.TEAMS)}
>
<T9n t='team' />
</Tab>
)}
{isVisibleTeam1PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM1}
onClick={() => setSelectedTab(Tabs.TEAM1)}
>
<Name nameObj={{
name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng),
name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus),
}}
/>
</Tab>
)}
{isVisibleTeam2PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM2}
onClick={() => setSelectedTab(Tabs.TEAM2)}
>
<Name nameObj={{
name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng),
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus),
}}
/>
</Tab>
)}
</TabList>
<Switch>
<SwitchTitle t={switchTitleLexic} />
<SwitchButton
isFinalStatsType={isFinalStatsType}
onClick={toggleStatsType}
>
{!isMobileDevice && <Tooltip lexic={tooltipLexic} />}
</SwitchButton>
</Switch>
</Header>
<TabPane
teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id}
/>
</Container>
)
}

@ -1,6 +1,7 @@
import styled, { css } from 'styled-components/macro'
import { TooltipWrapper } from 'features/Tooltip'
import { T9n } from 'features/T9n'
export const Container = styled.div``
@ -14,42 +15,40 @@ export const TabList = styled.div.attrs({ role: 'tablist' })`
display: flex;
`
type TabProps = {
selected?: boolean,
}
export const Tab = styled.div.attrs(({ selected }: TabProps) => ({
'aria-pressed': selected,
role: 'tab',
}))<TabProps>`
export const Tab = styled.button.attrs({ role: 'tab' })`
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px 10px;
padding: 0 10px 10px;
font-size: 12px;
color: ${({ theme }) => theme.colors.white};
opacity: ${({ selected }) => (selected ? '1' : '0.4')};
opacity: 0.4;
cursor: pointer;
border: none;
background: none;
border-bottom: 2px solid transparent;
${({ selected, theme }) => (selected
? css`
border-color: ${theme.colors.white};
`
: '')}
&[aria-pressed="true"] {
opacity: 1;
border-color: currentColor;
}
`
export const Switch = styled.div`
display: flex;
`
export const SwitchTitle = styled.span`
export const SwitchTitle = styled(T9n)`
font-size: 12px;
color: ${({ theme }) => theme.colors.white};
white-space: nowrap;
`
export const SwitchButton = styled.button`
type SwitchButtonProps = {
isFinalStatsType: boolean,
}
export const SwitchButton = styled.button<SwitchButtonProps>`
position: relative;
width: 20px;
height: 7px;
@ -63,9 +62,12 @@ export const SwitchButton = styled.button`
${TooltipWrapper} {
left: auto;
right: 0;
top: 15px;
padding: 2px 10px;
border-radius: 6px;
transform: none;
font-size: 11px;
line-height: 1;
::before {
display: none;
@ -78,7 +80,7 @@ export const SwitchButton = styled.button`
}
}
${({ theme }) => (true // Позже будет добавлен пропс
${({ isFinalStatsType, theme }) => (!isFinalStatsType
? css`
background-image: linear-gradient(
to right,

@ -1,6 +1,11 @@
import { Fragment } from 'react'
import {
Fragment,
useMemo,
useRef,
} from 'react'
import size from 'lodash/size'
import filter from 'lodash/filter'
import type {
PlaylistOption,
@ -10,12 +15,13 @@ import type {
import type { MatchInfo } from 'requests'
import { DropdownSection } from '../DropdownSection'
import { MatchPlaylists } from '../MatchPlaylists'
import { MatchPlaylists, LIST_ITEM_INDENT } from '../MatchPlaylists'
import { SideInterviews } from '../SideInterviews'
import { TabVideo } from '../../components/TabVideo'
import { Matches } from '../Matches'
type Props = {
onSelect: (option: PlaylistOption) => void,
playListFilter: number,
playlists: Playlists,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
@ -24,31 +30,50 @@ type Props = {
export const TabWatch = ({
onSelect,
playListFilter,
playlists,
profile,
selectedPlaylist,
tournamentData,
}: Props) => (
<Fragment>
<MatchPlaylists
playlists={playlists.match}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
live={profile?.live}
/>
<DropdownSection
itemsCount={size(playlists.interview)}
title={playlists.lexics?.interview}
>
<SideInterviews
interviews={playlists.interview}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
}: Props) => {
const matchPlaylistsRef = useRef<HTMLUListElement>(null)
const additionalScrollHeight = (matchPlaylistsRef.current?.clientHeight || 0) + LIST_ITEM_INDENT
const filteredPlayListByDuration = useMemo(() => (
filter(playlists.match, (playlist) => (
profile?.live
? Boolean(playlist.duration) || (playlist.id === 'full_game')
: Boolean(playlist.duration)
))
), [playlists.match, profile?.live])
return (
<Fragment>
{playListFilter > 1 && (
<MatchPlaylists
ref={matchPlaylistsRef}
playlists={filteredPlayListByDuration}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
live={profile?.live}
/>
)}
<DropdownSection
itemsCount={size(playlists.interview)}
title={playlists.lexics?.interview}
>
<SideInterviews
interviews={playlists.interview}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
/>
</DropdownSection>
<Matches
profile={profile}
tournamentData={tournamentData}
additionalScrollHeight={additionalScrollHeight}
/>
</DropdownSection>
<TabVideo
profile={profile}
tournamentData={tournamentData}
/>
</Fragment>
)
</Fragment>
)
}

@ -1,276 +0,0 @@
import {
Container,
Row,
TeamShortName,
ParamValueContainer,
ParamValue,
ParamTitle,
} from './styled'
export const TeamsStats = () => (
<Container>
<Row>
<TeamShortName>DIN</TeamShortName>
<TeamShortName>SPA</TeamShortName>
</Row>
<Row>
<ParamValueContainer>
<ParamValue clickable>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue clickable>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
<Row>
<ParamValueContainer>
<ParamValue>90</ParamValue>
</ParamValueContainer>
<ParamTitle>Points</ParamTitle>
<ParamValue>123</ParamValue>
</Row>
</Container>
)

@ -0,0 +1,31 @@
import isNumber from 'lodash/isNumber'
import find from 'lodash/find'
import round from 'lodash/round'
import type { Param } from 'requests'
import { useMatchPageStore } from 'features/MatchPage/store'
export const useTeamsStatsTable = () => {
const { profile, teamsStats } = useMatchPageStore()
const getDisplayedValue = (val: any) => (
isNumber(val) ? round(val, 2) : '-'
)
const getStatItemById = (paramId: number) => {
if (!profile) return null
return find(teamsStats[profile?.team2.id], ({ param1 }) => param1.id === paramId) || null
}
const isClickable = (param: Param) => (
Boolean(param.val) && param.clickable
)
return {
getDisplayedValue,
getStatItemById,
isClickable,
}
}

@ -0,0 +1,93 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import { useTeamsStatsTable } from './hooks'
import {
Container,
Row,
TeamShortName,
ParamValueContainer,
ParamValue,
StatItemTitle,
Divider,
} from './styled'
export const TeamsStatsTable = () => {
const { profile, teamsStats } = useMatchPageStore()
const {
getDisplayedValue,
getStatItemById,
isClickable,
} = useTeamsStatsTable()
const { lang } = useLexicsStore()
if (!profile) return null
return (
<Container>
<Row>
<TeamShortName
nameObj={profile.team1}
prefix='abbrev_'
/>
<TeamShortName
nameObj={profile.team2}
prefix='abbrev_'
/>
</Row>
{map(teamsStats[profile.team1.id], (team1StatItem) => {
const team2StatItem = getStatItemById(team1StatItem.param1.id)
const statItemTitle = team1StatItem[`name_${lang === 'ru' ? 'ru' : 'en'}`]
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>
<StatItemTitle>{statItemTitle}</StatItemTitle>
{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>
)
})}
</Container>
)
}

@ -1,16 +1,16 @@
import styled, { css } from 'styled-components/macro'
import { customScrollbar } from 'features/Common'
import { Name } from 'features/Name'
export const Container = styled.div`
width: 100%;
font-size: 11px;
overflow: hidden;
border-radius: 5px;
background-color: #333333;
${customScrollbar}
`
export const TeamShortName = styled.span`
export const TeamShortName = styled(Name)`
color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px;
text-transform: uppercase;
@ -37,7 +37,10 @@ type TParamValue = {
clickable?: boolean,
}
export const ParamValue = styled.span<TParamValue>`
export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({
...clickable && { tabIndex: 0 },
}))<TParamValue>`
font-weight: 600;
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)};
${({ clickable }) => (clickable
@ -47,10 +50,16 @@ export const ParamValue = styled.span<TParamValue>`
: '')}
`
export const ParamTitle = styled.span`
export const StatItemTitle = styled.span`
color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px;
text-transform: uppercase;
font-weight: 600;
opacity: 0.5;
`
export const Divider = styled.span`
color: ${({ theme }) => theme.colors.white};
opacity: 0.5;
font-weight: 600;
`

@ -5,6 +5,8 @@ import {
} from 'react'
import reduce from 'lodash/reduce'
import isEmpty from 'lodash/isEmpty'
import compact from 'lodash/compact'
import { useMatchPageStore } from 'features/MatchPage/store'
@ -14,26 +16,54 @@ export const useMatchSidePlaylists = () => {
const {
closePopup,
events,
isEmptyPlayersStats,
matchPlaylists: playlists,
profile: matchProfile,
teamsStats,
tournamentData,
} = useMatchPageStore()
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.WATCH)
const isWatchTabVisible = useMemo(() => {
const playListFilter = reduce(
playlists.match,
(acc, item) => {
let result = acc
if (item.duration) result++
return result
},
0,
)
return playListFilter > 1
}, [playlists])
const playListFilter = useMemo(() => reduce(
playlists.match,
(acc, item) => {
let result = acc
if (item.duration) result++
return result
},
0,
), [playlists.match])
const isWatchTabVisible = useMemo(() => (
playListFilter > 1 || tournamentData.matchDates.length > 1
), [playListFilter, tournamentData.matchDates.length])
const isEventTabVisible = useMemo(() => (
events.length > 0
), [events])
const isPlayersTabVisible = useMemo(() => (
!isEmpty(playlists.players.team1)
), [playlists.players.team1])
const isStatsTabVisible = useMemo(() => (
!isEmpty(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([
isWatchTabVisible,
isEventTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
]).length < 4
useEffect(() => {
switch (true) {
case isWatchTabVisible:
@ -42,18 +72,32 @@ export const useMatchSidePlaylists = () => {
case isEventTabVisible:
setSelectedTab(Tabs.EVENTS)
break
case isPlayersTabVisible:
setSelectedTab(Tabs.PLAYERS)
break
// case isStatsTabVisible:
// setSelectedTab(Tabs.STATS)
// break
}
}, [isEventTabVisible, isWatchTabVisible])
}, [
isEventTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
isWatchTabVisible,
])
useEffect(() => {
if (selectedTab !== Tabs.EVENTS) closePopup()
}, [selectedTab, closePopup])
return {
hasLessThanFourTabs,
isEventTabVisible,
isStatsTabVisible: true,
isPlayersTabVisible,
isStatsTabVisible,
isWatchTabVisible,
onTabClick: setSelectedTab,
playListFilter,
selectedTab,
}
}

@ -42,8 +42,6 @@ type Props = {
setCircleAnimation?: TSetCircleAnimation,
}
const hasLessThanFourTabs = false
export const MatchSidePlaylists = ({
circleAnimation,
onSelect,
@ -59,10 +57,13 @@ export const MatchSidePlaylists = ({
} = useMatchPageStore()
const {
hasLessThanFourTabs,
isEventTabVisible,
isStatsTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
isWatchTabVisible,
onTabClick,
playListFilter,
selectedTab,
} = useMatchSidePlaylists()
@ -108,7 +109,7 @@ export const MatchSidePlaylists = ({
<TabsGroup hasLessThanFourTabs={hasLessThanFourTabs}>
{isWatchTabVisible ? (
<Tab
selected={selectedTab === Tabs.WATCH}
aria-pressed={selectedTab === Tabs.WATCH}
onClick={() => onTabClick(Tabs.WATCH)}
>
<TabIcon icon='watch' />
@ -117,37 +118,38 @@ export const MatchSidePlaylists = ({
) : null}
{isEventTabVisible ? (
<Tab
selected={selectedTab === Tabs.EVENTS}
aria-pressed={selectedTab === Tabs.EVENTS}
onClick={() => onTabClick(Tabs.EVENTS)}
>
<TabIcon icon='plays' />
<TabTitle t='actions' />
</Tab>
) : null}
{isStatsTabVisible ? (
{isPlayersTabVisible ? (
<Tab
selected={selectedTab === Tabs.PLAYERS}
aria-pressed={selectedTab === Tabs.PLAYERS}
onClick={() => onTabClick(Tabs.PLAYERS)}
>
<TabIcon icon='players' />
<TabTitle t='stats' />
<TabTitle t='players' />
</Tab>
) : null}
{isStatsTabVisible ? (
{/* {isStatsTabVisible ? (
<Tab
selected={selectedTab === Tabs.STATS}
aria-pressed={selectedTab === Tabs.STATS}
onClick={() => onTabClick(Tabs.STATS)}
>
<TabIcon icon='stats' />
<TabTitle t='stats' />
</Tab>
) : null}
) : null} */}
</TabsGroup>
</TabsWrapper>
<Container
hasScroll={hasTabPaneScroll}
ref={tabPaneContainerRef}
forWatchTab={selectedTab === Tabs.WATCH}
>
<TabPane
setCircleAnimation={setCircleAnimation}
@ -157,6 +159,7 @@ export const MatchSidePlaylists = ({
playlists={playlists}
profile={profile}
selectedPlaylist={selectedPlaylist}
playListFilter={playListFilter}
/>
</Container>
</Wrapper>

@ -43,7 +43,7 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
height: 40px;
${Tab} {
justify-content: initial;
justify-content: center;
flex-direction: row;
gap: 5px;
}
@ -56,24 +56,30 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
: ''};
`
type TabProps = {
selected?: boolean,
}
export const TabTitle = styled(T9n)`
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
color: ${({ theme }) => theme.colors.white};
`
export const Tab = styled.div.attrs(({ selected }: TabProps) => ({
'aria-pressed': selected,
role: 'tab',
}))<TabProps>`
export const Tab = styled.button.attrs({ role: 'tab' })`
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
flex: 1;
opacity: ${({ selected }) => (selected ? '1' : '0.4')};
opacity: 0.4;
cursor: pointer;
border: none;
background: none;
:hover {
&[aria-pressed="true"], :hover {
opacity: 1;
${TabTitle} {
font-weight: 600;
}
}
`
@ -82,8 +88,8 @@ type TabIconProps = {
}
export const TabIcon = styled.div<TabIconProps>`
width: 20px;
height: 20px;
width: 22px;
height: 22px;
background-image: url(/images/matchTabs/${({ icon }) => `${icon}.svg`});
background-repeat: no-repeat;
background-position: center;
@ -96,28 +102,22 @@ export const TabIcon = styled.div<TabIconProps>`
: '')}
`
export const TabTitle = styled(T9n)`
font-size: 8px;
text-transform: uppercase;
color: ${({ theme }) => theme.colors.white};
`
type TContainer = {
forWatchTab?: boolean,
hasScroll: boolean,
}
export const Container = styled.div<TContainer>`
width: 320px;
margin-top: 23px;
margin-top: 14px;
max-height: calc(100vh - 130px);
overflow-y: ${({ forVideoTab }) => (forVideoTab ? 'hidden' : 'auto')};
padding-right: ${({ forVideoTab }) => (forVideoTab ? '0' : '')};
overflow-y: ${({ forWatchTab }) => (forWatchTab ? 'hidden' : 'auto')};
padding-right: ${({ forWatchTab }) => (forWatchTab ? '0' : '')};
padding-left: 14px;
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')};
${customScrollbar}
@media ${devices.tablet} {
margin-top: 15px;
}

@ -56,6 +56,7 @@ export type Props = {
chapters: Chapters,
isOpenPopup?: boolean,
onError?: () => void,
onPlayerProgressChange?: (ms: number) => void,
onPlayingChange: (playing: boolean) => void,
profile: MatchInfo,
setCircleAnimation: TSetCircleAnimation,
@ -64,6 +65,7 @@ export type Props = {
export const useMultiSourcePlayer = ({
chapters,
onError,
onPlayerProgressChange,
onPlayingChange,
setCircleAnimation,
}: Props) => {
@ -201,6 +203,7 @@ export const useMultiSourcePlayer = ({
timeForStatistics.current = (value + chapter.startMs) / 1000
setPlayerState({ playedProgress: value })
onPlayerProgressChange?.(playedMs + chapter.startMs)
}
const onEnded = () => {

@ -7,7 +7,7 @@ import { PlayerWrapper } from '../../styled'
import { useVideoPlayer, Props } from '../../hooks'
export const YoutubePlayer = (props: Props) => {
const { isOpenPopup, profile } = useMatchPageStore()
const { isOpenFiltersPopup, profile } = useMatchPageStore()
const {
onMouseMove,
@ -34,7 +34,7 @@ export const YoutubePlayer = (props: Props) => {
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
{isOpenPopup && <FiltersPopup />}
{isOpenFiltersPopup && <FiltersPopup />}
<YouTube
videoId={key}
opts={{

@ -30,7 +30,7 @@ import RewindMobile from './components/RewindMobile'
* HLS плеер, применяется на лайв и завершенных матчах
*/
export const StreamPlayer = (props: Props) => {
const { isOpenPopup, profile } = useMatchPageStore()
const { isOpenFiltersPopup, profile } = useMatchPageStore()
const { user } = useAuthStore()
const {
@ -96,7 +96,7 @@ export const StreamPlayer = (props: Props) => {
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
{isOpenPopup && <FiltersPopup />}
{isOpenFiltersPopup && <FiltersPopup />}
<LoaderWrapper buffering={buffering}>
<Loader color='#515151' />
</LoaderWrapper>

@ -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 './getRandomString'
export * from './selectedApi'
export * from './getTeamAbbr'

@ -22,6 +22,7 @@ export const usePageParams = () => {
return {
profileId: Number(pageId),
profileType: ProfileTypes[toUpper(profileName) as keyof typeof ProfileTypes],
sportName,
sportType: SportTypes[toUpper(sportName) as keyof typeof SportTypes],
}
}

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

@ -0,0 +1,56 @@
import isUndefined from 'lodash/isUndefined'
import { callApi } from 'helpers'
export type Param = {
clickable: boolean,
data_type: string,
id: number,
lexic: number,
markers: Array<number>,
name_en: string,
name_ru: string,
val: number | null,
}
export type TeamStatItem = {
lexic: number,
name_en: string,
name_ru: string,
order: number,
param1: Param,
param2: Param | null,
}
type Response = {
data?: {
[teamId: string]: Array<TeamStatItem>,
},
error?: string,
message?: string,
}
type GetTeamsStatsArgs = {
matchId: number,
second?: number,
sportName: string,
}
export const getTeamsStats = async ({
matchId,
second,
sportName,
}: GetTeamsStatsArgs) => {
const config = {
method: 'GET',
}
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}`}`,
})
if (response.error) Promise.reject(response)
return Promise.resolve(response.data || {})
}

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

Loading…
Cancel
Save