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

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

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

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

@ -6,7 +6,7 @@ import {
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import { MatchInfo } from 'requests/getMatchInfo' import type { MatchInfo } from 'requests/getMatchInfo'
import { usePageParams } from 'hooks/usePageParams' import { usePageParams } from 'hooks/usePageParams'
import { useInterval } from 'hooks/useInterval' import { useInterval } from 'hooks/useInterval'
@ -16,6 +16,9 @@ import { useMatchPopupStore } from 'features/MatchPopup'
import { useMatchPlaylists } from './useMatchPlaylists' import { useMatchPlaylists } from './useMatchPlaylists'
import { useEvents } from './useEvents' import { useEvents } from './useEvents'
import { useTeamsStats } from './useTeamsStats'
import { useStatsTab } from './useStatsTab'
import { usePlayersStats } from './usePlayersStats'
const MATCH_DATA_POLL_INTERVAL = 60000 const MATCH_DATA_POLL_INTERVAL = 60000
const MATCH_PLAYLISTS_DELAY = 5000 const MATCH_PLAYLISTS_DELAY = 5000
@ -24,6 +27,7 @@ export const useMatchData = (profile: MatchInfo) => {
const { profileId: matchId, sportType } = usePageParams() const { profileId: matchId, sportType } = usePageParams()
const { chapters } = useMatchPopupStore() const { chapters } = useMatchPopupStore()
const [matchDuration, setMatchDuration] = useState(0) const [matchDuration, setMatchDuration] = useState(0)
const [playingProgress, setPlayingProgress] = useState(0)
const { const {
fetchMatchPlaylists, fetchMatchPlaylists,
handlePlaylistClick, handlePlaylistClick,
@ -33,6 +37,21 @@ export const useMatchData = (profile: MatchInfo) => {
setSelectedPlaylist, setSelectedPlaylist,
} = useMatchPlaylists(profile) } = useMatchPlaylists(profile)
const { events, fetchMatchEvents } = useEvents() const { events, fetchMatchEvents } = useEvents()
const { setStatsType, statsType } = useStatsTab()
const { teamsStats } = useTeamsStats({
matchProfile: profile,
playingProgress,
statsType,
})
const {
isEmptyPlayersStats,
playersData,
playersStats,
} = usePlayersStats({
matchProfile: profile,
playingProgress,
statsType,
})
const fetchPlaylistsDebounced = useMemo( const fetchPlaylistsDebounced = useMemo(
() => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY), () => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY),
@ -93,8 +112,15 @@ export const useMatchData = (profile: MatchInfo) => {
return { return {
events, events,
handlePlaylistClick, handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists, matchPlaylists,
playersData,
playersStats,
selectedPlaylist, selectedPlaylist,
setFullMatchPlaylistDuration, 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 styled, { css } from 'styled-components/macro'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map' import map from 'lodash/map'
@ -15,6 +15,8 @@ import { T9n } from 'features/T9n'
import { PlayButton } from '../PlayButton' import { PlayButton } from '../PlayButton'
export const LIST_ITEM_INDENT = 12
type Props = { type Props = {
live?: boolean, live?: boolean,
onSelect?: (selectedMathPlaylist: PlaylistOption) => void, onSelect?: (selectedMathPlaylist: PlaylistOption) => void,
@ -25,7 +27,7 @@ type Props = {
const List = styled.ul`` const List = styled.ul``
const Item = styled.li` const Item = styled.li`
margin-bottom: 12px; margin-bottom: ${LIST_ITEM_INDENT}px;
width: 100%; width: 100%;
height: 36px; height: 36px;
${isMobileDevice ${isMobileDevice
@ -36,24 +38,17 @@ const Item = styled.li`
: ''}; : ''};
` `
export const MatchPlaylists = ({ export const MatchPlaylists = forwardRef(
live, ({
onSelect, live,
playlists, onSelect,
selectedMathPlaylist, playlists,
}: Props) => { selectedMathPlaylist,
const filteredPlayListByDuration = useMemo(() => ( }: Props,
filter(playlists, (playlist) => ( ref: ForwardedRef<HTMLUListElement>) => (
live <List ref={ref}>
? Boolean(playlist.duration) || (playlist.id === 'full_game')
: Boolean(playlist.duration)
))
), [playlists, live])
return (
<List>
{ {
map(filteredPlayListByDuration, (playlist) => ( map(playlists, (playlist) => (
<Item key={playlist.id}> <Item key={playlist.id}>
<PlayButton <PlayButton
duration={playlist.duration} duration={playlist.duration}
@ -68,5 +63,5 @@ export const MatchPlaylists = ({
)) ))
} }
</List> </List>
) ),
} )

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

@ -2,9 +2,14 @@ import styled, { css } from 'styled-components/macro'
import { customScrollbar } from 'features/Common' import { customScrollbar } from 'features/Common'
import { isMobileDevice } from '../../../../config/userAgent' 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; overflow-y: auto;
max-height: calc(100vh - 170px); max-height: calc(100vh - 165px - ${({ additionalScrollHeight }) => additionalScrollHeight}px);
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')}; padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')};
> * { > * {

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

@ -28,9 +28,8 @@ export const PlayerAvatar = styled(ProfileLogo)`
` `
export const Tabs = styled.div` export const Tabs = styled.div`
display: flex; margin-top: -10px;
margin-top: 4px; margin-bottom: 6px;
margin-bottom: 8px;
` `
type TabProps = { 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 { import {
Container,
TableWrapper,
Table, Table,
Thead, FirstColumn,
Th, Cell,
Tbody, Row,
Tr,
Td,
PlayerNum, PlayerNum,
PlayerNameWrapper,
PlayerName,
ParamShortTitle, ParamShortTitle,
ArrowButtonRight,
ArrowButtonLeft,
Arrow,
ExpandButton,
Tooltip,
} from './styled' } from './styled'
export const PlayersTable = () => ( export const PlayersTable = (props: PlayersTableProps) => {
<Table> const {
<Thead> containerRef,
<Th /> firstColumnWidth,
<Th> getDisplayedValue,
<ParamShortTitle>Min</ParamShortTitle> getFullName,
</Th> getPlayerParams,
<Th sorted> handleScroll,
<ParamShortTitle>Pt</ParamShortTitle> handleSortClick,
</Th> isExpanded,
<Th> paramColumnWidth,
<ParamShortTitle>Reb</ParamShortTitle> params,
</Th> players,
<Th> showExpandButton,
<ParamShortTitle>Ass</ParamShortTitle> showLeftArrow,
</Th> showRightArrow,
<Th> slideLeft,
<ParamShortTitle>To</ParamShortTitle> slideRight,
</Th> sortCondition,
</Thead> tableWrapperRef,
<Tbody> toggleIsExpanded,
<Tr> } = usePlayersTable(props)
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td> return (
<Td sorted>12</Td> <Container
<Td>2</Td> ref={containerRef}
<Td>1</Td> isExpanded={isExpanded}
<Td>4</Td> >
</Tr> <TableWrapper
<Tr> ref={tableWrapperRef}
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> isExpanded={isExpanded}
<Td clickable>97</Td> onScroll={handleScroll}
<Td sorted>12</Td> >
<Td>2</Td> {!isExpanded && (
<Td>1</Td> <Fragment>
<Td>4</Td> {showLeftArrow && (
</Tr> <ArrowButtonLeft
<Tr> aria-label='Scroll to left'
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> onClick={slideLeft}
<Td clickable>97</Td> >
<Td sorted>12</Td> <Arrow direction='left' />
<Td>2</Td> </ArrowButtonLeft>
<Td>1</Td> )}
<Td>4</Td> {showRightArrow && (
</Tr> <ArrowButtonRight
<Tr> aria-label='Scroll to right'
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> onClick={slideRight}
<Td clickable>97</Td> >
<Td sorted>12</Td> <Arrow direction='right' />
<Td>2</Td> </ArrowButtonRight>
<Td>1</Td> )}
<Td>4</Td> </Fragment>
</Tr> )}
<Tr> <FirstColumn columnWidth={firstColumnWidth}>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> <Row>
<Td clickable>97</Td> <Cell>
<Td sorted>12</Td> {showExpandButton && (
<Td>2</Td> <ExpandButton
<Td>1</Td> aria-label={isExpanded ? 'Reduce' : 'Expand'}
<Td>4</Td> onClick={toggleIsExpanded}
</Tr> >
<Tr> <Arrow direction={isExpanded ? 'right' : 'left'} />
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> <Arrow direction={isExpanded ? 'right' : 'left'} />
<Td clickable>97</Td> </ExpandButton>
<Td sorted>12</Td> )}
<Td>2</Td> </Cell>
<Td>1</Td> </Row>
<Td>4</Td> {map(players, (player) => {
</Tr> const fullName = getFullName(player)
<Tr>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> return (
<Td clickable>97</Td> <Row key={player.id}>
<Td sorted>12</Td> <Cell>
<Td>2</Td> <PlayerNum>
<Td>1</Td> {player.club_shirt_num}
<Td>4</Td> </PlayerNum>{' '}
</Tr> <PlayerNameWrapper>
<Tr> <PlayerName columnWidth={firstColumnWidth}>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> {fullName}
<Td clickable>97</Td> </PlayerName>
<Td sorted>12</Td> <Tooltip>
<Td>2</Td> <PlayerName>{fullName}</PlayerName>
<Td>1</Td> </Tooltip>
<Td>4</Td> </PlayerNameWrapper>
</Tr> </Cell>
<Tr> </Row>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> )
<Td clickable>97</Td> })}
<Td sorted>12</Td> </FirstColumn>
<Td>2</Td> <Table>
<Td>1</Td> <Row>
<Td>4</Td> {map(params, ({
</Tr> id,
<Tr> lexic,
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> lexica_short,
<Td clickable>97</Td> }) => (
<Td sorted>12</Td> <Cell
<Td>2</Td> key={id}
<Td>1</Td> columnWidth={paramColumnWidth}
<Td>4</Td> onClick={handleSortClick(id)}
</Tr> sorted={sortCondition.paramId === id}
<Tr> headerCell
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> >
<Td clickable>97</Td> <ParamShortTitle t={lexica_short || ''} />
<Td sorted>12</Td> <Tooltip>
<Td>2</Td> <T9n t={lexic} />
<Td>1</Td> </Tooltip>
<Td>4</Td> </Cell>
</Tr> ))}
<Tr> </Row>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td> {map(players, (player) => (
<Td sorted>12</Td> <Row key={player.id}>
<Td>2</Td> {map(params, ({ id }) => {
<Td>1</Td> const playerParam = getPlayerParams(player.id)[id] as PlayerParam | undefined
<Td>4</Td> const value = playerParam ? getDisplayedValue(playerParam) : '-'
</Tr> const clickable = Boolean(playerParam?.clickable) && !includes([0, '-'], value)
<Tr> const sorted = sortCondition.paramId === id
<Td><PlayerNum>57</PlayerNum> Selikhov</Td>
<Td clickable>97</Td> return (
<Td sorted>12</Td> <Cell
<Td>2</Td> columnWidth={paramColumnWidth}
<Td>1</Td> key={id}
<Td>4</Td> clickable={clickable}
</Tr> sorted={sorted}
<Tr> >
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> {value}
<Td clickable>97</Td> </Cell>
<Td sorted>12</Td> )
<Td>2</Td> })}
<Td>1</Td> </Row>
<Td>4</Td> ))}
</Tr> </Table>
<Tr> </TableWrapper>
<Td><PlayerNum>57</PlayerNum> Selikhov</Td> </Container>
<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>
)

@ -1,72 +1,133 @@
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { customScrollbar } from 'features/Common' 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` export const Container = styled.div<ContainerProps>`
width: 100%; ${({ 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-radius: 5px;
border-collapse: collapse; overflow-x: auto;
letter-spacing: -0.078px; scroll-behavior: smooth;
background-color: #333333; background-color: #333333;
table-layout: fixed; z-index: 50;
${customScrollbar} ${customScrollbar}
`
export const Thead = styled.thead` ${({ isExpanded }) => (isExpanded
height: 45px; ? css`
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); position: absolute;
right: 14px;
`
: '')}
` `
type ThProps = { export const Table = styled.div`
sorted?: boolean, 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; font-size: 11px;
color: ${({ theme }) => theme.colors.white}; line-height: 1;
text-transform: uppercase; color: ${({ theme }) => theme.colors.black};
:first-child { ::before {
width: 115px; display: none;
} }
${({ sorted }) => (sorted
? ''
: css`
opacity: 0.5;
`)}
` `
export const ParamShortTitle = styled.span`` export const ParamShortTitle = styled(T9n)`
text-transform: uppercase;
export const Tbody = styled.tbody`` `
export const Tr = styled.tr` export const Row = styled.div`
display: flex;
width: 100%;
height: 45px; height: 45px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
:last-child { :first-child {
border-bottom: none; position: sticky;
left: 0;
top: 0;
z-index: 1;
} }
` `
type TdProps = { type TdProps = {
clickable?: boolean, clickable?: boolean,
columnWidth?: number,
headerCell?: boolean,
sorted?: boolean, sorted?: boolean,
} }
export const Td = styled.td<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; font-size: 11px;
text-align: center; color: ${({
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; clickable,
headerCell,
theme,
}) => (clickable && !headerCell ? '#5EB2FF' : theme.colors.white)};
white-space: nowrap;
background-color: #333333;
:first-child { ${Tooltip} {
padding-left: 13px; top: 35px;
text-align: left; }
color: ${({ theme }) => theme.colors.white};
:hover {
${Tooltip} {
display: block;
}
} }
${({ headerCell }) => (headerCell
? ''
: css`
:first-child {
justify-content: unset;
padding-left: 13px;
color: ${({ theme }) => theme.colors.white};
}
`)}
${({ sorted }) => (sorted ${({ sorted }) => (sorted
? css` ? css`
@ -74,13 +135,107 @@ export const Td = styled.td<TdProps>`
` `
: '')} : '')}
${({ clickable }) => (clickable ${({ clickable, headerCell }) => (clickable || headerCell
? css` ? css`
cursor: pointer; cursor: pointer;
` `
: '')} : '')}
` `
type FirstColumnProps = {
columnWidth?: number,
}
export const FirstColumn = styled.div<FirstColumnProps>`
position: sticky;
left: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
`
export const PlayerNum = styled.span` export const PlayerNum = styled.span`
display: inline-block;
width: 20px;
flex-shrink: 0;
text-align: center;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
` `
type PlayerNameProps = {
columnWidth?: number,
}
export const PlayerName = styled.span<PlayerNameProps>`
display: inline-block;
margin-top: 2px;
text-overflow: ellipsis;
overflow: hidden;
${({ columnWidth }) => (columnWidth
? css`
max-width: calc(${columnWidth}px - 31px);
`
: css`
max-width: 110px;
`)}
`
export const PlayerNameWrapper = styled.span`
position: relative;
${Tooltip} {
top: 15px;
}
:hover {
${Tooltip} {
display: block;
}
}
`
const ArrowButton = styled(ArrowButtonBase)`
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, TEAM1,
TEAM2, 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 { 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 { PlayersTable } from '../PlayersTable'
import { TeamsStats } from '../TeamsStats' import { TeamsStatsTable } from '../TeamsStatsTable'
import { import {
Container, Container,
@ -13,22 +22,82 @@ import {
SwitchButton, SwitchButton,
} from './styled' } from './styled'
export const TabStats = () => ( const tabPanes = {
<Container> [Tabs.TEAMS]: TeamsStatsTable,
<Header> [Tabs.TEAM1]: PlayersTable,
<TabList> [Tabs.TEAM2]: PlayersTable,
<Tab selected>Teams</Tab> }
<Tab>DIN</Tab>
<Tab>SPA</Tab> export const TabStats = () => {
</TabList> const {
<Switch> isFinalStatsType,
<SwitchTitle>Final Stats</SwitchTitle> isVisibleTeam1PlayersTab,
<SwitchButton> isVisibleTeam2PlayersTab,
<Tooltip lexic='others' /> isVisibleTeamsTab,
</SwitchButton> selectedTab,
</Switch> setSelectedTab,
</Header> switchTitleLexic,
<PlayersTable /> toggleStatsType,
{/* <TeamsStats /> */} tooltipLexic,
</Container> } = 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 styled, { css } from 'styled-components/macro'
import { TooltipWrapper } from 'features/Tooltip' import { TooltipWrapper } from 'features/Tooltip'
import { T9n } from 'features/T9n'
export const Container = styled.div`` export const Container = styled.div``
@ -14,42 +15,40 @@ export const TabList = styled.div.attrs({ role: 'tablist' })`
display: flex; display: flex;
` `
type TabProps = { export const Tab = styled.button.attrs({ role: 'tab' })`
selected?: boolean,
}
export const Tab = styled.div.attrs(({ selected }: TabProps) => ({
'aria-pressed': selected,
role: 'tab',
}))<TabProps>`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 15px 10px; padding: 0 10px 10px;
font-size: 12px; font-size: 12px;
color: ${({ theme }) => theme.colors.white}; color: ${({ theme }) => theme.colors.white};
opacity: ${({ selected }) => (selected ? '1' : '0.4')}; opacity: 0.4;
cursor: pointer; cursor: pointer;
border: none;
background: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
&[aria-pressed="true"] {
${({ selected, theme }) => (selected opacity: 1;
? css` border-color: currentColor;
border-color: ${theme.colors.white}; }
`
: '')}
` `
export const Switch = styled.div` export const Switch = styled.div`
display: flex; display: flex;
` `
export const SwitchTitle = styled.span` export const SwitchTitle = styled(T9n)`
font-size: 12px; font-size: 12px;
color: ${({ theme }) => theme.colors.white}; 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; position: relative;
width: 20px; width: 20px;
height: 7px; height: 7px;
@ -63,9 +62,12 @@ export const SwitchButton = styled.button`
${TooltipWrapper} { ${TooltipWrapper} {
left: auto; left: auto;
right: 0; right: 0;
top: 15px;
padding: 2px 10px; padding: 2px 10px;
border-radius: 6px; border-radius: 6px;
transform: none; transform: none;
font-size: 11px;
line-height: 1;
::before { ::before {
display: none; display: none;
@ -78,7 +80,7 @@ export const SwitchButton = styled.button`
} }
} }
${({ theme }) => (true // Позже будет добавлен пропс ${({ isFinalStatsType, theme }) => (!isFinalStatsType
? css` ? css`
background-image: linear-gradient( background-image: linear-gradient(
to right, to right,

@ -1,6 +1,11 @@
import { Fragment } from 'react' import {
Fragment,
useMemo,
useRef,
} from 'react'
import size from 'lodash/size' import size from 'lodash/size'
import filter from 'lodash/filter'
import type { import type {
PlaylistOption, PlaylistOption,
@ -10,12 +15,13 @@ import type {
import type { MatchInfo } from 'requests' import type { MatchInfo } from 'requests'
import { DropdownSection } from '../DropdownSection' import { DropdownSection } from '../DropdownSection'
import { MatchPlaylists } from '../MatchPlaylists' import { MatchPlaylists, LIST_ITEM_INDENT } from '../MatchPlaylists'
import { SideInterviews } from '../SideInterviews' import { SideInterviews } from '../SideInterviews'
import { TabVideo } from '../../components/TabVideo' import { Matches } from '../Matches'
type Props = { type Props = {
onSelect: (option: PlaylistOption) => void, onSelect: (option: PlaylistOption) => void,
playListFilter: number,
playlists: Playlists, playlists: Playlists,
profile: MatchInfo, profile: MatchInfo,
selectedPlaylist?: PlaylistOption, selectedPlaylist?: PlaylistOption,
@ -24,31 +30,50 @@ type Props = {
export const TabWatch = ({ export const TabWatch = ({
onSelect, onSelect,
playListFilter,
playlists, playlists,
profile, profile,
selectedPlaylist, selectedPlaylist,
tournamentData, tournamentData,
}: Props) => ( }: Props) => {
<Fragment> const matchPlaylistsRef = useRef<HTMLUListElement>(null)
<MatchPlaylists
playlists={playlists.match} const additionalScrollHeight = (matchPlaylistsRef.current?.clientHeight || 0) + LIST_ITEM_INDENT
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect} const filteredPlayListByDuration = useMemo(() => (
live={profile?.live} filter(playlists.match, (playlist) => (
/> profile?.live
<DropdownSection ? Boolean(playlist.duration) || (playlist.id === 'full_game')
itemsCount={size(playlists.interview)} : Boolean(playlist.duration)
title={playlists.lexics?.interview} ))
> ), [playlists.match, profile?.live])
<SideInterviews
interviews={playlists.interview} return (
selectedMathPlaylist={selectedPlaylist} <Fragment>
onSelect={onSelect} {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> </Fragment>
<TabVideo )
profile={profile} }
tournamentData={tournamentData}
/>
</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 styled, { css } from 'styled-components/macro'
import { customScrollbar } from 'features/Common' import { Name } from 'features/Name'
export const Container = styled.div` export const Container = styled.div`
width: 100%; width: 100%;
font-size: 11px; font-size: 11px;
overflow: hidden;
border-radius: 5px;
background-color: #333333; background-color: #333333;
${customScrollbar}
` `
export const TeamShortName = styled.span` export const TeamShortName = styled(Name)`
color: ${({ theme }) => theme.colors.white}; color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px; letter-spacing: -0.078px;
text-transform: uppercase; text-transform: uppercase;
@ -37,7 +37,10 @@ type TParamValue = {
clickable?: boolean, 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)}; color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)};
${({ clickable }) => (clickable ${({ 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}; color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px; letter-spacing: -0.078px;
text-transform: uppercase; text-transform: uppercase;
font-weight: 600; font-weight: 600;
opacity: 0.5; 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' } from 'react'
import reduce from 'lodash/reduce' import reduce from 'lodash/reduce'
import isEmpty from 'lodash/isEmpty'
import compact from 'lodash/compact'
import { useMatchPageStore } from 'features/MatchPage/store' import { useMatchPageStore } from 'features/MatchPage/store'
@ -14,26 +16,54 @@ export const useMatchSidePlaylists = () => {
const { const {
closePopup, closePopup,
events, events,
isEmptyPlayersStats,
matchPlaylists: playlists, matchPlaylists: playlists,
profile: matchProfile,
teamsStats,
tournamentData,
} = useMatchPageStore() } = useMatchPageStore()
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.WATCH) const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.WATCH)
const isWatchTabVisible = useMemo(() => {
const playListFilter = reduce( const playListFilter = useMemo(() => reduce(
playlists.match, playlists.match,
(acc, item) => { (acc, item) => {
let result = acc let result = acc
if (item.duration) result++ if (item.duration) result++
return result return result
}, },
0, 0,
) ), [playlists.match])
return playListFilter > 1
}, [playlists]) const isWatchTabVisible = useMemo(() => (
playListFilter > 1 || tournamentData.matchDates.length > 1
), [playListFilter, tournamentData.matchDates.length])
const isEventTabVisible = useMemo(() => ( const isEventTabVisible = useMemo(() => (
events.length > 0 events.length > 0
), [events]) ), [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(() => { useEffect(() => {
switch (true) { switch (true) {
case isWatchTabVisible: case isWatchTabVisible:
@ -42,18 +72,32 @@ export const useMatchSidePlaylists = () => {
case isEventTabVisible: case isEventTabVisible:
setSelectedTab(Tabs.EVENTS) setSelectedTab(Tabs.EVENTS)
break break
case isPlayersTabVisible:
setSelectedTab(Tabs.PLAYERS)
break
// case isStatsTabVisible:
// setSelectedTab(Tabs.STATS)
// break
} }
}, [isEventTabVisible, isWatchTabVisible]) }, [
isEventTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
isWatchTabVisible,
])
useEffect(() => { useEffect(() => {
if (selectedTab !== Tabs.EVENTS) closePopup() if (selectedTab !== Tabs.EVENTS) closePopup()
}, [selectedTab, closePopup]) }, [selectedTab, closePopup])
return { return {
hasLessThanFourTabs,
isEventTabVisible, isEventTabVisible,
isStatsTabVisible: true, isPlayersTabVisible,
isStatsTabVisible,
isWatchTabVisible, isWatchTabVisible,
onTabClick: setSelectedTab, onTabClick: setSelectedTab,
playListFilter,
selectedTab, selectedTab,
} }
} }

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

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

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

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

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

@ -22,6 +22,7 @@ export const usePageParams = () => {
return { return {
profileId: Number(pageId), profileId: Number(pageId),
profileType: ProfileTypes[toUpper(profileName) as keyof typeof ProfileTypes], profileType: ProfileTypes[toUpper(profileName) as keyof typeof ProfileTypes],
sportName,
sportType: SportTypes[toUpper(sportName) as keyof typeof SportTypes], 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 './getSubscriptions'
export * from './buySubscription' export * from './buySubscription'
export * from './saveMatchStats' export * from './saveMatchStats'
export * from './getTeamsStats'
export * from './getPlayersStats'
export * from './getMatchParticipants'

Loading…
Cancel
Save