feat(in-141): players stats

pull/23/head
Ruslan Khayrullin 3 years ago
parent ae3e29bcf4
commit 9ac84c779c
  1. 3
      public/images/sortUp.svg
  2. 7
      src/config/routes.tsx
  3. 12
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx
  4. 4
      src/features/MatchSidePlaylists/components/PlayersTable/types.tsx
  5. 22
      src/features/MatchSidePlaylists/components/TabPlayers/index.tsx
  6. 27
      src/features/MatchSidePlaylists/components/TabStats/hooks.tsx
  7. 108
      src/features/MatchSidePlaylists/components/TabStats/index.tsx
  8. 54
      src/features/MatchSidePlaylists/components/TabStats/styled.tsx
  9. 58
      src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx
  10. 6
      src/features/MatchSidePlaylists/components/TeamsStatsTable/types.tsx
  11. 10
      src/features/MatchSidePlaylists/hooks.tsx
  12. 11
      src/features/Name/index.tsx
  13. 3
      src/features/T9n/index.tsx
  14. 5
      src/features/Tooltip/index.tsx
  15. 2
      src/hooks/index.tsx
  16. 5
      src/requests/getMatchParticipants.tsx
  17. 4
      src/requests/getPlayersStats.tsx

@ -0,0 +1,3 @@
<svg width="7" height="6" viewBox="0 0 7 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.06699 0.75C3.25944 0.416667 3.74056 0.416667 3.93301 0.75L6.09808 4.5C6.29053 4.83333 6.04996 5.25 5.66506 5.25L1.33494 5.25C0.950036 5.25 0.709474 4.83333 0.901924 4.5L3.06699 0.75Z" fill="white" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

@ -23,9 +23,16 @@ const VIEWS_APIS = {
staging: 'https://views.test.insports.tv', staging: 'https://views.test.insports.tv',
} }
const STATS_APIS = {
preproduction: 'https://statistic.insports.tv',
production: 'https://statistic.insports.tv',
staging: 'https://statistic-stage.insports.tv',
}
const env = isProduction ? ENV : readSelectedApi() ?? ENV const env = isProduction ? ENV : readSelectedApi() ?? ENV
export const VIEWS_API = VIEWS_APIS[env] export const VIEWS_API = VIEWS_APIS[env]
export const AUTH_SERVICE = APIS[env].auth export const AUTH_SERVICE = APIS[env].auth
export const API_ROOT = APIS[env].api export const API_ROOT = APIS[env].api
export const DATA_URL = `${API_ROOT}/data` export const DATA_URL = `${API_ROOT}/data`
export const STATS_API_URL = STATS_APIS[env]

@ -5,17 +5,20 @@ import { usePlayers } from './usePlayers'
import { useTable } from './useTable' import { useTable } from './useTable'
export const usePlayersTable = ({ teamId }: PlayersTableProps) => { export const usePlayersTable = ({ teamId }: PlayersTableProps) => {
const [sortCondition, setSortCondition] = useState<SortCondition>({ dir: 'asc', paramId: null }) const [sortCondition, setSortCondition] = useState<SortCondition>({
clicksCount: 0,
dir: 'asc',
paramId: null,
})
const { const {
getFullName, getPlayerName,
getPlayerParams, getPlayerParams,
players, players,
} = usePlayers({ sortCondition, teamId }) } = usePlayers({ sortCondition, teamId })
const { const {
containerRef, containerRef,
firstColumnWidth,
getDisplayedValue, getDisplayedValue,
handleScroll, handleScroll,
handleSortClick, handleSortClick,
@ -36,9 +39,8 @@ export const usePlayersTable = ({ teamId }: PlayersTableProps) => {
return { return {
containerRef, containerRef,
firstColumnWidth,
getDisplayedValue, getDisplayedValue,
getFullName, getPlayerName,
getPlayerParams, getPlayerParams,
handleScroll, handleScroll,
handleSortClick, handleSortClick,

@ -1,8 +1,12 @@
import { TCircleAnimation } from 'features/CircleAnimationBar'
export type PlayersTableProps = { export type PlayersTableProps = {
circleAnimation?: TCircleAnimation,
teamId: number, teamId: number,
} }
export type SortCondition = { export type SortCondition = {
clicksCount: number,
dir: 'asc' | 'desc', dir: 'asc' | 'desc',
paramId: number | null, paramId: number | null,
} }

@ -1,5 +1,3 @@
import isEmpty from 'lodash/isEmpty'
import type { Playlists, PlaylistOption } from 'features/MatchPage/types' import type { Playlists, PlaylistOption } from 'features/MatchPage/types'
import type { MatchInfo } from 'requests' import type { MatchInfo } from 'requests'
@ -17,15 +15,11 @@ export const TabPlayers = ({
playlists, playlists,
profile, profile,
selectedPlaylist, selectedPlaylist,
}: Props) => { }: Props) => (
if (isEmpty(playlists.players.team1)) return null <PlayersPlaylists
profile={profile}
return ( players={playlists.players}
<PlayersPlaylists selectedMathPlaylist={selectedPlaylist}
profile={profile} onSelect={onSelect}
players={playlists.players} />
selectedMathPlaylist={selectedPlaylist} )
onSelect={onSelect}
/>
)
}

@ -2,6 +2,8 @@ import { useEffect, useState } from 'react'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import { useTooltip } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store' import { useMatchPageStore } from 'features/MatchPage/store'
import { StatsType, Tabs } from './config' import { StatsType, Tabs } from './config'
@ -12,15 +14,23 @@ export const useTabStats = () => {
const { const {
isEmptyPlayersStats, isEmptyPlayersStats,
profile: matchProfile, profile: matchProfile,
setStatsType,
statsType, statsType,
teamsStats, teamsStats,
toggleStatsType,
} = useMatchPageStore() } = useMatchPageStore()
const {
isTooltipShown,
onMouseLeave,
onMouseOver,
tooltipStyle,
tooltipText,
} = useTooltip()
const isFinalStatsType = statsType === StatsType.FINAL_STATS const isFinalStatsType = statsType === StatsType.FINAL_STATS
const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats' const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats'
const tooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video' const switchButtonTooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video'
const isVisibleTeamsTab = !isEmpty(teamsStats) const isVisibleTeamsTab = !isEmpty(teamsStats)
const isVisibleTeam1PlayersTab = Boolean( const isVisibleTeam1PlayersTab = Boolean(
@ -30,12 +40,6 @@ export const useTabStats = () => {
matchProfile && !isEmptyPlayersStats(matchProfile.team2.id), matchProfile && !isEmptyPlayersStats(matchProfile.team2.id),
) )
const toggleStatsType = () => {
const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS
setStatsType(newStatsType)
}
useEffect(() => { useEffect(() => {
switch (true) { switch (true) {
case isVisibleTeamsTab: case isVisibleTeamsTab:
@ -56,13 +60,18 @@ export const useTabStats = () => {
return { return {
isFinalStatsType, isFinalStatsType,
isTooltipShown,
isVisibleTeam1PlayersTab, isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab, isVisibleTeam2PlayersTab,
isVisibleTeamsTab, isVisibleTeamsTab,
onMouseLeave,
onMouseOver,
selectedTab, selectedTab,
setSelectedTab, setSelectedTab,
switchButtonTooltipLexic,
switchTitleLexic, switchTitleLexic,
toggleStatsType, toggleStatsType,
tooltipLexic, tooltipStyle,
tooltipText,
} }
} }

@ -1,11 +1,17 @@
import { isMobileDevice } from 'config/userAgent' import type { ComponentProps } from 'react'
import { createPortal } from 'react-dom'
import { isMobileDevice } from 'config'
import { getTeamAbbr } from 'helpers' import { getTeamAbbr } from 'helpers'
import { Tooltip } from 'features/Tooltip' import { useModalRoot } from 'hooks'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store' import { useMatchPageStore } from 'features/MatchPage/store'
import { Name } from 'features/Name' import { Name } from 'features/Name'
import { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar'
import { useLexicsStore } from 'features/LexicsStore'
import { Tabs } from './config' import { Tabs } from './config'
import { useTabStats } from './hooks' import { useTabStats } from './hooks'
@ -20,27 +26,44 @@ import {
Switch, Switch,
SwitchTitle, SwitchTitle,
SwitchButton, SwitchButton,
Tooltip,
TabTitle,
} from './styled' } from './styled'
const tabPanes = { const tabPanes = {
[Tabs.TEAMS]: TeamsStatsTable, [Tabs.TEAMS]: TeamsStatsTable,
[Tabs.TEAM1]: PlayersTable, // eslint-disable-next-line react/jsx-props-no-spreading
[Tabs.TEAM2]: PlayersTable, [Tabs.TEAM1]: (props: ComponentProps<typeof PlayersTable>) => <PlayersTable {...props} />,
// eslint-disable-next-line react/jsx-props-no-spreading
[Tabs.TEAM2]: (props: ComponentProps<typeof PlayersTable>) => <PlayersTable {...props} />,
} }
export const TabStats = () => { type Props = {
circleAnimation?: TCircleAnimation,
setCircleAnimation?: TSetCircleAnimation,
}
export const TabStats = ({ circleAnimation, setCircleAnimation }: Props) => {
const { const {
isFinalStatsType, isFinalStatsType,
isTooltipShown,
isVisibleTeam1PlayersTab, isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab, isVisibleTeam2PlayersTab,
isVisibleTeamsTab, isVisibleTeamsTab,
onMouseLeave,
onMouseOver,
selectedTab, selectedTab,
setSelectedTab, setSelectedTab,
switchButtonTooltipLexic,
switchTitleLexic, switchTitleLexic,
toggleStatsType, toggleStatsType,
tooltipLexic, tooltipStyle,
tooltipText,
} = useTabStats() } = useTabStats()
const { profile: matchProfile } = useMatchPageStore() const { profile: matchProfile } = useMatchPageStore()
const { suffix, translate } = useLexicsStore()
const modalRoot = useModalRoot()
const TabPane = tabPanes[selectedTab] const TabPane = tabPanes[selectedTab]
@ -57,7 +80,9 @@ export const TabStats = () => {
aria-pressed={selectedTab === Tabs.TEAMS} aria-pressed={selectedTab === Tabs.TEAMS}
onClick={() => setSelectedTab(Tabs.TEAMS)} onClick={() => setSelectedTab(Tabs.TEAMS)}
> >
<T9n t='team' /> <TabTitle>
<T9n t='team' />
</TabTitle>
</Tab> </Tab>
)} )}
{isVisibleTeam1PlayersTab && ( {isVisibleTeam1PlayersTab && (
@ -65,11 +90,25 @@ export const TabStats = () => {
aria-pressed={selectedTab === Tabs.TEAM1} aria-pressed={selectedTab === Tabs.TEAM1}
onClick={() => setSelectedTab(Tabs.TEAM1)} onClick={() => setSelectedTab(Tabs.TEAM1)}
> >
<Name nameObj={{ <TabTitle
name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng), onMouseOver={isMobileDevice
name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus), ? undefined
}} : onMouseOver({
/> anchorId: 'team1Tab',
horizontalPosition: 'left',
indent: 25,
tooltipText: team1[`name_${suffix}`],
})}
onMouseLeave={isMobileDevice ? undefined : onMouseLeave}
>
<Name
id='team1Tab'
nameObj={{
name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng),
name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus),
}}
/>
</TabTitle>
</Tab> </Tab>
)} )}
{isVisibleTeam2PlayersTab && ( {isVisibleTeam2PlayersTab && (
@ -77,27 +116,56 @@ export const TabStats = () => {
aria-pressed={selectedTab === Tabs.TEAM2} aria-pressed={selectedTab === Tabs.TEAM2}
onClick={() => setSelectedTab(Tabs.TEAM2)} onClick={() => setSelectedTab(Tabs.TEAM2)}
> >
<Name nameObj={{ <TabTitle
name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng), onMouseOver={isMobileDevice
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus), ? undefined
}} : onMouseOver({
/> anchorId: 'team2Tab',
horizontalPosition: 'left',
indent: 25,
tooltipText: team2[`name_${suffix}`],
})}
onMouseLeave={isMobileDevice ? undefined : onMouseLeave}
>
<Name
id='team2Tab'
nameObj={{
name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng),
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus),
}}
/>
</TabTitle>
</Tab> </Tab>
)} )}
</TabList> </TabList>
<Switch> <Switch>
<SwitchTitle t={switchTitleLexic} /> <SwitchTitle t={switchTitleLexic} />
<SwitchButton <SwitchButton
id='switchButton'
isFinalStatsType={isFinalStatsType} isFinalStatsType={isFinalStatsType}
onClick={toggleStatsType} onClick={toggleStatsType}
> onMouseOver={isMobileDevice
{!isMobileDevice && <Tooltip lexic={tooltipLexic} />} ? undefined
</SwitchButton> : onMouseOver({
anchorId: 'switchButton',
horizontalPosition: 'right',
tooltipText: translate(switchButtonTooltipLexic),
})}
onMouseLeave={isMobileDevice ? undefined : onMouseLeave}
/>
</Switch> </Switch>
</Header> </Header>
<TabPane <TabPane
teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id} teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id}
circleAnimation={circleAnimation}
setCircleAnimation={setCircleAnimation}
/> />
{isTooltipShown && modalRoot.current && createPortal(
<Tooltip style={tooltipStyle}>
{tooltipText}
</Tooltip>,
modalRoot.current,
)}
</Container> </Container>
) )
} }

@ -8,29 +8,51 @@ export const Container = styled.div``
export const Header = styled.div` export const Header = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 23px; margin-bottom: 6px;
` `
export const TabList = styled.div.attrs({ role: 'tablist' })` export const TabList = styled.div.attrs({ role: 'tablist' })`
display: flex; display: flex;
` `
export const Tooltip = styled(TooltipWrapper)`
display: block;
padding: 2px 10px;
border-radius: 6px;
transform: none;
font-size: 11px;
line-height: 1;
color: ${({ theme }) => theme.colors.black};
z-index: 999;
::before {
display: none;
}
`
export const TabTitle = styled.span`
color: ${({ theme }) => theme.colors.white};
opacity: 0.4;
`
export const Tab = styled.button.attrs({ role: 'tab' })` export const Tab = styled.button.attrs({ role: 'tab' })`
position: relative;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 10px 10px; padding: 0 10px 10px;
font-size: 12px; font-size: 12px;
color: ${({ theme }) => theme.colors.white};
opacity: 0.4;
cursor: pointer; cursor: pointer;
border: none; border: none;
background: none; background: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
&[aria-pressed="true"] { &[aria-pressed="true"] {
opacity: 1; border-color: ${({ theme }) => theme.colors.white};
border-color: currentColor;
${TabTitle} {
opacity: 1;
}
} }
` `
@ -49,7 +71,6 @@ type SwitchButtonProps = {
} }
export const SwitchButton = styled.button<SwitchButtonProps>` export const SwitchButton = styled.button<SwitchButtonProps>`
position: relative;
width: 20px; width: 20px;
height: 7px; height: 7px;
margin-left: 5px; margin-left: 5px;
@ -59,27 +80,6 @@ export const SwitchButton = styled.button<SwitchButtonProps>`
border: 1px solid ${({ theme }) => theme.colors.white}; border: 1px solid ${({ theme }) => theme.colors.white};
cursor: pointer; cursor: pointer;
${TooltipWrapper} {
left: auto;
right: 0;
top: 15px;
padding: 2px 10px;
border-radius: 6px;
transform: none;
font-size: 11px;
line-height: 1;
::before {
display: none;
}
}
:hover {
${TooltipWrapper} {
display: block;
}
}
${({ isFinalStatsType, theme }) => (!isFinalStatsType ${({ isFinalStatsType, theme }) => (!isFinalStatsType
? css` ? css`
background-image: linear-gradient( background-image: linear-gradient(

@ -1,13 +1,27 @@
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { Name } from 'features/Name' import { Name } from 'features/Name'
import { customScrollbar } from 'features/Common'
export const Container = styled.div` export const Container = styled.div``
export const TableWrapper = styled.div`
width: 100%; width: 100%;
max-height: calc(100vh - 203px);
overflow: auto;
font-size: 11px; font-size: 11px;
overflow: hidden;
border-radius: 5px; border-radius: 5px;
background-color: #333333; background-color: #333333;
${customScrollbar}
`
export const Table = styled.table`
width: 100%;
border-spacing: 0;
border-collapse: collapse;
letter-spacing: -0.078px;
table-layout: fixed;
` `
export const TeamShortName = styled(Name)` export const TeamShortName = styled(Name)`
@ -18,16 +32,44 @@ export const TeamShortName = styled(Name)`
opacity: 0.5; opacity: 0.5;
` `
export const Row = styled.div` export const Cell = styled.td`
display: flex;
justify-content: space-between;
align-items: center;
height: 45px; height: 45px;
padding: 0 12px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
background-color: #333333;
:nth-child(2) {
text-align: center;
}
:first-child, :last-child {
width: 32px;
}
:first-child {
padding-left: 12px;
}
:last-child { :last-child {
border-bottom: none; text-align: right;
padding-right: 12px;
}
`
export const Row = styled.tr`
:last-child:not(:first-child) {
${Cell} {
border-bottom: none;
}
}
`
export const Header = styled.thead`
position: sticky;
top: 0;
z-index: 1;
${Cell} {
background-color: #292929;
} }
` `

@ -0,0 +1,6 @@
import { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar'
export type Props = {
circleAnimation?: TCircleAnimation,
setCircleAnimation?: TSetCircleAnimation,
}

@ -61,7 +61,7 @@ export const useMatchSidePlaylists = () => {
isWatchTabVisible, isWatchTabVisible,
isEventTabVisible, isEventTabVisible,
isPlayersTabVisible, isPlayersTabVisible,
// isStatsTabVisible, isStatsTabVisible,
]).length < 4 ]).length < 4
useEffect(() => { useEffect(() => {
@ -75,14 +75,14 @@ export const useMatchSidePlaylists = () => {
case isPlayersTabVisible: case isPlayersTabVisible:
setSelectedTab(Tabs.PLAYERS) setSelectedTab(Tabs.PLAYERS)
break break
// case isStatsTabVisible: case isStatsTabVisible:
// setSelectedTab(Tabs.STATS) setSelectedTab(Tabs.STATS)
// break break
} }
}, [ }, [
isEventTabVisible, isEventTabVisible,
isPlayersTabVisible, isPlayersTabVisible,
// isStatsTabVisible, isStatsTabVisible,
isWatchTabVisible, isWatchTabVisible,
]) ])

@ -10,6 +10,7 @@ export type ObjectWithName = {
type Props = { type Props = {
className?: string, className?: string,
id?: string,
nameObj: ObjectWithName, nameObj: ObjectWithName,
prefix?: string, prefix?: string,
} }
@ -42,9 +43,17 @@ export const useName = (
export const Name = ({ export const Name = ({
className, className,
id,
nameObj, nameObj,
prefix, prefix,
}: Props) => { }: Props) => {
const name = useName(nameObj, prefix) const name = useName(nameObj, prefix)
return <NameStyled className={className}>{name}</NameStyled> return (
<NameStyled
id={id}
className={className}
>
{name}
</NameStyled>
)
} }

@ -7,6 +7,7 @@ const Text = styled.span``
type Props = { type Props = {
className?: string, className?: string,
id?: string,
onClick?: () => void, onClick?: () => void,
t: LexicsId, t: LexicsId,
values?: Values, values?: Values,
@ -14,6 +15,7 @@ type Props = {
export const T9n = ({ export const T9n = ({
className, className,
id,
onClick, onClick,
t, t,
values, values,
@ -22,6 +24,7 @@ export const T9n = ({
return ( return (
<Text <Text
id={id}
onClick={onClick} onClick={onClick}
className={className} className={className}
> >

@ -34,7 +34,10 @@ export const Tooltip = ({
lexic, lexic,
}: Props) => ( }: Props) => (
<Fragment> <Fragment>
<TooltipWrapper top={0}> <TooltipWrapper
role='tooltip'
top={0}
>
<T9n t={lexic} /> <T9n t={lexic} />
</TooltipWrapper> </TooltipWrapper>
{children} {children}

@ -5,3 +5,5 @@ export * from './useInterval'
export * from './useEventListener' export * from './useEventListener'
export * from './useObjectState' export * from './useObjectState'
export * from './usePageParams' export * from './usePageParams'
export * from './useTooltip'
export * from './useModalRoot'

@ -1,6 +1,6 @@
import isUndefined from 'lodash/isUndefined' import isUndefined from 'lodash/isUndefined'
import { SportTypes } from 'config' import { SportTypes, STATS_API_URL } from 'config'
import { callApi } from 'helpers' import { callApi } from 'helpers'
@ -23,6 +23,7 @@ export type Player = {
national_shirt_num: number, national_shirt_num: number,
nickname_eng: string | null, nickname_eng: string | null,
nickname_rus: string | null, nickname_rus: string | null,
num: number | null,
weight: number | null, weight: number | null,
} }
@ -56,7 +57,7 @@ export const getMatchParticipants = async ({
const response: Response = await callApi({ const response: Response = await callApi({
config, config,
url: `http://136.243.17.103:8888/ask/participants?sport_id=${sportType}&match_id=${matchId}${isUndefined(second) ? '' : `&second=${second}`}`, url: `${STATS_API_URL}/ask/participants?sport_id=${sportType}&match_id=${matchId}${isUndefined(second) ? '' : `&second=${second}`}`,
}) })
if (response.error) Promise.reject(response) if (response.error) Promise.reject(response)

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

Loading…
Cancel
Save