|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 908 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 21 KiB |
@ -1,9 +0,0 @@ |
|||||||
#!/bin/bash |
|
||||||
|
|
||||||
branch=$1 |
|
||||||
composefile=$2 |
|
||||||
|
|
||||||
cd /home/ubuntu/ott-auth |
|
||||||
docker-compose -f $composefile down |
|
||||||
docker-compose -f $composefile up -d |
|
||||||
echo "[>] Deployment done." |
|
||||||
@ -0,0 +1,16 @@ |
|||||||
|
import { |
||||||
|
ClientConfig, |
||||||
|
ClientIds, |
||||||
|
ClientNames, |
||||||
|
} from './types' |
||||||
|
|
||||||
|
import { insports } from './insports' |
||||||
|
|
||||||
|
export const india: ClientConfig = { |
||||||
|
...insports, |
||||||
|
about_the_project: 'https://prsolution.pro', |
||||||
|
auth: { |
||||||
|
clientId: ClientIds.India, |
||||||
|
}, |
||||||
|
name: ClientNames.India, |
||||||
|
} |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
import { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { |
||||||
|
ClientConfig, |
||||||
|
ClientIds, |
||||||
|
ClientNames, |
||||||
|
} from './types' |
||||||
|
|
||||||
|
const randomHash = () => ( |
||||||
|
(Math.random() ** Math.random()) * 9999999999999999 |
||||||
|
) |
||||||
|
|
||||||
|
export const tunis: ClientConfig = { |
||||||
|
auth: { |
||||||
|
clientId: ClientIds.Tunis, |
||||||
|
metaDataUrlParams: `?hash=${randomHash()}`, |
||||||
|
}, |
||||||
|
defaultLanguage: 'fr', |
||||||
|
description: 'Live sports streaming platform. All matches playing under the auspices of Czech Republic FA. Access to full matches, various player playlists, and highlights. Free access in the Czech Republic. Available across all devices', |
||||||
|
disabledPreferences: false, |
||||||
|
name: ClientNames.Tunis, |
||||||
|
privacyLink: '/privacy-policy-and-statement', |
||||||
|
showSearch: false, |
||||||
|
styles: { |
||||||
|
background: '', |
||||||
|
homePageHeader: css` |
||||||
|
background: radial-gradient( |
||||||
|
160.34% 257.27% at -7.45% 162.22%, |
||||||
|
#2AB7AA 3.27%, |
||||||
|
#02505C 43.69%, #0B2E4D 100%); |
||||||
|
`,
|
||||||
|
logo: 'tunis-logo.svg', |
||||||
|
logoHeight: 6.3, |
||||||
|
logoLeft: 1.1, |
||||||
|
logoTop: 1.74, |
||||||
|
logoWidth: 8.25, |
||||||
|
matchLogoHeight: 3.4, |
||||||
|
matchLogoTopMargin: 0.9, |
||||||
|
matchLogoWidth: 4.5, |
||||||
|
matchPageMobileHeaderLogo: css` |
||||||
|
width: 35px; |
||||||
|
height: 25px; |
||||||
|
top: 2px; |
||||||
|
`,
|
||||||
|
mobileHeaderLogo: css` |
||||||
|
width: 48px; |
||||||
|
height: 37px; |
||||||
|
`,
|
||||||
|
userAccountLogo: css` |
||||||
|
width: 4.56rem; |
||||||
|
height: 3.488rem; |
||||||
|
`,
|
||||||
|
}, |
||||||
|
termsLink: '/terms-and-conditions?client_id=facr-ott-web', |
||||||
|
title: 'FACR.TV - The home of Czech football streaming', |
||||||
|
userAccountLinksDisabled: true, |
||||||
|
} |
||||||
@ -1,3 +1,5 @@ |
|||||||
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) |
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) |
||||||
|
|
||||||
|
export const isAndroid = /Android/.test(navigator.userAgent) |
||||||
|
|
||||||
export const isMobileDevice = /iPhone|Android/.test(navigator.userAgent) |
export const isMobileDevice = /iPhone|Android/.test(navigator.userAgent) |
||||||
|
|||||||
@ -0,0 +1,9 @@ |
|||||||
|
import { insports as platformInsports } from 'config/clients/insports' |
||||||
|
|
||||||
|
import type { ClientConfig } from './types' |
||||||
|
import { insports } from './insports' |
||||||
|
|
||||||
|
export const india: ClientConfig = { |
||||||
|
...platformInsports, |
||||||
|
...insports, |
||||||
|
} |
||||||
@ -0,0 +1,67 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { tunis as platformTunis } from 'config/clients/tunis' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config/userAgent' |
||||||
|
import type { ClientConfig } from './types' |
||||||
|
|
||||||
|
const Background = styled.div` |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 100vh; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
background: linear-gradient(0deg, rgba(2, 46, 48, 0.3), |
||||||
|
rgba(2, 46, 48, 0.3)),
|
||||||
|
radial-gradient(152.89% 271.81% at 0% 96.71%, #2AB7AA 3.27%, #02505C 43.69%, #0B2E4D 100%); |
||||||
|
` |
||||||
|
|
||||||
|
export const tunis: ClientConfig = { |
||||||
|
...platformTunis, |
||||||
|
background: Background, |
||||||
|
styles: { |
||||||
|
centerBlock: css` |
||||||
|
margin-top: 9.15rem; |
||||||
|
${isMobileDevice ? css` |
||||||
|
margin-top: 107px; |
||||||
|
@media screen and (orientation: landscape) { |
||||||
|
width: 290px; |
||||||
|
margin: auto; |
||||||
|
} |
||||||
|
` : ''};
|
||||||
|
`,
|
||||||
|
input: css` |
||||||
|
background-color: transparent; |
||||||
|
:not(:last-of-type) { |
||||||
|
border-color: ${({ theme }) => theme.colors.white}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
inputGroup: css` |
||||||
|
border: 1px solid ${({ theme }) => theme.colors.white}; |
||||||
|
`,
|
||||||
|
loader: css` |
||||||
|
color: #0B2E4D; |
||||||
|
`,
|
||||||
|
logo: css` |
||||||
|
background-image: url(/images/tunis_auth_logo.svg); |
||||||
|
width: 200px; |
||||||
|
height: 178px; |
||||||
|
margin-bottom: 1.82rem; |
||||||
|
|
||||||
|
${isMobileDevice ? css` |
||||||
|
margin-bottom: 20px; |
||||||
|
width: 130px; |
||||||
|
height: 100px; |
||||||
|
` : ''}
|
||||||
|
`,
|
||||||
|
popupApplyButton: css` |
||||||
|
background-color: #0E8F84; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
`,
|
||||||
|
popupLoader: '#FFFFFF', |
||||||
|
submitButton: css` |
||||||
|
background-color: ${({ theme }) => theme.colors.white}; |
||||||
|
color: #0B2E4D; |
||||||
|
`,
|
||||||
|
}, |
||||||
|
} |
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
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 |
||||||
|
export const CELL_WIDTH = PARAM_COLUMN_WIDTH |
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,166 @@ |
|||||||
|
import { Fragment } from 'react' |
||||||
|
|
||||||
|
import map from 'lodash/map' |
||||||
|
import includes from 'lodash/includes' |
||||||
|
|
||||||
|
import { PlayerParam } from 'requests' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
import type { PlayersTableProps } from './types' |
||||||
|
import { usePlayersTable } from './hooks' |
||||||
|
import { |
||||||
|
Container, |
||||||
|
TableWrapper, |
||||||
|
Table, |
||||||
|
FirstColumn, |
||||||
|
Cell, |
||||||
|
Row, |
||||||
|
PlayerNum, |
||||||
|
PlayerNameWrapper, |
||||||
|
PlayerName, |
||||||
|
ParamShortTitle, |
||||||
|
ArrowButtonRight, |
||||||
|
ArrowButtonLeft, |
||||||
|
Arrow, |
||||||
|
ExpandButton, |
||||||
|
Tooltip, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export const PlayersTable = (props: PlayersTableProps) => { |
||||||
|
const { |
||||||
|
containerRef, |
||||||
|
firstColumnWidth, |
||||||
|
getDisplayedValue, |
||||||
|
getFullName, |
||||||
|
getPlayerParams, |
||||||
|
handleScroll, |
||||||
|
handleSortClick, |
||||||
|
isExpanded, |
||||||
|
paramColumnWidth, |
||||||
|
params, |
||||||
|
players, |
||||||
|
showExpandButton, |
||||||
|
showLeftArrow, |
||||||
|
showRightArrow, |
||||||
|
slideLeft, |
||||||
|
slideRight, |
||||||
|
sortCondition, |
||||||
|
tableWrapperRef, |
||||||
|
toggleIsExpanded, |
||||||
|
} = usePlayersTable(props) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container |
||||||
|
ref={containerRef} |
||||||
|
isExpanded={isExpanded} |
||||||
|
> |
||||||
|
<TableWrapper |
||||||
|
ref={tableWrapperRef} |
||||||
|
isExpanded={isExpanded} |
||||||
|
onScroll={handleScroll} |
||||||
|
> |
||||||
|
{!isExpanded && ( |
||||||
|
<Fragment> |
||||||
|
{showLeftArrow && ( |
||||||
|
<ArrowButtonLeft |
||||||
|
aria-label='Scroll to left' |
||||||
|
onClick={slideLeft} |
||||||
|
> |
||||||
|
<Arrow direction='left' /> |
||||||
|
</ArrowButtonLeft> |
||||||
|
)} |
||||||
|
{showRightArrow && ( |
||||||
|
<ArrowButtonRight |
||||||
|
aria-label='Scroll to right' |
||||||
|
onClick={slideRight} |
||||||
|
> |
||||||
|
<Arrow direction='right' /> |
||||||
|
</ArrowButtonRight> |
||||||
|
)} |
||||||
|
</Fragment> |
||||||
|
)} |
||||||
|
<FirstColumn columnWidth={firstColumnWidth}> |
||||||
|
<Row> |
||||||
|
<Cell> |
||||||
|
{showExpandButton && ( |
||||||
|
<ExpandButton |
||||||
|
aria-label={isExpanded ? 'Reduce' : 'Expand'} |
||||||
|
onClick={toggleIsExpanded} |
||||||
|
> |
||||||
|
<Arrow direction={isExpanded ? 'right' : 'left'} /> |
||||||
|
<Arrow direction={isExpanded ? 'right' : 'left'} /> |
||||||
|
</ExpandButton> |
||||||
|
)} |
||||||
|
</Cell> |
||||||
|
</Row> |
||||||
|
{map(players, (player) => { |
||||||
|
const fullName = getFullName(player) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Row key={player.id}> |
||||||
|
<Cell> |
||||||
|
<PlayerNum> |
||||||
|
{player.club_shirt_num} |
||||||
|
</PlayerNum>{' '} |
||||||
|
<PlayerNameWrapper> |
||||||
|
<PlayerName columnWidth={firstColumnWidth}> |
||||||
|
{fullName} |
||||||
|
</PlayerName> |
||||||
|
<Tooltip> |
||||||
|
<PlayerName>{fullName}</PlayerName> |
||||||
|
</Tooltip> |
||||||
|
</PlayerNameWrapper> |
||||||
|
</Cell> |
||||||
|
</Row> |
||||||
|
) |
||||||
|
})} |
||||||
|
</FirstColumn> |
||||||
|
<Table> |
||||||
|
<Row> |
||||||
|
{map(params, ({ |
||||||
|
id, |
||||||
|
lexic, |
||||||
|
lexica_short, |
||||||
|
}) => ( |
||||||
|
<Cell |
||||||
|
key={id} |
||||||
|
columnWidth={paramColumnWidth} |
||||||
|
onClick={handleSortClick(id)} |
||||||
|
sorted={sortCondition.paramId === id} |
||||||
|
headerCell |
||||||
|
> |
||||||
|
<ParamShortTitle t={lexica_short || ''} /> |
||||||
|
<Tooltip> |
||||||
|
<T9n t={lexic} /> |
||||||
|
</Tooltip> |
||||||
|
</Cell> |
||||||
|
))} |
||||||
|
</Row> |
||||||
|
|
||||||
|
{map(players, (player) => ( |
||||||
|
<Row key={player.id}> |
||||||
|
{map(params, ({ id }) => { |
||||||
|
const playerParam = getPlayerParams(player.id)[id] as PlayerParam | undefined |
||||||
|
const value = playerParam ? getDisplayedValue(playerParam) : '-' |
||||||
|
const clickable = Boolean(playerParam?.clickable) && !includes([0, '-'], value) |
||||||
|
const sorted = sortCondition.paramId === id |
||||||
|
|
||||||
|
return ( |
||||||
|
<Cell |
||||||
|
columnWidth={paramColumnWidth} |
||||||
|
key={id} |
||||||
|
clickable={clickable} |
||||||
|
sorted={sorted} |
||||||
|
> |
||||||
|
{value} |
||||||
|
</Cell> |
||||||
|
) |
||||||
|
})} |
||||||
|
</Row> |
||||||
|
))} |
||||||
|
</Table> |
||||||
|
</TableWrapper> |
||||||
|
</Container> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,241 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import { customScrollbar } from 'features/Common' |
||||||
|
import { TooltipWrapper } from 'features/Tooltip' |
||||||
|
import { |
||||||
|
ArrowButton as ArrowButtonBase, |
||||||
|
Arrow as ArrowBase, |
||||||
|
} from 'features/HeaderFilters/components/DateFilter/styled' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
type ContainerProps = { |
||||||
|
isExpanded?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Container = styled.div<ContainerProps>` |
||||||
|
${({ isExpanded }) => (isExpanded |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
position: relative; |
||||||
|
`)}
|
||||||
|
` |
||||||
|
|
||||||
|
type TableWrapperProps = { |
||||||
|
isExpanded?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const TableWrapper = styled.div<TableWrapperProps>` |
||||||
|
display: flex; |
||||||
|
max-width: 100%; |
||||||
|
max-height: calc(100vh - 235px); |
||||||
|
border-radius: 5px; |
||||||
|
overflow-x: auto; |
||||||
|
scroll-behavior: smooth; |
||||||
|
background-color: #333333; |
||||||
|
z-index: 50; |
||||||
|
${customScrollbar} |
||||||
|
|
||||||
|
${({ isExpanded }) => (isExpanded |
||||||
|
? css` |
||||||
|
position: absolute; |
||||||
|
right: 14px; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
` |
||||||
|
|
||||||
|
export const Table = styled.div` |
||||||
|
flex-grow: 1; |
||||||
|
border-radius: 5px; |
||||||
|
border-collapse: collapse;
|
||||||
|
letter-spacing: -0.078px; |
||||||
|
` |
||||||
|
|
||||||
|
export const Tooltip = styled(TooltipWrapper)` |
||||||
|
left: auto; |
||||||
|
padding: 2px 10px; |
||||||
|
border-radius: 6px; |
||||||
|
transform: none; |
||||||
|
font-size: 11px; |
||||||
|
line-height: 1; |
||||||
|
color: ${({ theme }) => theme.colors.black}; |
||||||
|
|
||||||
|
::before { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const ParamShortTitle = styled(T9n)` |
||||||
|
text-transform: uppercase; |
||||||
|
` |
||||||
|
|
||||||
|
export const Row = styled.div` |
||||||
|
display: flex; |
||||||
|
width: 100%; |
||||||
|
height: 45px; |
||||||
|
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); |
||||||
|
|
||||||
|
:first-child { |
||||||
|
position: sticky; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
type TdProps = { |
||||||
|
clickable?: boolean, |
||||||
|
columnWidth?: number, |
||||||
|
headerCell?: boolean, |
||||||
|
sorted?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
color: ${({ |
||||||
|
clickable, |
||||||
|
headerCell, |
||||||
|
theme, |
||||||
|
}) => (clickable && !headerCell ? '#5EB2FF' : theme.colors.white)}; |
||||||
|
white-space: nowrap; |
||||||
|
background-color: #333333; |
||||||
|
|
||||||
|
${Tooltip} { |
||||||
|
top: 35px; |
||||||
|
} |
||||||
|
|
||||||
|
:hover { |
||||||
|
${Tooltip} { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
${({ headerCell }) => (headerCell |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
:first-child { |
||||||
|
justify-content: unset; |
||||||
|
padding-left: 13px; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
} |
||||||
|
`)}
|
||||||
|
|
||||||
|
${({ sorted }) => (sorted |
||||||
|
? css` |
||||||
|
font-weight: bold; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
|
||||||
|
${({ clickable, headerCell }) => (clickable || headerCell |
||||||
|
? css` |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
` |
||||||
|
|
||||||
|
type FirstColumnProps = { |
||||||
|
columnWidth?: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const FirstColumn = styled.div<FirstColumnProps>` |
||||||
|
position: sticky; |
||||||
|
left: 0; |
||||||
|
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')}; |
||||||
|
` |
||||||
|
|
||||||
|
export const PlayerNum = styled.span` |
||||||
|
display: inline-block; |
||||||
|
width: 20px; |
||||||
|
flex-shrink: 0; |
||||||
|
text-align: center; |
||||||
|
color: rgba(255, 255, 255, 0.5); |
||||||
|
` |
||||||
|
|
||||||
|
type PlayerNameProps = { |
||||||
|
columnWidth?: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const PlayerName = styled.span<PlayerNameProps>` |
||||||
|
display: inline-block; |
||||||
|
margin-top: 2px; |
||||||
|
text-overflow: ellipsis; |
||||||
|
overflow: hidden; |
||||||
|
|
||||||
|
${({ columnWidth }) => (columnWidth |
||||||
|
? css` |
||||||
|
max-width: calc(${columnWidth}px - 31px); |
||||||
|
` |
||||||
|
: css` |
||||||
|
max-width: 110px; |
||||||
|
`)}
|
||||||
|
` |
||||||
|
|
||||||
|
export const PlayerNameWrapper = styled.span` |
||||||
|
position: relative; |
||||||
|
|
||||||
|
${Tooltip} { |
||||||
|
top: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
:hover { |
||||||
|
${Tooltip} { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
const ArrowButton = styled(ArrowButtonBase)` |
||||||
|
position: absolute; |
||||||
|
width: 17px; |
||||||
|
margin-top: 2px; |
||||||
|
background-color: #333333; |
||||||
|
z-index: 3; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
height: 45px; |
||||||
|
margin-top: 0; |
||||||
|
` |
||||||
|
: ''}; |
||||||
|
` |
||||||
|
|
||||||
|
export const ArrowButtonRight = styled(ArrowButton)` |
||||||
|
right: 0; |
||||||
|
` |
||||||
|
|
||||||
|
export const ArrowButtonLeft = styled(ArrowButton)` |
||||||
|
left: 75px; |
||||||
|
` |
||||||
|
|
||||||
|
export const Arrow = styled(ArrowBase)` |
||||||
|
width: 10px; |
||||||
|
height: 10px; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
border-color: ${({ theme }) => theme.colors.white}; |
||||||
|
` |
||||||
|
: ''};
|
||||||
|
` |
||||||
|
|
||||||
|
export const ExpandButton = styled(ArrowButton)` |
||||||
|
left: 20px; |
||||||
|
top: 0; |
||||||
|
|
||||||
|
${Arrow} { |
||||||
|
left: 0; |
||||||
|
|
||||||
|
:last-child { |
||||||
|
margin-left: 7px; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
export type PlayersTableProps = { |
||||||
|
teamId: number, |
||||||
|
} |
||||||
|
|
||||||
|
export type SortCondition = { |
||||||
|
dir: 'asc' | 'desc', |
||||||
|
paramId: number | null, |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import isEmpty from 'lodash/isEmpty' |
||||||
|
|
||||||
|
import type { Playlists, PlaylistOption } from 'features/MatchPage/types' |
||||||
|
import type { MatchInfo } from 'requests' |
||||||
|
|
||||||
|
import { PlayersPlaylists } from '../PlayersPlaylists' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
onSelect: (option: PlaylistOption) => void, |
||||||
|
playlists: Playlists, |
||||||
|
profile: MatchInfo, |
||||||
|
selectedPlaylist?: PlaylistOption, |
||||||
|
} |
||||||
|
|
||||||
|
export const TabPlayers = ({ |
||||||
|
onSelect, |
||||||
|
playlists, |
||||||
|
profile, |
||||||
|
selectedPlaylist, |
||||||
|
}: Props) => { |
||||||
|
if (isEmpty(playlists.players.team1)) return null |
||||||
|
|
||||||
|
return ( |
||||||
|
<PlayersPlaylists |
||||||
|
profile={profile} |
||||||
|
players={playlists.players} |
||||||
|
selectedMathPlaylist={selectedPlaylist} |
||||||
|
onSelect={onSelect} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
export enum Tabs { |
||||||
|
TEAMS, |
||||||
|
TEAM1, |
||||||
|
TEAM2, |
||||||
|
} |
||||||
|
|
||||||
|
export enum StatsType { |
||||||
|
FINAL_STATS, |
||||||
|
CURRENT_STATS, |
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
import { useEffect, useState } from 'react' |
||||||
|
|
||||||
|
import isEmpty from 'lodash/isEmpty' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
|
||||||
|
import { StatsType, Tabs } from './config' |
||||||
|
|
||||||
|
export const useTabStats = () => { |
||||||
|
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS) |
||||||
|
|
||||||
|
const { |
||||||
|
isEmptyPlayersStats, |
||||||
|
profile: matchProfile, |
||||||
|
setStatsType, |
||||||
|
statsType, |
||||||
|
teamsStats, |
||||||
|
} = useMatchPageStore() |
||||||
|
|
||||||
|
const isFinalStatsType = statsType === StatsType.FINAL_STATS |
||||||
|
|
||||||
|
const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats' |
||||||
|
const tooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video' |
||||||
|
|
||||||
|
const isVisibleTeamsTab = !isEmpty(teamsStats) |
||||||
|
const isVisibleTeam1PlayersTab = Boolean( |
||||||
|
matchProfile && !isEmptyPlayersStats(matchProfile.team1.id), |
||||||
|
) |
||||||
|
const isVisibleTeam2PlayersTab = Boolean( |
||||||
|
matchProfile && !isEmptyPlayersStats(matchProfile.team2.id), |
||||||
|
) |
||||||
|
|
||||||
|
const toggleStatsType = () => { |
||||||
|
const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS |
||||||
|
|
||||||
|
setStatsType(newStatsType) |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
switch (true) { |
||||||
|
case isVisibleTeamsTab: |
||||||
|
setSelectedTab(Tabs.TEAMS) |
||||||
|
break |
||||||
|
|
||||||
|
case isVisibleTeam1PlayersTab: |
||||||
|
setSelectedTab(Tabs.TEAM1) |
||||||
|
break |
||||||
|
|
||||||
|
case isVisibleTeam2PlayersTab: |
||||||
|
setSelectedTab(Tabs.TEAM2) |
||||||
|
break |
||||||
|
|
||||||
|
default: |
||||||
|
} |
||||||
|
}, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab]) |
||||||
|
|
||||||
|
return { |
||||||
|
isFinalStatsType, |
||||||
|
isVisibleTeam1PlayersTab, |
||||||
|
isVisibleTeam2PlayersTab, |
||||||
|
isVisibleTeamsTab, |
||||||
|
selectedTab, |
||||||
|
setSelectedTab, |
||||||
|
switchTitleLexic, |
||||||
|
toggleStatsType, |
||||||
|
tooltipLexic, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
import { isMobileDevice } from 'config/userAgent' |
||||||
|
|
||||||
|
import { getTeamAbbr } from 'helpers' |
||||||
|
|
||||||
|
import { Tooltip } from 'features/Tooltip' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
import { Name } from 'features/Name' |
||||||
|
|
||||||
|
import { Tabs } from './config' |
||||||
|
import { useTabStats } from './hooks' |
||||||
|
import { PlayersTable } from '../PlayersTable' |
||||||
|
import { TeamsStatsTable } from '../TeamsStatsTable' |
||||||
|
|
||||||
|
import { |
||||||
|
Container, |
||||||
|
Header, |
||||||
|
TabList, |
||||||
|
Tab, |
||||||
|
Switch, |
||||||
|
SwitchTitle, |
||||||
|
SwitchButton, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
const tabPanes = { |
||||||
|
[Tabs.TEAMS]: TeamsStatsTable, |
||||||
|
[Tabs.TEAM1]: PlayersTable, |
||||||
|
[Tabs.TEAM2]: PlayersTable, |
||||||
|
} |
||||||
|
|
||||||
|
export const TabStats = () => { |
||||||
|
const { |
||||||
|
isFinalStatsType, |
||||||
|
isVisibleTeam1PlayersTab, |
||||||
|
isVisibleTeam2PlayersTab, |
||||||
|
isVisibleTeamsTab, |
||||||
|
selectedTab, |
||||||
|
setSelectedTab, |
||||||
|
switchTitleLexic, |
||||||
|
toggleStatsType, |
||||||
|
tooltipLexic, |
||||||
|
} = useTabStats() |
||||||
|
const { profile: matchProfile } = useMatchPageStore() |
||||||
|
|
||||||
|
const TabPane = tabPanes[selectedTab] |
||||||
|
|
||||||
|
if (!matchProfile) return null |
||||||
|
|
||||||
|
const { team1, team2 } = matchProfile |
||||||
|
|
||||||
|
return ( |
||||||
|
<Container> |
||||||
|
<Header> |
||||||
|
<TabList> |
||||||
|
{isVisibleTeamsTab && ( |
||||||
|
<Tab |
||||||
|
aria-pressed={selectedTab === Tabs.TEAMS} |
||||||
|
onClick={() => setSelectedTab(Tabs.TEAMS)} |
||||||
|
> |
||||||
|
<T9n t='team' /> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
{isVisibleTeam1PlayersTab && ( |
||||||
|
<Tab |
||||||
|
aria-pressed={selectedTab === Tabs.TEAM1} |
||||||
|
onClick={() => setSelectedTab(Tabs.TEAM1)} |
||||||
|
> |
||||||
|
<Name nameObj={{ |
||||||
|
name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng), |
||||||
|
name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus), |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
{isVisibleTeam2PlayersTab && ( |
||||||
|
<Tab |
||||||
|
aria-pressed={selectedTab === Tabs.TEAM2} |
||||||
|
onClick={() => setSelectedTab(Tabs.TEAM2)} |
||||||
|
> |
||||||
|
<Name nameObj={{ |
||||||
|
name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng), |
||||||
|
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus), |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Tab> |
||||||
|
)} |
||||||
|
</TabList> |
||||||
|
<Switch> |
||||||
|
<SwitchTitle t={switchTitleLexic} /> |
||||||
|
<SwitchButton |
||||||
|
isFinalStatsType={isFinalStatsType} |
||||||
|
onClick={toggleStatsType} |
||||||
|
> |
||||||
|
{!isMobileDevice && <Tooltip lexic={tooltipLexic} />} |
||||||
|
</SwitchButton> |
||||||
|
</Switch> |
||||||
|
</Header> |
||||||
|
<TabPane |
||||||
|
teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id} |
||||||
|
/> |
||||||
|
</Container> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,110 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { TooltipWrapper } from 'features/Tooltip' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
export const Container = styled.div`` |
||||||
|
|
||||||
|
export const Header = styled.div` |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
margin-bottom: 23px; |
||||||
|
` |
||||||
|
|
||||||
|
export const TabList = styled.div.attrs({ role: 'tablist' })` |
||||||
|
display: flex; |
||||||
|
` |
||||||
|
|
||||||
|
export const Tab = styled.button.attrs({ role: 'tab' })` |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
padding: 0 10px 10px; |
||||||
|
font-size: 12px; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
opacity: 0.4; |
||||||
|
cursor: pointer; |
||||||
|
border: none; |
||||||
|
background: none; |
||||||
|
border-bottom: 2px solid transparent; |
||||||
|
|
||||||
|
&[aria-pressed="true"] { |
||||||
|
opacity: 1; |
||||||
|
border-color: currentColor; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Switch = styled.div` |
||||||
|
display: flex; |
||||||
|
` |
||||||
|
|
||||||
|
export const SwitchTitle = styled(T9n)` |
||||||
|
font-size: 12px; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
white-space: nowrap; |
||||||
|
` |
||||||
|
|
||||||
|
type SwitchButtonProps = { |
||||||
|
isFinalStatsType: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const SwitchButton = styled.button<SwitchButtonProps>` |
||||||
|
position: relative; |
||||||
|
width: 20px; |
||||||
|
height: 7px; |
||||||
|
margin-left: 5px; |
||||||
|
margin-top: 5px; |
||||||
|
border-radius: 2px; |
||||||
|
border: none; |
||||||
|
border: 1px solid ${({ theme }) => theme.colors.white}; |
||||||
|
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 |
||||||
|
? css` |
||||||
|
background-image: linear-gradient( |
||||||
|
to right, |
||||||
|
${theme.colors.white} 33.333%, |
||||||
|
${theme.colors.black} 33.333%, |
||||||
|
${theme.colors.black} 66.666%, |
||||||
|
${theme.colors.white} 66.666%, |
||||||
|
${theme.colors.white} 72%, |
||||||
|
${theme.colors.black} 72%, |
||||||
|
${theme.colors.black} 100%) |
||||||
|
` |
||||||
|
: css` |
||||||
|
border-color: transparent; |
||||||
|
background-image: linear-gradient( |
||||||
|
to right, |
||||||
|
${theme.colors.white} 33.333%, |
||||||
|
${theme.colors.black} 33.333%, |
||||||
|
${theme.colors.black} 38%, |
||||||
|
${theme.colors.white} 38%, |
||||||
|
${theme.colors.white} 66.666%, |
||||||
|
${theme.colors.black} 66.666%, |
||||||
|
${theme.colors.black} 72%, |
||||||
|
${theme.colors.white} 72%, |
||||||
|
${theme.colors.white} 100%) |
||||||
|
` |
||||||
|
)} |
||||||
|
` |
||||||
@ -1,53 +1,79 @@ |
|||||||
import { Fragment } from 'react' |
import { |
||||||
|
Fragment, |
||||||
|
useMemo, |
||||||
|
useRef, |
||||||
|
} from 'react' |
||||||
|
|
||||||
import isEmpty from 'lodash/isEmpty' |
|
||||||
import size from 'lodash/size' |
import size from 'lodash/size' |
||||||
|
import filter from 'lodash/filter' |
||||||
|
|
||||||
import type { PlaylistOption, Playlists } from 'features/MatchPage/types' |
import type { |
||||||
|
PlaylistOption, |
||||||
|
Playlists, |
||||||
|
TournamentData, |
||||||
|
} from 'features/MatchPage/types' |
||||||
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 { PlayersPlaylists } from '../PlayersPlaylists' |
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, |
||||||
|
tournamentData: TournamentData, |
||||||
} |
} |
||||||
|
|
||||||
export const TabWatch = ({ |
export const TabWatch = ({ |
||||||
onSelect, |
onSelect, |
||||||
|
playListFilter, |
||||||
playlists, |
playlists, |
||||||
profile, |
profile, |
||||||
selectedPlaylist, |
selectedPlaylist, |
||||||
}: Props) => ( |
tournamentData, |
||||||
<Fragment> |
}: Props) => { |
||||||
<MatchPlaylists |
const matchPlaylistsRef = useRef<HTMLUListElement>(null) |
||||||
playlists={playlists.match} |
|
||||||
selectedMathPlaylist={selectedPlaylist} |
const additionalScrollHeight = (matchPlaylistsRef.current?.clientHeight || 0) + LIST_ITEM_INDENT |
||||||
onSelect={onSelect} |
|
||||||
live={profile?.live} |
const filteredPlayListByDuration = useMemo(() => ( |
||||||
/> |
filter(playlists.match, (playlist) => ( |
||||||
<DropdownSection |
profile?.live |
||||||
itemsCount={size(playlists.interview)} |
? Boolean(playlist.duration) || (playlist.id === 'full_game') |
||||||
title={playlists.lexics?.interview} |
: Boolean(playlist.duration) |
||||||
> |
)) |
||||||
<SideInterviews |
), [playlists.match, profile?.live]) |
||||||
interviews={playlists.interview} |
|
||||||
selectedMathPlaylist={selectedPlaylist} |
return ( |
||||||
onSelect={onSelect} |
<Fragment> |
||||||
/> |
{playListFilter > 1 && ( |
||||||
</DropdownSection> |
<MatchPlaylists |
||||||
{!isEmpty(playlists.players.team1) && ( |
ref={matchPlaylistsRef} |
||||||
<PlayersPlaylists |
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} |
profile={profile} |
||||||
players={playlists.players} |
tournamentData={tournamentData} |
||||||
selectedMathPlaylist={selectedPlaylist} |
additionalScrollHeight={additionalScrollHeight} |
||||||
onSelect={onSelect} |
|
||||||
/> |
/> |
||||||
)} |
</Fragment> |
||||||
</Fragment> |
) |
||||||
) |
} |
||||||
|
|||||||
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { Name } from 'features/Name' |
||||||
|
|
||||||
|
export const Container = styled.div` |
||||||
|
width: 100%; |
||||||
|
font-size: 11px; |
||||||
|
overflow: hidden; |
||||||
|
border-radius: 5px; |
||||||
|
background-color: #333333; |
||||||
|
` |
||||||
|
|
||||||
|
export const TeamShortName = styled(Name)` |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
letter-spacing: -0.078px; |
||||||
|
text-transform: uppercase; |
||||||
|
font-weight: 600; |
||||||
|
opacity: 0.5; |
||||||
|
` |
||||||
|
|
||||||
|
export const Row = styled.div` |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
height: 45px; |
||||||
|
padding: 0 12px; |
||||||
|
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5); |
||||||
|
|
||||||
|
:last-child { |
||||||
|
border-bottom: none; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const ParamValueContainer = styled.div`` |
||||||
|
|
||||||
|
type TParamValue = { |
||||||
|
clickable?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({ |
||||||
|
...clickable && { tabIndex: 0 }, |
||||||
|
}))<TParamValue>` |
||||||
|
font-weight: 600; |
||||||
|
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; |
||||||
|
|
||||||
|
${({ clickable }) => (clickable |
||||||
|
? css` |
||||||
|
cursor: pointer; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
` |
||||||
|
|
||||||
|
export const StatItemTitle = styled.span` |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
letter-spacing: -0.078px; |
||||||
|
text-transform: uppercase; |
||||||
|
font-weight: 600; |
||||||
|
opacity: 0.5; |
||||||
|
` |
||||||
|
|
||||||
|
export const Divider = styled.span` |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
opacity: 0.5; |
||||||
|
font-weight: 600; |
||||||
|
` |
||||||
@ -1,5 +1,6 @@ |
|||||||
export enum Tabs { |
export enum Tabs { |
||||||
WATCH, |
WATCH, |
||||||
EVENTS, |
EVENTS, |
||||||
VIDEO |
STATS, |
||||||
|
PLAYERS, |
||||||
} |
} |
||||||
|
|||||||
@ -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]) |
||||||
|
} |
||||||
@ -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 || {}) |
||||||
|
} |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
import { SportTypes, VIEWS_API } from 'config' |
||||||
|
|
||||||
|
import { callApi } from 'helpers' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
matchId: number, |
||||||
|
matchSecond: number, |
||||||
|
sportType: SportTypes, |
||||||
|
} |
||||||
|
|
||||||
|
export const VIEW_INTERVAL_MS = 5000 |
||||||
|
|
||||||
|
export const saveMatchStats = ({ |
||||||
|
matchId, |
||||||
|
matchSecond, |
||||||
|
sportType, |
||||||
|
}: Props) => { |
||||||
|
const url = `${VIEWS_API}/user/view` |
||||||
|
|
||||||
|
const config = { |
||||||
|
body: { |
||||||
|
interval: VIEW_INTERVAL_MS / 1000, |
||||||
|
match_id: matchId, |
||||||
|
second: matchSecond, |
||||||
|
sport_id: sportType, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return callApi({ config, url }) |
||||||
|
} |
||||||