parent
7c17307b75
commit
7148a5ec3c
@ -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, |
||||
} |
||||
} |
||||
@ -1 +1,6 @@ |
||||
export const CELL_WIDTH = 47 |
||||
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 |
||||
|
||||
@ -1,61 +0,0 @@ |
||||
import type { SyntheticEvent } from 'react' |
||||
import { |
||||
useRef, |
||||
useState, |
||||
useEffect, |
||||
} from 'react' |
||||
|
||||
import { isMobileDevice } from 'config/userAgent' |
||||
|
||||
import { CELL_WIDTH } from './config' |
||||
|
||||
export const usePlayersTable = () => { |
||||
const containerRef = useRef<HTMLDivElement>(null) |
||||
const tableWrapperRef = useRef<HTMLDivElement>(null) |
||||
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false) |
||||
const [showRightArrow, setShowRightArrow] = useState(false) |
||||
|
||||
const cellWidth = isMobileDevice |
||||
? ((containerRef.current?.clientWidth || 0) - 98) / 4 |
||||
: CELL_WIDTH |
||||
|
||||
const slideLeft = () => tableWrapperRef.current!.scrollBy(-cellWidth, 0) |
||||
const slideRight = () => tableWrapperRef.current!.scrollBy(cellWidth, 0) |
||||
|
||||
const handleScroll = (e: SyntheticEvent<HTMLDivElement>) => { |
||||
const { |
||||
clientWidth, |
||||
scrollLeft, |
||||
scrollWidth, |
||||
} = e.currentTarget |
||||
|
||||
const scrollRight = scrollWidth - (scrollLeft + clientWidth) |
||||
|
||||
setShowLeftArrow(scrollLeft > 1) |
||||
setShowRightArrow(scrollRight > 1) |
||||
} |
||||
|
||||
useEffect(() => { |
||||
const { |
||||
clientWidth = 0, |
||||
scrollLeft = 0, |
||||
scrollWidth = 0, |
||||
} = tableWrapperRef.current || {} |
||||
|
||||
const scrollRight = scrollWidth - (scrollLeft + clientWidth) |
||||
|
||||
setShowRightArrow(scrollRight > 1) |
||||
}, []) |
||||
|
||||
return { |
||||
cellWidth, |
||||
containerRef, |
||||
handleScroll, |
||||
showLeftArrow, |
||||
showRightArrow, |
||||
slideLeft, |
||||
slideRight, |
||||
tableWrapperRef, |
||||
} |
||||
} |
||||
@ -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, |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@ |
||||
export type PlayersTableProps = { |
||||
teamId: number, |
||||
} |
||||
|
||||
export type SortCondition = { |
||||
dir: 'asc' | 'desc', |
||||
paramId: number | null, |
||||
} |
||||
@ -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 || {}) |
||||
} |
||||
Loading…
Reference in new issue