feat(in-424): statistics tour

pull/148/head
Ruslan Khayrullin 3 years ago
parent e48b890fd7
commit c4403ea089
  1. 45
      package-lock.json
  2. 2
      package.json
  3. 9
      src/components/Overlay/index.tsx
  4. 1
      src/config/index.tsx
  5. 3
      src/config/keyboardKeys.tsx
  6. 17
      src/config/lexics/indexLexics.tsx
  7. 11
      src/features/MatchPage/components/MatchDescription/index.tsx
  8. 32
      src/features/MatchPage/index.tsx
  9. 37
      src/features/MatchPage/store/hooks/index.tsx
  10. 3513
      src/features/MatchPage/store/hooks/useFakeData.tsx
  11. 59
      src/features/MatchPage/store/hooks/usePlayersStats.tsx
  12. 16
      src/features/MatchPage/store/hooks/useStatsTab.tsx
  13. 63
      src/features/MatchPage/store/hooks/useTeamsStats.tsx
  14. 15
      src/features/MatchPage/styled.tsx
  15. 2
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx
  16. 42
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx
  17. 42
      src/features/MatchSidePlaylists/components/PlayersTable/index.tsx
  18. 42
      src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx
  19. 13
      src/features/MatchSidePlaylists/components/TabStats/hooks.tsx
  20. 25
      src/features/MatchSidePlaylists/components/TabStats/index.tsx
  21. 33
      src/features/MatchSidePlaylists/components/TabStats/styled.tsx
  22. 28
      src/features/MatchSidePlaylists/components/TeamsStatsTable/Cell.tsx
  23. 2
      src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx
  24. 13
      src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx
  25. 15
      src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx
  26. 76
      src/features/MatchSidePlaylists/index.tsx
  27. 46
      src/features/MatchSidePlaylists/styled.tsx
  28. 198
      src/features/MatchTour/TourProvider.tsx
  29. 161
      src/features/MatchTour/components/ContentComponent/hooks.tsx
  30. 123
      src/features/MatchTour/components/ContentComponent/index.tsx
  31. 158
      src/features/MatchTour/components/ContentComponent/styled.tsx
  32. 100
      src/features/MatchTour/components/Spotlight/index.tsx
  33. 2
      src/features/MatchTour/components/index.tsx
  34. 13
      src/features/MatchTour/config.tsx
  35. 3
      src/features/MatchTour/index.tsx
  36. 15
      src/features/MultiSourcePlayer/hooks/index.tsx
  37. 29
      src/features/PageLayout/styled.tsx
  38. 15
      src/features/StreamPlayer/hooks/index.tsx
  39. 267
      src/helpers/bodyScroll/index.tsx
  40. 1
      src/helpers/index.tsx
  41. 1
      src/hooks/index.tsx
  42. 16
      src/hooks/useEventListener.tsx
  43. 2
      src/requests/getMatchParticipants.tsx
  44. 2
      src/requests/getPlayersStats.tsx
  45. 4
      src/requests/getTeamsStats.tsx

45
package-lock.json generated

@ -2982,6 +2982,41 @@
"react-lifecycles-compat": "^3.0.4"
}
},
"@reactour/mask": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@reactour/mask/-/mask-1.0.5.tgz",
"integrity": "sha512-SMakvPUsH83j4MAq87jBMpdzoVQ+amZTQ6rYsDBqN1Hcz+A8JN9IDgaA49UaLcRPZq+ioQrmos8mBTe8uEtPeQ==",
"requires": {
"@reactour/utils": "*"
}
},
"@reactour/popover": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@reactour/popover/-/popover-1.0.5.tgz",
"integrity": "sha512-d6BMcyXGj3RdSc2huiU6v/wG2XG1ad+lAFmjyFerlZNS1ccp/49HvLUnqxE0Td+86e7RPrdEzpZb5PKtBybPrA==",
"requires": {
"@reactour/utils": "*"
}
},
"@reactour/tour": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@reactour/tour/-/tour-3.3.0.tgz",
"integrity": "sha512-Dx/jDKEZ29fSOmnc07zCgHS6lmEKCNreyvFhhPQTI1OAG6MTTWuQJhKUmrytECncFJb+oH95zvE9mf137rSBtA==",
"requires": {
"@reactour/mask": "*",
"@reactour/popover": "*",
"@reactour/utils": "*"
}
},
"@reactour/utils": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/@reactour/utils/-/utils-0.4.7.tgz",
"integrity": "sha512-d+/Xhi2nKCc6OrDEFGg15iN8ZyWDTdOrwIKkndJXrnWiN6b+nqoS2Tb7hZvt79rqCyUsQxekUndPFPQyIW81EA==",
"requires": {
"@rooks/use-mutation-observer": "^4.11.2",
"resize-observer-polyfill": "^1.5.1"
}
},
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -3030,6 +3065,11 @@
}
}
},
"@rooks/use-mutation-observer": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@rooks/use-mutation-observer/-/use-mutation-observer-4.11.2.tgz",
"integrity": "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A=="
},
"@rushstack/eslint-patch": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
@ -24912,6 +24952,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",

@ -4,6 +4,7 @@
"private": true,
"scripts": {
"start": "react-scripts start",
"start-https": "export HTTPS=true&&SSL_CRT_FILE=cert.pem&&SSL_KEY_FILE=key.pem react-scripts start",
"build": "GENERATE_SOURCEMAP=false react-scripts build && gzipper --verbose ./build",
"test": "react-scripts test --testMatch '**/__tests__/*' --passWithNoTests --watchAll=false",
"test:watch": "react-scripts test --testMatch '**/__tests__/*'",
@ -21,6 +22,7 @@
"insports": "REACT_APP_CLIENT=insports react-scripts start"
},
"dependencies": {
"@reactour/tour": "^3.3.0",
"@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.13.2",
"babel-polyfill": "^6.26.0",

@ -0,0 +1,9 @@
import styled from 'styled-components/macro'
export const Overlay = styled.div`
position: fixed;
inset: 0;
opacity: 0.6;
background-color: ${({ theme }) => theme.colors.black};
z-index: 9999;
`

@ -11,3 +11,4 @@ export * from './env'
export * from './userAgent'
export * from './queries'
export * from './keyboardKeys'
export * from './clients'

@ -1,3 +1,6 @@
export enum KEYBOARD_KEYS {
ArrowLeft = 'ArrowLeft',
ArrowRight = 'ArrowRight',
Enter = 'Enter',
Esc = 'Escape',
}

@ -7,12 +7,20 @@ import { sportsLexic } from './sportsLexic'
const matchPopupLexics = {
actions: 1020,
apply: 13491,
back: 696,
blue_stats_are_clickable: 20071,
check_out_the_stats: 20066,
choose_fav_team: 19776,
click_to_see_full_time_stats: 20074,
click_to_see_stats_in_real_time: 20075,
click_to_watch_playlist: 20072,
commentators: 15424,
compare_teams_across_multiple_metrics: 20070,
continue_watching: 20007,
current_stats: 19592,
display_all_stats: 19932,
display_stats_according_to_video: 19931,
end_tour: 20076,
episode_duration: 13410,
events: 1020,
final_stats: 19591,
@ -24,9 +32,11 @@ const matchPopupLexics = {
go_back_to_match: 13405,
group: 7850,
half_time: 1033,
here_you_will_discover_tons: 20067,
languages: 15030,
match_interviews: 13031,
match_settings: 13490,
next_step: 15156,
no_data: 15397,
other_games: 19997,
others: 19902,
@ -38,12 +48,18 @@ const matchPopupLexics = {
sec_60: 20006,
sec_after: 13412,
sec_before: 13411,
see_interactive_game_stats: 20069,
selected_player_actions: 13413,
show_less_stats: 20064,
show_more_stats: 20063,
sign_in: 20003,
sign_in_full_game: 20004,
skip_tour: 20065,
start_tour: 20062,
started_streaming_at: 16042,
stats: 18179,
streamed_live_on: 16043,
team_players_stats: 20073,
video: 1017,
views: 13440,
watch: 818,
@ -51,6 +67,7 @@ const matchPopupLexics = {
watch_live_stream: 13020,
watch_players_episodes: 14052,
watching_now: 16041,
welcom_to_stats_tab: 20068,
}
const filterPopup = {

@ -2,6 +2,8 @@ import { useCallback } from 'react'
import { useQuery } from 'react-query'
import { useTour } from '@reactour/tour'
import { format } from 'date-fns'
import includes from 'lodash/includes'
@ -28,6 +30,8 @@ import { usePageParams } from 'hooks/usePageParams'
import { getMatchScore } from 'requests'
import { Steps } from 'features/MatchTour'
import {
Description,
DescriptionInnerBlock,
@ -48,6 +52,7 @@ export const MatchDescription = () => {
const { isScoreHidden } = useMatchSwitchesStore()
const { suffix } = useLexicsStore()
const { profile, profileCardShown } = useMatchPageStore()
const { isOpen } = useTour()
const getTeamName = useCallback((team: Team) => (
isMobileDevice
@ -68,6 +73,8 @@ export const MatchDescription = () => {
refetchInterval: 5000,
})
if (isOpen && !isMobileDevice) return null
if (!profile) return <Description />
const {
@ -88,7 +95,9 @@ export const MatchDescription = () => {
return (
<Description isHidden={!profileCardShown}>
<DescriptionInnerBlock>
<Title>
<Title
data-step={isMobileDevice && isOpen ? Steps.Start : undefined}
>
<StyledLink
id={team1.id}
profileType={ProfileTypes.TEAMS}

@ -1,6 +1,8 @@
import { useEffect } from 'react'
import { useHistory } from 'react-router'
import { useTour } from '@reactour/tour'
import { useTheme } from 'styled-components'
import { ProfileHeader } from 'features/ProfileHeader'
@ -13,20 +15,24 @@ import {
import { FavoritesActions } from 'requests'
import { ProfileTypes } from 'config'
import { client } from 'config/clients'
import { isIOS } from 'config/userAgent'
import {
ProfileTypes,
isIOS,
client,
} from 'config'
import { usePageLogger } from 'hooks/usePageLogger'
import { usePageParams } from 'hooks/usePageParams'
import { usePageLogger, usePageParams } from 'hooks'
import { checkUrlParams } from 'helpers/parseUrlParams/parseUrlParams'
import { TourProvider } from 'features/MatchTour'
import { MatchPageStore, useMatchPageStore } from './store'
import { SubscriptionGuard } from './components/SubscriptionGuard'
import { LiveMatch } from './components/LiveMatch'
import { Wrapper } from './styled'
import { FinishedMatch } from './components/FinishedMatch'
import { FavouriteTeamPopup } from './components/FavouriteTeam'
import { Wrapper } from './styled'
const MatchPageComponent = () => {
usePageLogger()
@ -39,6 +45,7 @@ const MatchPageComponent = () => {
profile,
user,
} = useMatchPageStore()
const isFavorite = profile && userFavorites?.find((fav) => fav.id === profile?.tournament.id)
const {
@ -46,6 +53,8 @@ const MatchPageComponent = () => {
sportType,
} = usePageParams()
const { isOpen } = useTour()
useEffect(() => {
let timer = 0
timer = window.setTimeout(() => {
@ -91,12 +100,15 @@ const MatchPageComponent = () => {
}
return (
<PageWrapper isIOS={isIOS}>
<PageWrapper
isIOS={isIOS}
isTourOpen={Boolean(isOpen)}
>
<ProfileHeader color={colors.matchHeaderBackground} height={client.name === 'facr' ? 5 : 4.5} />
<Main>
<UserFavorites />
<SubscriptionGuard>
<Wrapper>
<Wrapper isTourOpen={Boolean(isOpen)}>
{playFromOTT && (
<LiveMatch />
)}
@ -118,7 +130,9 @@ const MatchPageComponent = () => {
const MatchPage = () => (
<MatchPageStore>
<MatchPageComponent />
<TourProvider>
<MatchPageComponent />
</TourProvider>
</MatchPageStore>
)

@ -12,6 +12,8 @@ import { useAuthStore } from 'features/AuthStore'
import { Tabs } from 'features/MatchSidePlaylists/config'
import { initialCircleAnimation } from 'features/CircleAnimationBar'
import type { TCircleAnimation } from 'features/CircleAnimationBar'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour'
import { PAGES } from 'config/pages'
@ -24,6 +26,7 @@ import { usePageParams, useToggle } from 'hooks'
import { redirectToUrl } from 'helpers/redirectToUrl'
import { parseDate } from 'helpers/parseDate'
import { setLocalStorageItem } from 'helpers/getLocalStorage'
import { useTournamentData } from './useTournamentData'
import { useMatchData } from './useMatchData'
@ -192,19 +195,25 @@ export const useMatchPage = () => {
const {
circleAnimation: statsCircleAnimation,
filteredEvents: statsFilteredEvents,
isExpanded,
isPlayersStatsFetching,
isPlayFilterEpisodes: isStatsPlayFilterEpisodes,
isTeamsStatsFetching,
plaingOrder: statsPlaingOrder,
playEpisodes: playStatsEpisodes,
playNextEpisode: playStatsNextEpisode,
reduceTable,
selectedStatsTable,
setCircleAnimation: setStatsCircleAnimation,
setIsPlayersStatsFetching,
setIsPlayingFiltersEpisodes: setStatsIsPlayinFiltersEpisodes,
setIsTeamsStatsFetching,
setPlaingOrder: setStatsPlaingOrder,
setSelectedStatsTable,
setStatsType,
setWatchAllEpisodesTimer: setStatsWatchAllEpisodesTimer,
statsType,
toggleIsExpanded,
toggleStatsType,
watchAllEpisodesTimer: statsWatchAllEpisodesTimer,
} = useStatsTab({
@ -214,7 +223,12 @@ export const useMatchPage = () => {
selectedPlaylist,
})
const { teamsStats } = useTeamsStats({
const {
beforeCloseTourCallback: beforeCloseTourCallbackTeams,
getFirstClickableParam,
isClickable,
teamsStats,
} = useTeamsStats({
matchProfile,
playingProgress,
selectedPlaylist,
@ -223,6 +237,8 @@ export const useMatchPage = () => {
})
const {
beforeCloseTourCallback: beforeCloseTourCallbackPlayers,
getParams,
isEmptyPlayersStats,
playersData,
playersStats,
@ -234,6 +250,15 @@ export const useMatchPage = () => {
statsType,
})
const beforeCloseTourCallback = () => {
beforeCloseTourCallbackPlayers()
beforeCloseTourCallbackTeams()
setStatsType(profile?.live ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS)
isExpanded && toggleIsExpanded()
setLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY, 'true')
}
const isStarted = useMemo(() => (
profile?.date
? parseDate(profile.date) < new Date()
@ -310,17 +335,22 @@ export const useMatchPage = () => {
allActionsToggle,
allPlayersToggle,
applyFilters,
beforeCloseTourCallback,
circleAnimation: isStatsTab ? statsCircleAnimation : circleAnimation,
closePopup,
countOfFilters,
disablePlayingEpisodes,
events,
filteredEvents: isStatsTab ? statsFilteredEvents : filteredEvents,
getFirstClickableParam,
getParams,
handlePlaylistClick,
hideProfileCard,
isAllActionsChecked,
isClickable,
isEmptyFilters,
isEmptyPlayersStats,
isExpanded,
isFirstTeamPlayersChecked,
isLiveMatch,
isOpenFiltersPopup,
@ -342,8 +372,10 @@ export const useMatchPage = () => {
playingProgress,
profile,
profileCardShown,
reduceTable,
reversedGroupEvents,
selectedPlaylist,
selectedStatsTable,
selectedTab,
setCircleAnimation: isStatsTab ? setStatsCircleAnimation : setCircleAnimation,
setFullMatchPlaylistDuration,
@ -354,7 +386,9 @@ export const useMatchPage = () => {
setPlayingData,
setPlayingProgress,
setReversed,
setSelectedStatsTable,
setSelectedTab,
setStatsType,
setUnreversed,
setWatchAllEpisodesTimer: isStatsTab ? setStatsWatchAllEpisodesTimer : setWatchAllEpisodesTimer,
showProfileCard,
@ -362,6 +396,7 @@ export const useMatchPage = () => {
teamsStats,
toggleActiveEvents,
toggleActivePlayers,
toggleIsExpanded,
togglePopup,
toggleStatsType,
tournamentData,

File diff suppressed because it is too large Load Diff

@ -3,6 +3,7 @@ import {
useMemo,
useEffect,
useState,
useCallback,
} from 'react'
import { useQueryClient } from 'react-query'
@ -11,10 +12,14 @@ import isEmpty from 'lodash/isEmpty'
import every from 'lodash/every'
import find from 'lodash/find'
import isUndefined from 'lodash/isUndefined'
import flatMapDepth from 'lodash/flatMapDepth'
import uniqBy from 'lodash/uniqBy'
import values from 'lodash/values'
import size from 'lodash/size'
import { querieKeys } from 'config'
import type { MatchScore } from 'requests'
import type { MatchScore, PlayerParam } from 'requests'
import {
MatchInfo,
PlayersStats,
@ -23,12 +28,20 @@ import {
getMatchParticipants,
} from 'requests'
import { getLocalStorageItem } from 'helpers/getLocalStorage'
import { useObjectState, usePageParams } from 'hooks'
import type{ PlaylistOption } from 'features/MatchPage/types'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists'
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime'
import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour'
import { DISPLAYED_PARAMS_COLUMNS } from 'features/MatchSidePlaylists/components/PlayersTable/config'
import { useFakeData } from './useFakeData'
type HeaderParam = Pick<PlayerParam, 'id' | 'lexica_short' | 'lexic'>
const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000
@ -62,6 +75,8 @@ export const usePlayersStats = ({
sportType,
} = usePageParams()
const fakeData = useFakeData(matchProfile)
const client = useQueryClient()
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore)
@ -74,6 +89,10 @@ export const usePlayersStats = ({
|| isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2'])
)
const getParams = useCallback((stats: PlayersStats) => (
uniqBy(flatMapDepth(stats, values), 'id') as unknown as Record<string, HeaderParam>
), [])
const fetchPlayers = useMemo(() => throttle(async (second?: number) => {
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds
@ -116,12 +135,11 @@ export const usePlayersStats = ({
} catch (e) {
return Promise.reject(e)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [
matchId,
matchScore?.video_bounds,
matchProfile,
sportName,
matchProfile?.team1.id,
matchProfile?.team2.id,
matchId,
])
const fetchData = useMemo(() => throttle(async (second?: number) => {
@ -139,14 +157,29 @@ export const usePlayersStats = ({
const team1Players = find(res1, { team_id: matchProfile?.team1.id })?.players || []
const team2Players = find(res1, { team_id: matchProfile?.team2.id })?.players || []
const needUseFakeData = getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) !== 'true'
&& (
isEmpty(team1Players)
|| isEmpty(res2)
|| size(getParams(res2)) <= DISPLAYED_PARAMS_COLUMNS
)
setPlayersData({
team1: team1Players,
team2: team2Players,
team1: needUseFakeData ? fakeData.playersData[0].players : team1Players,
team2: needUseFakeData ? fakeData.playersData[1].players : team2Players,
})
setPlayersStats({
...(matchProfile?.team1.id && res2 && { [matchProfile.team1.id]: res2 }),
...(matchProfile?.team2.id && res3 && { [matchProfile.team2.id]: res3 }),
...(matchProfile?.team1.id && res2 && {
[matchProfile.team1.id]: needUseFakeData
? fakeData.playersStats as unknown as PlayersStats
: res2,
}),
...(matchProfile?.team2.id && res3 && {
[matchProfile.team2.id]: needUseFakeData
? fakeData.playersStats as unknown as PlayersStats
: res3,
}),
})
setIsPlayersStatsFetching(false)
@ -160,8 +193,14 @@ export const usePlayersStats = ({
matchProfile?.live,
matchProfile?.c_match_calc_status,
setIsPlayersStatsFetching,
getParams,
fakeData,
])
const beforeCloseTourCallback = () => {
isCurrentStats ? fetchData(playingProgress) : fetchData()
}
useEffect(() => {
let interval: NodeJS.Timeout
@ -194,6 +233,8 @@ export const usePlayersStats = ({
])
return {
beforeCloseTourCallback,
getParams,
isEmptyPlayersStats,
playersData,
playersStats,

@ -14,7 +14,8 @@ import type { EventPlaylistOption, PlaylistOption } from 'features/MatchPage/typ
import type { TCircleAnimation } from 'features/CircleAnimationBar'
import { initialCircleAnimation } from 'features/CircleAnimationBar'
import { PlaylistTypes } from 'features/MatchPage/types'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
import { StatsType, Tabs } from 'features/MatchSidePlaylists/components/TabStats/config'
import { useToggle } from 'hooks'
type UseStatsTabArgs = {
disablePlayingEpisodes: () => void,
@ -55,6 +56,13 @@ export const useStatsTab = ({
const [isPlayFilterEpisodes, setIsPlayingFiltersEpisodes] = useState(false)
const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false)
const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation)
const [selectedStatsTable, setSelectedStatsTable] = useState<Tabs>(Tabs.TEAMS)
const {
close: reduceTable,
isOpen: isExpanded,
toggle: toggleIsExpanded,
} = useToggle()
const isFinalStatsType = statsType === StatsType.FINAL_STATS
@ -125,19 +133,25 @@ export const useStatsTab = ({
return {
circleAnimation,
filteredEvents,
isExpanded,
isPlayFilterEpisodes,
isPlayersStatsFetching,
isTeamsStatsFetching,
plaingOrder,
playEpisodes,
playNextEpisode,
reduceTable,
selectedStatsTable,
setCircleAnimation,
setIsPlayersStatsFetching,
setIsPlayingFiltersEpisodes,
setIsTeamsStatsFetching,
setPlaingOrder,
setSelectedStatsTable,
setStatsType,
setWatchAllEpisodesTimer,
statsType,
toggleIsExpanded,
toggleStatsType,
watchAllEpisodesTimer,
}

@ -3,23 +3,35 @@ import {
useEffect,
useState,
useMemo,
useCallback,
} from 'react'
import { useQueryClient } from 'react-query'
import throttle from 'lodash/throttle'
import isUndefined from 'lodash/isUndefined'
import find from 'lodash/find'
import isEmpty from 'lodash/isEmpty'
import { querieKeys } from 'config'
import type { MatchInfo, MatchScore } from 'requests'
import type {
MatchInfo,
MatchScore,
Param,
} from 'requests'
import { getTeamsStats, TeamStatItem } from 'requests'
import { usePageParams } from 'hooks'
import { getLocalStorageItem } from 'helpers/getLocalStorage'
import type { PlaylistOption } from 'features/MatchPage/types'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists'
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime'
import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour'
import { useFakeData } from './useFakeData'
const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000
@ -32,6 +44,10 @@ type UseTeamsStatsArgs = {
statsType: StatsType,
}
type TeamsStats = {
[teamId: number]: Array<TeamStatItem>,
}
export const useTeamsStats = ({
matchProfile,
playingProgress,
@ -39,18 +55,42 @@ export const useTeamsStats = ({
setIsTeamsStatsFetching,
statsType,
}: UseTeamsStatsArgs) => {
const [teamsStats, setTeamsStats] = useState<{
[teamId: string]: Array<TeamStatItem>,
}>({})
const [teamsStats, setTeamsStats] = useState<TeamsStats>({})
const { profileId: matchId, sportName } = usePageParams()
const fakeData = useFakeData(matchProfile)
const client = useQueryClient()
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore)
const isCurrentStats = statsType === StatsType.CURRENT_STATS
const isClickable = (param: Param) => (
Boolean(param.val) && param.clickable
)
const getFirstClickableParam = useCallback((stats: TeamsStats) => {
if (isEmpty(stats)) return null
const statItem = (matchProfile?.team1.id && find(
stats[matchProfile.team1.id],
({ param1, param2 }) => isClickable(param1) || Boolean(param2 && isClickable(param1)),
)) || (matchProfile?.team2.id && find(
stats[matchProfile.team2.id],
({ param1, param2 }) => isClickable(param1) || Boolean(param2 && isClickable(param1)),
))
if (!statItem) return null
if (isClickable(statItem.param1)) return statItem.param1
return statItem.param2 && isClickable(statItem.param2)
? statItem.param2
: null
}, [matchProfile?.team1.id, matchProfile?.team2.id])
const fetchTeamsStats = useMemo(() => throttle(async (second?: number) => {
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds
@ -68,7 +108,11 @@ export const useTeamsStats = ({
...(!isUndefined(second) && getHalfTime(videoBounds, second)),
})
setTeamsStats(data)
const needUseFakeData = getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) !== 'true' && !getFirstClickableParam(data)
const stats = needUseFakeData ? fakeData.teamsStats : data
setTeamsStats(stats)
setIsTeamsStatsFetching(false)
// eslint-disable-next-line no-empty
@ -82,8 +126,14 @@ export const useTeamsStats = ({
matchId,
setIsTeamsStatsFetching,
sportName,
fakeData,
getFirstClickableParam,
])
const beforeCloseTourCallback = () => {
isCurrentStats ? fetchTeamsStats(playingProgress) : fetchTeamsStats()
}
useEffect(() => {
let interval: NodeJS.Timeout
@ -107,6 +157,9 @@ export const useTeamsStats = ({
}, [fetchTeamsStats, playingProgress, isCurrentStats])
return {
beforeCloseTourCallback,
getFirstClickableParam,
isClickable,
statsType,
teamsStats,
}

@ -1,9 +1,12 @@
import styled, { css } from 'styled-components/macro'
import { devices } from 'config/devices'
import { isMobileDevice } from 'config/userAgent'
import { isMobileDevice, devices } from 'config'
export const Wrapper = styled.div`
type WrapperProps = {
isTourOpen?: boolean,
}
export const Wrapper = styled.div<WrapperProps>`
width: 100%;
height: calc(100vh - 115px);
margin: 20px 0px 0 10px;
@ -24,6 +27,12 @@ export const Wrapper = styled.div`
}
`
: ''};
${({ isTourOpen }) => (isTourOpen && isMobileDevice
? css`
padding: 0 5px;
`
: '')};
`
export const Container = styled.div`

@ -30,6 +30,7 @@ export const usePlayersTable = ({ teamId }: PlayersTableProps) => {
isExpanded,
paramColumnWidth,
params,
paramsCount,
showExpandButton,
showLeftArrow,
showRightArrow,
@ -60,6 +61,7 @@ export const usePlayersTable = ({ teamId }: PlayersTableProps) => {
isExpanded,
paramColumnWidth,
params,
paramsCount,
players,
showExpandButton,
showLeftArrow,

@ -16,20 +16,15 @@ import { useQueryClient } from 'react-query'
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 map from 'lodash/map'
import { isMobileDevice, querieKeys } from 'config'
import type {
PlayerParam,
PlayersStats,
MatchScore,
} from 'requests'
import type { PlayerParam, MatchScore } from 'requests'
import { getStatsEvents } from 'requests'
import { usePageParams, useToggle } from 'hooks'
import { usePageParams } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsConfig } from 'features/LexicsStore'
@ -63,47 +58,27 @@ export const useTable = ({
const [paramColumnWidth, setParamColumnWidth] = useState(PARAM_COLUMN_WIDTH_DEFAULT)
const {
close: reduceTable,
isOpen: isExpanded,
toggle: toggleIsExpanded,
} = useToggle()
const {
getParams,
isExpanded,
playersStats,
playingProgress,
playStatsEpisodes,
profile,
reduceTable,
setIsPlayingFiltersEpisodes,
setPlayingData,
setWatchAllEpisodesTimer,
statsType,
toggleIsExpanded,
} = useMatchPageStore()
const { profileId, sportType } = usePageParams()
const client = useQueryClient()
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore)
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 params = useMemo(() => getParams(playersStats[teamId]), [getParams, playersStats, teamId])
const lexics = useMemo(() => (
reduce<HeaderParam, Array<number>>(
@ -251,6 +226,7 @@ export const useTable = ({
isExpanded,
paramColumnWidth,
params,
paramsCount,
showExpandButton: !isMobileDevice && paramsCount > DISPLAYED_PARAMS_COLUMNS,
showLeftArrow,
showRightArrow,

@ -1,5 +1,7 @@
import { Fragment } from 'react'
import { useTour } from '@reactour/tour'
import map from 'lodash/map'
import includes from 'lodash/includes'
@ -11,9 +13,14 @@ import { useLexicsStore } from 'features/LexicsStore'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Loader } from 'features/Loader'
import { defaultTheme } from 'features/Theme/config'
import { Spotlight, Steps } from 'features/MatchTour'
import type { PlayersTableProps } from './types'
import { FIRST_COLUMN_WIDTH_DEFAULT, FIRST_COLUMN_WIDTH_EXPANDED } from './config'
import {
DISPLAYED_PARAMS_COLUMNS,
FIRST_COLUMN_WIDTH_DEFAULT,
FIRST_COLUMN_WIDTH_EXPANDED,
} from './config'
import { usePlayersTable } from './hooks'
import { Cell } from './Cell'
import {
@ -44,6 +51,7 @@ export const PlayersTable = (props: PlayersTableProps) => {
isExpanded,
paramColumnWidth,
params,
paramsCount,
players,
showExpandButton,
showLeftArrow,
@ -61,6 +69,7 @@ export const PlayersTable = (props: PlayersTableProps) => {
playingData,
watchAllEpisodesTimer,
} = useMatchPageStore()
const { currentStep, isOpen } = useTour()
const firstColumnWidth = isExpanded ? FIRST_COLUMN_WIDTH_EXPANDED : FIRST_COLUMN_WIDTH_DEFAULT
@ -75,21 +84,28 @@ export const PlayersTable = (props: PlayersTableProps) => {
<TableWrapper
ref={tableWrapperRef}
isExpanded={isExpanded}
isOpenTour={Boolean(isOpen)}
onScroll={handleScroll}
>
{!isExpanded && (
{!isExpanded && paramsCount > DISPLAYED_PARAMS_COLUMNS && (
<Fragment>
{showRightArrow && (
<ArrowButtonRight
aria-label='Scroll to right'
onClick={slideRight}
>
<Arrow direction='right' />
</ArrowButtonRight>
)}
<ArrowButtonRight
aria-label='Scroll to right'
onClick={slideRight}
visible={showRightArrow}
>
<Arrow direction='right' data-step={Steps.ShowMoreStats} />
{Boolean(currentStep === Steps.ShowMoreStats && isOpen) && (
<Spotlight />
)}
</ArrowButtonRight>
</Fragment>
)}
<Table role='marquee' aria-live='off'>
<Table
role='marquee'
aria-live='off'
fullWidth={paramsCount <= DISPLAYED_PARAMS_COLUMNS}
>
<Header>
<Row>
<Cell
@ -109,9 +125,13 @@ export const PlayersTable = (props: PlayersTableProps) => {
isExpanded={isExpanded}
aria-label={isExpanded ? 'Reduce' : 'Expand'}
onClick={toggleIsExpanded}
data-step={Steps.ShowLessStats}
>
<Arrow direction={isExpanded ? 'right' : 'left'} />
<Arrow direction={isExpanded ? 'right' : 'left'} />
{Boolean(currentStep === Steps.ShowLessStats && isOpen) && (
<Spotlight />
)}
</ExpandButton>
)}
</Cell>

@ -29,6 +29,7 @@ export const Container = styled.div<ContainerProps>`
type TableWrapperProps = {
isExpanded?: boolean,
isOpenTour?: boolean,
}
export const TableWrapper = styled.div<TableWrapperProps>`
@ -54,6 +55,19 @@ export const TableWrapper = styled.div<TableWrapperProps>`
`
: '')}
${({ isOpenTour }) => (isOpenTour
? css`
clip-path: none;
`
: '')}
${({ isExpanded, isOpenTour }) => (isOpenTour && isExpanded
? css`
overflow-x: initial;
`
: '')}
${isMobileDevice
? ''
: css`
@ -67,12 +81,23 @@ export const TableWrapper = styled.div<TableWrapperProps>`
: ''};
`
export const Table = styled.table`
type TableProps = {
fullWidth?: boolean,
}
export const Table = styled.table<TableProps>`
border-radius: 5px;
border-spacing: 0;
border-collapse: collapse;
letter-spacing: -0.078px;
table-layout: fixed;
${({ fullWidth }) => (fullWidth
? css`
width: 100%;
`
: '')
}
`
type ParamShortTitleProps = {
@ -251,7 +276,11 @@ const ArrowButton = styled(ArrowButtonBase)`
: ''};
`
export const ArrowButtonRight = styled(ArrowButton)`
type ArrowButtonRightProps = {
visible?: boolean,
}
export const ArrowButtonRight = styled(ArrowButton)<ArrowButtonRightProps>`
right: 0;
border-top-right-radius: 5px;
@ -259,6 +288,13 @@ export const ArrowButtonRight = styled(ArrowButton)`
left: auto;
right: 7px;
}
${({ visible }) => (!visible
? css`
visibility: hidden;
`
: '')
}
`
export const ArrowButtonLeft = styled(ArrowButton)`
@ -276,7 +312,7 @@ export const ExpandButton = styled(ArrowButton)<ExpandButtonProps>`
${Arrow} {
left: ${({ isExpanded }) => (isExpanded ? -6 : -2)}px;
:last-child {
:last-of-type {
margin-left: 7px;
}
}

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import isEmpty from 'lodash/isEmpty'
@ -9,11 +9,11 @@ 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,
selectedStatsTable: selectedTab,
setSelectedStatsTable: setSelectedTab,
statsType,
teamsStats,
toggleStatsType,
@ -56,7 +56,12 @@ export const useTabStats = () => {
default:
}
}, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab])
}, [
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
setSelectedTab,
])
return {
isFinalStatsType,

@ -1,6 +1,10 @@
import type { ComponentProps } from 'react'
import { createPortal } from 'react-dom'
import { useTour } from '@reactour/tour'
import includes from 'lodash/includes'
import { isMobileDevice } from 'config'
import { getTeamAbbr } from 'helpers'
@ -11,6 +15,7 @@ import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Name } from 'features/Name'
import { useLexicsStore } from 'features/LexicsStore'
import { Spotlight, Steps } from 'features/MatchTour'
import { Tabs } from './config'
import { useTabStats } from './hooks'
@ -59,6 +64,8 @@ export const TabStats = () => {
const modalRoot = useModalRoot()
const { currentStep, isOpen } = useTour()
const TabPane = tabPanes[selectedTab]
if (!matchProfile) return null
@ -73,16 +80,21 @@ export const TabStats = () => {
<Tab
aria-pressed={selectedTab === Tabs.TEAMS}
onClick={() => setSelectedTab(Tabs.TEAMS)}
data-step={Steps.TeamsTab}
>
<TabTitle>
<T9n t='team' />
</TabTitle>
{Boolean(currentStep === Steps.TeamsTab && isOpen) && (
<Spotlight />
)}
</Tab>
)}
{isVisibleTeam1PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM1}
onClick={() => setSelectedTab(Tabs.TEAM1)}
data-step={Steps.PlayersTab}
>
<TabTitle
teamColor={team1.shirt_color}
@ -104,6 +116,9 @@ export const TabStats = () => {
}}
/>
</TabTitle>
{Boolean(currentStep === Steps.PlayersTab && isOpen) && (
<Spotlight />
)}
</Tab>
)}
{isVisibleTeam2PlayersTab && (
@ -134,7 +149,15 @@ export const TabStats = () => {
</Tab>
)}
</TabList>
<Switch>
<Switch
data-step={Steps.FinalStats}
highlighted={Boolean(isOpen) && includes(
isMobileDevice
? [Steps.ShowLessStats, Steps.FinalStats]
: [Steps.FinalStats, Steps.CurrentStats],
currentStep,
)}
>
<SwitchTitle t={switchTitleLexic} />
<SwitchButton
id='switchButton'

@ -1,5 +1,7 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { TooltipWrapper } from 'features/Tooltip'
import { T9n } from 'features/T9n'
@ -62,7 +64,7 @@ export const TabTitle = styled.span<TabTitleProps>`
export const Tab = styled.button.attrs({ role: 'tab' })`
position: relative;
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
padding: 0 10px 10px;
font-size: 12px;
@ -78,10 +80,36 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
color: ${({ theme }) => theme.colors.white};
}
}
${isMobileDevice
? css`
width: 20vw;
`
: ''
}
`
export const Switch = styled.div`
type SwitchProps = {
highlighted?: boolean,
}
export const Switch = styled.div<SwitchProps>`
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 27px;
margin-top: -5px;
padding: 0 5px;
border: 1px solid transparent;
${({ highlighted }) => (highlighted
? css`
border-radius: 6px;
border-color: #0057FF;
background: radial-gradient(50% 50% at 50% 50%, rgba(0, 87, 255, 0) 70.25%, rgba(0, 87, 255, 0.4) 100%);
`
: '')}
`
export const SwitchTitle = styled(T9n)`
@ -98,7 +126,6 @@ export const SwitchButton = styled.button<SwitchButtonProps>`
width: 20px;
height: 7px;
margin-left: 5px;
margin-top: 5px;
border-radius: 2px;
border: none;
border: 1px solid ${({ theme }) => theme.colors.white};

@ -1,6 +1,8 @@
import { Fragment, useRef } from 'react'
import { useQueryClient } from 'react-query'
import { useTour } from '@reactour/tour'
import isNumber from 'lodash/isNumber'
import { KEYBOARD_KEYS, querieKeys } from 'config'
@ -16,6 +18,7 @@ import { usePageParams, useEventListener } from 'hooks'
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Spotlight, Steps } from 'features/MatchTour'
import { StatsType } from '../TabStats/config'
import { CircleAnimationBar } from '../CircleAnimationBar'
@ -28,19 +31,22 @@ import {
} from './styled'
type CellProps = {
firstClickableParam: Param | null,
teamId: number,
teamStatItem: TeamStatItem | null,
}
export const Cell = ({
firstClickableParam,
teamId,
teamStatItem,
}: CellProps) => {
const paramValueContainerRef = useRef(null)
const paramValueContainerRef = useRef<HTMLDivElement>(null)
const { profileId, sportType } = usePageParams()
const {
isClickable,
playingData,
playingProgress,
playStatsEpisodes,
@ -52,14 +58,12 @@ export const Cell = ({
watchAllEpisodesTimer,
} = useMatchPageStore()
const { currentStep, isOpen } = useTour()
const client = useQueryClient()
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore)
const isClickable = (param: Param) => (
Boolean(param.val) && param.clickable
)
const getDisplayedValue = (val: number | null) => (
isNumber(val) ? String(val) : '-'
)
@ -137,8 +141,15 @@ export const Cell = ({
onClick={() => onParamClick(teamStatItem.param1)}
data-param-id={teamStatItem.param1.id}
hasValue={Boolean(teamStatItem.param1.val)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...firstClickableParam === teamStatItem.param1 && {
'data-step': Steps.ClickToWatchPlaylist,
}}
>
{getDisplayedValue(teamStatItem.param1.val)}
{firstClickableParam === teamStatItem.param1
&& Boolean(currentStep === Steps.ClickToWatchPlaylist && isOpen)
&& <Spotlight />}
</ParamValue>
)}
@ -163,8 +174,15 @@ export const Cell = ({
onClick={() => onParamClick(teamStatItem.param2!)}
data-param-id={teamStatItem.param2.id}
hasValue={Boolean(teamStatItem.param2.val)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...firstClickableParam === teamStatItem.param2 && {
'data-step': Steps.ClickToWatchPlaylist,
}}
>
{getDisplayedValue(teamStatItem.param2.val)}
{firstClickableParam === teamStatItem.param2
&& Boolean(currentStep === Steps.ClickToWatchPlaylist && isOpen)
&& <Spotlight />}
</ParamValue>
</Fragment>
)}

@ -6,6 +6,7 @@ import { useMatchPageStore } from 'features/MatchPage/store'
export const useTeamsStatsTable = () => {
const {
getFirstClickableParam,
plaingOrder,
profile,
setCircleAnimation,
@ -26,6 +27,7 @@ export const useTeamsStatsTable = () => {
}, [setCircleAnimation, plaingOrder])
return {
firstClickableParam: getFirstClickableParam(teamsStats),
getStatItemById,
}
}

@ -1,3 +1,5 @@
import { useTour } from '@reactour/tour'
import map from 'lodash/map'
import { useMatchPageStore } from 'features/MatchPage/store'
@ -25,10 +27,15 @@ export const TeamsStatsTable = () => {
teamsStats,
} = useMatchPageStore()
const { getStatItemById } = useTeamsStatsTable()
const {
firstClickableParam,
getStatItemById,
} = useTeamsStatsTable()
const { shortSuffix } = useLexicsStore()
const { isOpen } = useTour()
if (!profile) return null
if (isTeamsStatsFetching) {
@ -39,7 +46,7 @@ export const TeamsStatsTable = () => {
return (
<Container>
<TableWrapper>
<TableWrapper isOpenTour={Boolean(isOpen)}>
<Table role='marquee' aria-live='off'>
<Header>
<Row>
@ -69,6 +76,7 @@ export const TeamsStatsTable = () => {
<Cell
teamStatItem={team1StatItem}
teamId={profile.team1.id}
firstClickableParam={firstClickableParam}
/>
<CellContainer>
@ -78,6 +86,7 @@ export const TeamsStatsTable = () => {
<Cell
teamStatItem={team2StatItem}
teamId={profile.team2.id}
firstClickableParam={firstClickableParam}
/>
</Row>
)

@ -7,7 +7,11 @@ import { customScrollbar } from 'features/Common'
export const Container = styled.div``
export const TableWrapper = styled.div`
type TableWrapperProps = {
isOpenTour?: boolean,
}
export const TableWrapper = styled.div<TableWrapperProps>`
width: 100%;
overflow: auto;
font-size: 11px;
@ -20,6 +24,13 @@ export const TableWrapper = styled.div`
max-height: calc(100vh - 203px);
`};
${({ isOpenTour }) => (isOpenTour
? css`
clip-path: none;
overflow: initial;
`
: '')}
${customScrollbar}
`
@ -90,11 +101,11 @@ type TParamValue = {
export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({
...clickable && { tabIndex: 0 },
}))<TParamValue>`
position: relative;
display: inline-block;
width: 15px;
height: 15px;
text-align: center;
position: relative;
font-weight: ${({ clickable }) => (clickable ? 700 : 400)};
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)};

@ -3,14 +3,26 @@ import {
useRef,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import { useTour } from '@reactour/tour'
import type { PlaylistOption } from 'features/MatchPage/types'
import { useMatchPageStore } from 'features/MatchPage/store'
import {
Spotlight,
Steps,
TOUR_COMPLETED_STORAGE_KEY,
} from 'features/MatchTour'
import { Overlay } from 'components/Overlay'
import { useEventListener } from 'hooks'
import { useEventListener, useModalRoot } from 'hooks'
import { isIOS } from 'config/userAgent'
import { getLocalStorageItem } from 'helpers/getLocalStorage'
import { Tabs } from './config'
import { TabEvents } from './components/TabEvents'
import { TabWatch } from './components/TabWatch'
@ -25,6 +37,7 @@ import {
TabIcon,
TabTitle,
Container,
TabButton,
} from './styled'
const tabPanes = {
@ -61,6 +74,14 @@ export const MatchSidePlaylists = ({
onTabClick,
} = useMatchSidePlaylists()
const {
currentStep,
isOpen,
setIsOpen,
} = useTour()
const modalRoot = useModalRoot()
const TabPane = tabPanes[selectedTab]
const containerRef = useRef<HTMLDivElement | null>(null)
@ -82,6 +103,19 @@ export const MatchSidePlaylists = ({
tabPaneContainerRef.current?.clientHeight,
])
useEffect(() => {
if (
getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) === 'true'
|| isOpen
|| !isStatsTabVisible
|| Number(profile?.c_match_calc_status) < 2
) return undefined
const timer = setTimeout(() => setIsOpen(true), 1500)
return () => clearTimeout(timer)
}, [isStatsTabVisible, setIsOpen, profile?.c_match_calc_status, isOpen])
useEventListener({
callback: () => {
const screenLandscape = isIOS ? window.orientation : window.screen.orientation.type
@ -98,7 +132,12 @@ export const MatchSidePlaylists = ({
})
return (
<Wrapper ref={containerRef}>
<Wrapper
ref={containerRef}
data-step={Steps.Welcome}
highlighted={Boolean(isOpen) && currentStep === Steps.Welcome}
isTourOpen={Boolean(isOpen)}
>
<TabsWrapper>
<TabsGroup hasLessThanFourTabs={hasLessThanFourTabs}>
{isWatchTabVisible ? (
@ -106,8 +145,10 @@ export const MatchSidePlaylists = ({
aria-pressed={selectedTab === Tabs.WATCH}
onClick={() => onTabClick(Tabs.WATCH)}
>
<TabIcon icon='watch' />
<TabTitle t='watch' />
<TabButton>
<TabIcon icon='watch' />
<TabTitle t='watch' />
</TabButton>
</Tab>
) : null}
{isEventTabVisible ? (
@ -115,8 +156,10 @@ export const MatchSidePlaylists = ({
aria-pressed={selectedTab === Tabs.EVENTS}
onClick={() => onTabClick(Tabs.EVENTS)}
>
<TabIcon icon='plays' />
<TabTitle t='actions' />
<TabButton>
<TabIcon icon='plays' />
<TabTitle t='actions' />
</TabButton>
</Tab>
) : null}
{isPlayersTabVisible ? (
@ -124,17 +167,25 @@ export const MatchSidePlaylists = ({
aria-pressed={selectedTab === Tabs.PLAYERS}
onClick={() => onTabClick(Tabs.PLAYERS)}
>
<TabIcon icon='players' />
<TabTitle t='players' />
<TabButton>
<TabIcon icon='players' />
<TabTitle t='players' />
</TabButton>
</Tab>
) : null}
{isStatsTabVisible ? (
<Tab
aria-pressed={selectedTab === Tabs.STATS}
onClick={() => onTabClick(Tabs.STATS)}
data-step={Steps.Start}
>
<TabIcon icon='stats' />
<TabTitle t='stats' />
{Boolean(currentStep === Steps.Start && isOpen) && (
<Spotlight />
)}
<TabButton>
<TabIcon icon='stats' />
<TabTitle t='stats' />
</TabButton>
</Tab>
) : null}
</TabsGroup>
@ -144,6 +195,7 @@ export const MatchSidePlaylists = ({
hasScroll={hasTabPaneScroll}
ref={tabPaneContainerRef}
forWatchTab={selectedTab === Tabs.WATCH}
highlighted={Boolean(isOpen)}
>
<TabPane
tournamentData={tournamentData}
@ -153,6 +205,10 @@ export const MatchSidePlaylists = ({
selectedPlaylist={selectedPlaylist}
/>
</Container>
{modalRoot.current && isOpen && createPortal(
<Overlay />,
modalRoot.current,
)}
</Wrapper>
)
}

@ -9,8 +9,22 @@ import {
import { customScrollbar } from 'features/Common'
import { T9n } from 'features/T9n'
export const Wrapper = styled.div`
type WrapperProps = {
highlighted?: boolean,
isTourOpen?: boolean,
}
export const Wrapper = styled.div<WrapperProps>`
padding-right: 14px;
padding-top: 10px;
${({ highlighted }) => (highlighted
? css`
border: 1px solid #0057FF;
border-radius: 5px;
box-shadow: 0px 0px 66px 16px rgba(0, 87, 255, 0.8);
`
: '')}
${isMobileDevice
? css`
@ -21,6 +35,13 @@ export const Wrapper = styled.div`
${customScrollbar}
`
: ''};
${({ isTourOpen }) => (isTourOpen
? css`
overflow-y: initial;
z-index: 9999;
`
: '')}
`
export const TabsWrapper = styled.div``
@ -36,8 +57,6 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
${({ hasLessThanFourTabs }) => (hasLessThanFourTabs
? css`
padding-top: 10px;
${Tab} {
justify-content: center;
flex-direction: row;
@ -58,7 +77,7 @@ export const TabTitle = styled(T9n)`
color: ${({ theme }) => theme.colors.white};
`
export const Tab = styled.button.attrs({ role: 'tab' })`
export const TabButton = styled.button`
display: flex;
flex-direction: column;
justify-content: space-between;
@ -69,12 +88,18 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
cursor: pointer;
border: none;
background: none;
`
export const Tab = styled.div.attrs({ role: 'tab' })`
position: relative;
&[aria-pressed="true"], :hover {
opacity: 1;
${TabButton} {
opacity: 1;
${TabTitle} {
font-weight: 600;
${TabTitle} {
font-weight: 600;
}
}
}
@ -107,6 +132,7 @@ export const TabIcon = styled.div<TabIconProps>`
type TContainer = {
forWatchTab?: boolean,
hasScroll: boolean,
highlighted?: boolean,
}
export const Container = styled.div<TContainer>`
@ -140,6 +166,12 @@ export const Container = styled.div<TContainer>`
}
`
: ''};
${({ highlighted }) => (highlighted
? css`
overflow-y: initial;
`
: '')}
`
type ButtonProps = {

@ -0,0 +1,198 @@
import type {
PropsWithChildren,
ComponentProps,
CSSProperties,
} from 'react'
import { Fragment } from 'react'
import compact from 'lodash/compact'
import type { StepType } from '@reactour/tour'
import { TourProvider as TourProviderLib } from '@reactour/tour'
import { isMobileDevice } from 'config'
import { disableBodyScroll, enableBodyScroll } from 'helpers'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Steps } from './config'
import {
ContentComponent,
Body,
BodyText,
Title,
} from './components/ContentComponent'
const getPopoverStyle = (base: CSSProperties): CSSProperties => ({
...base,
borderRadius: 6,
lineHeight: 1,
maxWidth: 'auto',
padding: 20,
width: 340,
...isMobileDevice && {
padding: '20px 25px',
textAlign: 'center',
width: '95vw',
},
})
const getPopoverPosition = (baseCoords: [number, number]): [number, number] => (
isMobileDevice ? [0.001, 0.001] : baseCoords
)
const getSelector = (step: Steps) => (
isMobileDevice ? `[data-step="${Steps.Start}"]` : `[data-step="${step}"]`
)
const steps: Array<StepType> = compact([
{
content: (
<Fragment>
<Title
alignLeft
t='check_out_the_stats'
/>
<Body>
<BodyText t='here_you_will_discover_tons' />
</Body>
</Fragment>
),
padding: {
popover: getPopoverPosition([15, 10]),
},
selector: getSelector(Steps.Start),
styles: {
popover: (base) => ({
...getPopoverStyle(base),
textAlign: 'left',
}),
},
},
{
content: (
<Fragment>
<Title t='welcom_to_stats_tab' />
<Body>
<BodyText t='see_interactive_game_stats' />
</Body>
</Fragment>
),
padding: {
popover: getPopoverPosition([5, 0.001]),
},
selector: getSelector(Steps.Welcome),
styles: {
popover: (base) => ({
...getPopoverStyle(base),
...isMobileDevice && {
padding: '20px 0',
},
}),
},
},
{
content: (
<Fragment>
<Title t='compare_teams_across_multiple_metrics' />
<Body>
<BodyText t='blue_stats_are_clickable' />
</Body>
</Fragment>
),
padding: {
popover: getPopoverPosition([8, 20]),
},
selector: getSelector(Steps.TeamsTab),
},
{
content: (
<Title t='click_to_watch_playlist' />
),
selector: getSelector(Steps.ClickToWatchPlaylist),
},
{
content: (
<Title t='team_players_stats' />
),
padding: {
popover: getPopoverPosition([10, 17]),
},
selector: getSelector(Steps.PlayersTab),
},
{
content: (
<Title t='show_more_stats' />
),
selector: getSelector(Steps.ShowMoreStats),
},
!isMobileDevice && {
content: (
<Title t='show_less_stats' />
),
padding: {
popover: [20, 10],
},
selector: getSelector(Steps.ShowLessStats),
},
{
content: (
<Title t='click_to_see_full_time_stats' />
),
padding: {
popover: getPopoverPosition([10, 0.001]),
},
selector: getSelector(Steps.FinalStats),
},
{
content: (
<Title t='click_to_see_stats_in_real_time' />
),
padding: {
popover: getPopoverPosition([10, 0.001]),
},
selector: getSelector(Steps.FinalStats),
},
])
const styles: ComponentProps<typeof TourProviderLib>['styles'] = {
maskWrapper: () => ({
display: 'none',
}),
popover: getPopoverStyle,
}
const padding: ComponentProps<typeof TourProviderLib>['padding'] = {
popover: isMobileDevice ? [0.001, 0.001] : [15, 25],
}
export const TourProvider = ({ children }: PropsWithChildren<{}>) => {
const { beforeCloseTourCallback } = useMatchPageStore()
const afterOpen = (target: Element | null) => {
target && disableBodyScroll(target)
}
const beforeClose = (target: Element | null) => {
target && enableBodyScroll(target)
beforeCloseTourCallback()
}
return (
<TourProviderLib
steps={steps}
ContentComponent={ContentComponent}
showDots={false}
afterOpen={afterOpen}
beforeClose={beforeClose}
position={isMobileDevice ? 'top' : 'left'}
padding={padding}
disableInteraction
disableKeyboardNavigation
styles={styles}
>
{children}
</TourProviderLib>
)
}

@ -0,0 +1,161 @@
import {
useEffect,
useRef,
useMemo,
useCallback,
} from 'react'
import throttle from 'lodash/throttle'
import type { PopoverContentProps } from '@reactour/tour'
import { isMobileDevice, KEYBOARD_KEYS } from 'config'
import { useEventListener } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Tabs } from 'features/MatchSidePlaylists/config'
import { StatsType, Tabs as StatTabs } from 'features/MatchSidePlaylists/components/TabStats/config'
import { Steps } from '../../config'
const KEY_PRESS_DELAY = 1500
export const useContentComponent = ({
currentStep,
setCurrentStep,
setIsOpen,
steps,
}: PopoverContentProps) => {
const {
setSelectedStatsTable,
setSelectedTab,
setStatsType,
toggleIsExpanded,
} = useMatchPageStore()
const timerRef = useRef<NodeJS.Timeout>()
const back = useCallback(() => {
switch (currentStep) {
case Steps.Start:
case Steps.Welcome:
return
case Steps.PlayersTab:
setSelectedStatsTable(StatTabs.TEAMS)
break
case Steps.ShowLessStats:
if (!isMobileDevice) {
toggleIsExpanded()
}
break
case Steps.FinalStats:
if (isMobileDevice) {
setStatsType(StatsType.FINAL_STATS)
} else {
toggleIsExpanded()
}
break
case Steps.CurrentStats:
setStatsType(StatsType.FINAL_STATS)
break
default:
}
timerRef.current = setTimeout(() => setCurrentStep((step) => step - 1), 0)
}, [
currentStep,
setCurrentStep,
setSelectedStatsTable,
setStatsType,
toggleIsExpanded,
])
const next = useCallback(() => {
switch (currentStep) {
case steps.length - 1:
return
case Steps.Start:
setSelectedTab(Tabs.STATS)
break
case Steps.ClickToWatchPlaylist:
setSelectedStatsTable(StatTabs.TEAM1)
break
case Steps.ShowMoreStats:
if (isMobileDevice) {
setStatsType(StatsType.FINAL_STATS)
} else {
toggleIsExpanded()
}
break
case Steps.ShowLessStats:
if (isMobileDevice) {
setStatsType(StatsType.CURRENT_STATS)
} else {
setStatsType(StatsType.FINAL_STATS)
toggleIsExpanded()
}
break
case Steps.FinalStats:
if (!isMobileDevice) {
setStatsType(StatsType.CURRENT_STATS)
}
break
default:
}
timerRef.current = setTimeout(() => setCurrentStep((step) => step + 1), 0)
}, [
currentStep,
setCurrentStep,
setSelectedStatsTable,
setSelectedTab,
setStatsType,
toggleIsExpanded,
steps.length,
])
const skipTour = useCallback(() => {
setIsOpen(false)
}, [setIsOpen])
useEventListener({
callback: useMemo(() => throttle((e: KeyboardEvent) => {
e.stopPropagation()
switch (e.code) {
case KEYBOARD_KEYS.ArrowLeft:
back()
break
case KEYBOARD_KEYS.ArrowRight:
next()
break
case KEYBOARD_KEYS.Esc:
skipTour()
break
default:
}
}, KEY_PRESS_DELAY), [back, next, skipTour]),
event: 'keydown',
options: true,
})
useEffect(() => () => {
timerRef.current && clearTimeout(timerRef.current)
}, [])
return {
back,
next,
skipTour,
}
}

@ -0,0 +1,123 @@
import type { ComponentType } from 'react'
import { Fragment } from 'react'
import type { PopoverContentProps } from '@reactour/tour'
import { isMobileDevice } from 'config'
import { T9n } from 'features/T9n'
import { useContentComponent } from './hooks'
import {
PrevButton,
NextButton,
ActionButtonsContainer,
Counter,
SkipTour,
ArrowWrapper,
} from './styled'
import { Steps } from '../../config'
export * from './styled'
const Arrow = () => (
<ArrowWrapper
width='7'
height='7'
viewBox='0 0 7 7'
fill='none'
>
<path
d='M5.25 3.06699C5.58333 3.25944 5.58333 3.74056 5.25 3.93301L1.5 6.09808C1.16667 6.29053 0.75 6.04996 0.75 5.66506L0.75 1.33494C0.75 0.950036 1.16667 0.709474 1.5 0.901924L5.25 3.06699Z'
fill='black'
fillOpacity='0.7'
/>
</ArrowWrapper>
)
export const ContentComponent: ComponentType<PopoverContentProps> = (props) => {
const {
back,
next,
skipTour,
} = useContentComponent(props)
const { currentStep, steps } = props
const renderActionButtons = () => {
switch (currentStep) {
case Steps.Start:
return (
<Fragment>
<NextButton onClick={next}>
<T9n t='start_tour' />
</NextButton>
<SkipTour onClick={skipTour}>
<T9n t='skip_tour' />
</SkipTour>
</Fragment>
)
case Steps.Welcome:
return (
<Fragment>
<Counter>
{currentStep}/{steps.length - 1}
</Counter>
<NextButton onClick={next}>
<T9n t='next_step' />
</NextButton>
<SkipTour onClick={skipTour}>
<T9n t='skip_tour' />
</SkipTour>
</Fragment>
)
case steps.length - 1:
return (
<Fragment>
<Counter>
{currentStep}/{steps.length - 1}
</Counter>
<PrevButton isLastStep onClick={back}>
<T9n t='back' />
</PrevButton>
<NextButton onClick={skipTour}>
<T9n t='end_tour' />
</NextButton>
</Fragment>
)
default:
return (
<Fragment>
<Counter>
{currentStep}/{steps.length - 1}
</Counter>
<PrevButton onClick={back}>
<T9n t='back' />
</PrevButton>
<NextButton onClick={next}>
<T9n t='next_step' />
</NextButton>
<SkipTour onClick={skipTour}>
<T9n t='skip_tour' />
</SkipTour>
</Fragment>
)
}
}
return (
<Fragment>
{steps[currentStep].content}
<ActionButtonsContainer step={currentStep}>
{renderActionButtons()}
</ActionButtonsContainer>
{!isMobileDevice && <Arrow />}
</Fragment>
)
}

@ -0,0 +1,158 @@
import styled, { css } from 'styled-components/macro'
import includes from 'lodash/includes'
import { isMobileDevice } from 'config'
import { T9n } from 'features/T9n'
import { Steps } from '../../config'
const NavButton = styled.button`
padding: 0;
border: none;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
text-transform: uppercase;
text-decoration: none;
background: none;
cursor: pointer;
${isMobileDevice
? css`
font-size: 15px;
`
: ''}
`
type PrevButtonProps = {
isLastStep?: boolean,
}
export const PrevButton = styled(NavButton)<PrevButtonProps>`
color: rgba(0, 0, 0, 0.5);
`
export const NextButton = styled(NavButton)`
color: #294FC3;
`
export const SkipTour = styled.button`
margin-top: -2px;
padding: 0;
border: none;
font-weight: 400;
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
text-decoration: underline;
text-transform: uppercase;
cursor: pointer;
background: none;
${isMobileDevice
? css`
font-size: 15px;
`
: ''}
`
export const Counter = styled.div`
color: rgba(0, 0, 0, 0.5);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
${isMobileDevice
? css`
font-size: 15px;
`
: ''}
`
type TitleProps = {
alignLeft?: boolean,
}
export const Title = styled(T9n)<TitleProps>`
display: block;
margin-bottom: 10px;
font-size: 14px;
font-weight: 700;
line-height: 17px;
${isMobileDevice
? css`
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
padding: 0 3%;
line-height: 20px;
font-size: 16px;
`
: ''}
${({ alignLeft }) => (alignLeft
? css`
padding: 0;
`
: '')}
`
export const Body = styled.div`
margin-bottom: 15px;
`
export const BodyText = styled(T9n)`
font-size: 14px;
line-height: 17px;
${isMobileDevice
? css`
line-height: 20px;
font-size: 16px;
`
: ''}
`
type ActionButtonsContainerProps = {
step: Steps,
}
export const ActionButtonsContainer = styled.div<ActionButtonsContainerProps>`
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
padding: 0;
justify-content: space-between;
${isMobileDevice
? css`
padding: 0 5%;
margin: auto;
gap: 20px;
justify-content: center;
`
: ''}
${({ step }) => (isMobileDevice && step === Steps.Start
? css`
justify-content: space-between;
padding: 0;
`
: '')}
${({ step }) => (isMobileDevice && (includes([Steps.FinalStats, Steps.Welcome], step))
? css`
padding: 0 20%;
`
: '')}
`
export const ArrowWrapper = styled.svg`
position: absolute;
top: 24px;
right: 15px;
`

@ -0,0 +1,100 @@
import { memo, useRef } from 'react'
import { useTour } from '@reactour/tour'
import styled, { css, keyframes } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { Steps } from '../../config'
type WrapperProps = {
step: number,
}
const getBaseSize = ({ step }: WrapperProps) => {
let baseSize = isMobileDevice ? 57 : 55
switch (step) {
case Steps.ClickToWatchPlaylist:
if (isMobileDevice) baseSize = 39
break
case Steps.TeamsTab:
case Steps.PlayersTab:
case Steps.ShowMoreStats:
if (isMobileDevice) baseSize = 75
break
case isMobileDevice ? Steps.ShowLessStats : Steps.FinalStats:
case isMobileDevice ? Steps.FinalStats : Steps.CurrentStats:
baseSize = 118
break
default:
}
return baseSize
}
const getAnimation = ({ step }: WrapperProps) => {
const baseSize = getBaseSize({ step })
return keyframes`
to {
scale: ${(baseSize + 5) / baseSize};
}
`
}
const Wrapper = styled.div<WrapperProps>`
position: absolute;
top: 50%;
left: 50%;
display: block;
width: ${getBaseSize}px;
height: ${getBaseSize}px;
border-radius: 100%;
border: 1px solid #0057FF;
translate: -50% -50%;
background: radial-gradient(50% 50% at 50% 50%, rgba(0, 87, 255, 0) 70.25%, rgba(0, 87, 255, 0.4) 100%);
animation: ${getAnimation} 0.8s ease-in-out infinite alternate;
z-index: 9999;
${({ step }) => {
switch (step) {
case Steps.ShowMoreStats:
return css`left: 3px;`
case Steps.ShowLessStats:
return isMobileDevice
? ''
: css`left: 0;`
case isMobileDevice ? Steps.ShowLessStats : Steps.FinalStats:
case isMobileDevice ? Steps.FinalStats : Steps.CurrentStats:
return css`
right: -14px;
left: auto;
translate: 0 -50%;
`
default:
return ''
}
}}
`
const SpotlightFC = () => {
const ref = useRef<HTMLDivElement>(null)
const { currentStep } = useTour()
return (
<Wrapper
ref={ref}
step={currentStep}
/>
)
}
export const Spotlight = memo(SpotlightFC)

@ -0,0 +1,2 @@
export * from './ContentComponent'
export * from './Spotlight'

@ -0,0 +1,13 @@
export enum Steps {
Start,
Welcome,
TeamsTab,
ClickToWatchPlaylist,
PlayersTab,
ShowMoreStats,
ShowLessStats,
FinalStats,
CurrentStats,
}
export const TOUR_COMPLETED_STORAGE_KEY = 'tour_completed'

@ -0,0 +1,3 @@
export * from './config'
export * from './TourProvider'
export * from './components'

@ -5,8 +5,12 @@ import {
useRef,
} from 'react'
import { useTour } from '@reactour/tour'
import size from 'lodash/size'
import { KEYBOARD_KEYS } from 'config'
import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility'
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
@ -83,6 +87,8 @@ export const useMultiSourcePlayer = ({
setPlayingProgress,
} = useMatchPageStore()
const { isOpen } = useTour()
const { profileId, sportType } = usePageParams()
/** время для сохранения статистики просмотра матча */
@ -95,7 +101,7 @@ export const useMultiSourcePlayer = ({
activePlayer,
loadedProgress,
playedProgress,
playing,
playing: statePlaying,
ready,
seek,
seeking,
@ -121,6 +127,8 @@ export const useMultiSourcePlayer = ({
const duration = useDuration(chapters)
const playing = Boolean(statePlaying && !isOpen)
const handleError = useCallback(() => {
onError?.()
}, [onError])
@ -310,8 +318,9 @@ export const useMultiSourcePlayer = ({
useEventListener({
callback: (e: KeyboardEvent) => {
if (e.code === 'ArrowLeft') rewindBackward()
else if (e.code === 'ArrowRight') rewindForward()
if (isOpen) return
if (e.code === KEYBOARD_KEYS.ArrowLeft) rewindBackward()
else if (e.code === KEYBOARD_KEYS.ArrowRight) rewindForward()
},
event: 'keydown',
})

@ -3,9 +3,36 @@ import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { customScrollbar } from 'features/Common'
export const PageWrapper = styled.div<{isIOS?: boolean}>`
type PageWrapperProps = {
isIOS?: boolean,
isTourOpen?: boolean,
}
export const PageWrapper = styled.div<PageWrapperProps>`
width: 100%;
touch-action: ${({ isIOS }) => (isIOS ? 'none' : 'unset')};
${({ isTourOpen }) => (isTourOpen
? css`
pointer-events: none;
overflow: hidden;
`
: '')}
~ .reactour__popover {
${isMobileDevice
? css`
@media screen and (orientation: landscape) {
/* добавлен important чтобы переопределить стили либы */
left: 50% !important;
top: auto !important;
bottom: 0;
width: 70vw !important;
transform: translate(-50%, 0) !important;
}`
: ''};
}
`
export const Main = styled.main`

@ -7,6 +7,8 @@ import {
useState,
} from 'react'
import { useTour } from '@reactour/tour'
import size from 'lodash/size'
import isNumber from 'lodash/isNumber'
import isEmpty from 'lodash/isEmpty'
@ -14,7 +16,7 @@ import isUndefined from 'lodash/isUndefined'
import Hls from 'hls.js'
import { isIOS } from 'config/userAgent'
import { isIOS, KEYBOARD_KEYS } from 'config'
import {
useObjectState,
@ -86,7 +88,7 @@ export const useVideoPlayer = ({
duration: fullMatchDuration,
loadedProgress,
playedProgress,
playing,
playing: statePlaying,
ready,
seek,
seeking,
@ -104,6 +106,10 @@ export const useVideoPlayer = ({
setPlayingProgress,
} = useMatchPageStore()
const { isOpen } = useTour()
const playing = Boolean(statePlaying && !isOpen)
/** время для сохранения статистики просмотра матча */
const timeForStatistics = useRef(0)
@ -319,8 +325,9 @@ export const useVideoPlayer = ({
useEventListener({
callback: (e: KeyboardEvent) => {
if (e.code === 'ArrowLeft') rewindBackward()
else if (e.code === 'ArrowRight') rewindForward()
if (isOpen) return
if (e.code === KEYBOARD_KEYS.ArrowLeft) rewindBackward()
else if (e.code === KEYBOARD_KEYS.ArrowRight) rewindForward()
},
event: 'keydown',
})

@ -0,0 +1,267 @@
/* eslint-disable no-param-reassign */
import { isIOS } from 'config'
type BodyScrollOptions = {
allowTouchMove?: ((el: EventTarget) => boolean) | undefined,
reserveScrollBarGap?: boolean | undefined,
}
let previousBodyPosition: {
left: string,
position: string,
right: string,
top: string,
} | undefined
let locks: Array<{
options: BodyScrollOptions,
targetElement: HTMLElement | Element,
}> = []
let initialClientY = -1
let documentListenerAdded = false
let previousBodyOverflowSetting: string | undefined
let previousBodyPaddingRight: string | undefined
// returns true if `el` should be allowed to receive touchmove events.
const allowTouchMove = (el: EventTarget | null) => locks.some((lock) => {
if (el && lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {
return true
}
return false
})
const preventDefault = (rawEvent?: Event) => {
const e = rawEvent || window.event
// For the case whereby consumers adds a touchmove event listener to document.
// Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })
// in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then
// the touchmove event on document will break.
// @ts-expect-error
if (allowTouchMove(e.target)) {
return true
}
/** Do not prevent if the event has more than one touch
* (usually meaning this is a multi touch gesture like pinch to zoom).
* */
// @ts-expect-error
if (e.touches.length > 1) return true
// @ts-expect-error
if (e.preventDefault) e.preventDefault()
return false
}
const setPositionFixed = () => window.requestAnimationFrame(() => {
// If previousBodyPosition is already set, don't set it again.
if (previousBodyPosition === undefined) {
previousBodyPosition = {
left: document.body.style.left,
position: document.body.style.position,
right: document.body.style.right,
top: document.body.style.top,
}
// Update the dom inside an animation frame
const {
innerHeight,
scrollX,
scrollY,
} = window
document.body.style.position = 'fixed'
// @ts-expect-error
document.body.style.top = -scrollY
// @ts-expect-error
document.body.style.left = -scrollX
// @ts-expect-error
document.body.style.right = 0
setTimeout(() => window.requestAnimationFrame(() => {
// Attempt to check if the bottom bar appeared due to the position change
const bottomBarHeight = innerHeight - window.innerHeight
if (bottomBarHeight && scrollY >= innerHeight) {
// Move the content further up so that the bottom bar doesn't hide it
// @ts-expect-error
document.body.style.top = -(scrollY + bottomBarHeight)
}
}), 300)
}
})
const setOverflowHidden = (options?: BodyScrollOptions) => {
// If previousBodyPaddingRight is already set, don't set it again.
if (previousBodyPaddingRight === undefined) {
const reserveScrollBarGap = !!options && options.reserveScrollBarGap === true
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
if (reserveScrollBarGap && scrollBarGap > 0) {
const computedBodyPaddingRight = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'), 10)
previousBodyPaddingRight = document.body.style.paddingRight
document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`
}
}
// If previousBodyOverflowSetting is already set, don't set it again.
if (previousBodyOverflowSetting === undefined) {
previousBodyOverflowSetting = document.body.style.overflow
document.body.style.overflow = 'hidden'
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
const isTargetElementTotallyScrolled = (targetElement: HTMLElement | Element | null) => (
targetElement
? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight
: false
)
const handleScroll = (event: TouchEvent, targetElement: HTMLElement | Element | null) => {
const clientY = event.targetTouches[0].clientY - initialClientY
if (allowTouchMove(event.target)) {
return false
}
if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
// element is at the top of its scroll.
return preventDefault(event)
}
if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
// element is at the bottom of its scroll.
return preventDefault(event)
}
event.stopPropagation()
return true
}
const restorePositionSetting = () => {
if (previousBodyPosition !== undefined) {
// Convert the position from "px" to Int
const y = -parseInt(document.body.style.top, 10)
const x = -parseInt(document.body.style.left, 10)
// Restore styles
document.body.style.position = previousBodyPosition.position
document.body.style.top = previousBodyPosition.top
document.body.style.left = previousBodyPosition.left
document.body.style.right = previousBodyPosition.right
// Restore scroll
window.scrollTo(x, y)
previousBodyPosition = undefined
}
}
const restoreOverflowSetting = () => {
if (previousBodyPaddingRight !== undefined) {
document.body.style.paddingRight = previousBodyPaddingRight
// Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it
// can be set again.
previousBodyPaddingRight = undefined
}
if (previousBodyOverflowSetting !== undefined) {
document.body.style.overflow = previousBodyOverflowSetting
// Restore previousBodyOverflowSetting to undefined
// so setOverflowHidden knows it can be set again.
previousBodyOverflowSetting = undefined
}
}
// Enables body scroll locking without breaking scrolling of a target element
export const enableBodyScroll = (targetElement: HTMLElement | Element) => {
if (!targetElement) {
// eslint-disable-next-line no-console
console.error('enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.')
return
}
locks = locks.filter((lock) => lock.targetElement !== targetElement)
if (isIOS) {
// @ts-expect-error
targetElement.ontouchstart = null
// @ts-expect-error
targetElement.ontouchmove = null
if (documentListenerAdded && locks.length === 0) {
document.removeEventListener(
'touchmove',
preventDefault,
// @ts-expect-error
{ passive: false },
)
documentListenerAdded = false
}
}
if (isIOS) {
restorePositionSetting()
} else {
restoreOverflowSetting()
}
}
// Disable body scroll locking
export const disableBodyScroll = (
targetElement: HTMLElement | Element, options?: BodyScrollOptions,
) => {
// targetElement must be provided
if (!targetElement) {
// eslint-disable-next-line no-console
console.error('disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.')
return
}
// disableBodyScroll must not have been called on this targetElement before
if (locks.some((lock) => lock.targetElement === targetElement)) {
return
}
const lock = {
options: options || {},
targetElement,
}
locks = [...locks, lock]
if (isIOS) {
setPositionFixed()
} else {
setOverflowHidden(options)
}
if (isIOS) {
// @ts-expect-error
targetElement.ontouchstart = (event) => {
if (event.targetTouches.length === 1) {
// detect single touch.
initialClientY = event.targetTouches[0].clientY
}
}
// @ts-expect-error
targetElement.ontouchmove = (event) => {
if (event.targetTouches.length === 1) {
// detect single touch.
handleScroll(event, targetElement)
}
}
if (!documentListenerAdded) {
document.addEventListener(
'touchmove',
preventDefault,
{ passive: false },
)
documentListenerAdded = true
}
}
}

@ -14,3 +14,4 @@ export * from './getTeamAbbr'
export * from './cookie'
export * from './isMatchPage'
export * from './languageUrlParam'
export * from './bodyScroll'

@ -7,3 +7,4 @@ export * from './useObjectState'
export * from './usePageParams'
export * from './useTooltip'
export * from './useModalRoot'
export * from './usePageLogger'

@ -9,6 +9,7 @@ type Target = RefObject<HTMLElement> | HTMLElement | Window
type Args<E extends keyof EventMap> = {
callback: (e: EventMap[E]) => void,
event: E,
options?: Parameters<(HTMLElement | Window)['addEventListener']>[2],
target?: Target,
}
@ -19,6 +20,7 @@ type Args<E extends keyof EventMap> = {
export const useEventListener = <E extends keyof EventMap>({
callback,
event,
options,
target = window,
}: Args<E>) => {
const callbackRef = useRef(callback)
@ -39,9 +41,17 @@ export const useEventListener = <E extends keyof EventMap>({
callbackRef.current(e)
}
windowOrElement?.addEventListener(event, listener)
windowOrElement?.addEventListener(
event,
listener,
options,
)
return () => {
windowOrElement?.removeEventListener(event, listener)
windowOrElement?.removeEventListener(
event,
listener,
options,
)
}
}, [event, target])
}, [event, target, options])
}

@ -20,7 +20,7 @@ export type Player = {
lastname_national: string | null,
lastname_rus: string,
national_f_team: number | null,
national_shirt_num: number,
national_shirt_num: number | null,
nickname_eng: string | null,
nickname_rus: string | null,
num: number | null,

@ -12,7 +12,7 @@ export type PlayerParam = {
lexica_short: number | null,
markers: Array<number> | null,
name_en: string,
name_ru: string,
name_ru: string | null,
val: number | null,
}

@ -9,9 +9,9 @@ export type Param = {
data_type: string,
id: number,
lexic: number,
markers: Array<number>,
markers: Array<number> | null,
name_en: string,
name_ru: string,
name_ru: string | null,
val: number | null,
}

Loading…
Cancel
Save