|
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 isAndroid = /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%) |
||||
` |
||||
)} |
||||
` |
||||
@ -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 { |
||||
WATCH, |
||||
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 }) |
||||
} |
||||