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. 30
      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. 32
      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. 60
      src/features/MatchSidePlaylists/index.tsx
  27. 40
      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" "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": { "@rollup/plugin-babel": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "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": { "@rushstack/eslint-patch": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" "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": { "resolve": {
"version": "1.22.1", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",

@ -4,6 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "react-scripts start", "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", "build": "GENERATE_SOURCEMAP=false react-scripts build && gzipper --verbose ./build",
"test": "react-scripts test --testMatch '**/__tests__/*' --passWithNoTests --watchAll=false", "test": "react-scripts test --testMatch '**/__tests__/*' --passWithNoTests --watchAll=false",
"test:watch": "react-scripts test --testMatch '**/__tests__/*'", "test:watch": "react-scripts test --testMatch '**/__tests__/*'",
@ -21,6 +22,7 @@
"insports": "REACT_APP_CLIENT=insports react-scripts start" "insports": "REACT_APP_CLIENT=insports react-scripts start"
}, },
"dependencies": { "dependencies": {
"@reactour/tour": "^3.3.0",
"@stripe/react-stripe-js": "^1.4.0", "@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.13.2", "@stripe/stripe-js": "^1.13.2",
"babel-polyfill": "^6.26.0", "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 './userAgent'
export * from './queries' export * from './queries'
export * from './keyboardKeys' export * from './keyboardKeys'
export * from './clients'

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

@ -29,6 +29,7 @@ export const Container = styled.div<ContainerProps>`
type TableWrapperProps = { type TableWrapperProps = {
isExpanded?: boolean, isExpanded?: boolean,
isOpenTour?: boolean,
} }
export const TableWrapper = styled.div<TableWrapperProps>` 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 ${isMobileDevice
? '' ? ''
: css` : 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-radius: 5px;
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
letter-spacing: -0.078px; letter-spacing: -0.078px;
table-layout: fixed; table-layout: fixed;
${({ fullWidth }) => (fullWidth
? css`
width: 100%;
`
: '')
}
` `
type ParamShortTitleProps = { 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; right: 0;
border-top-right-radius: 5px; border-top-right-radius: 5px;
@ -259,6 +288,13 @@ export const ArrowButtonRight = styled(ArrowButton)`
left: auto; left: auto;
right: 7px; right: 7px;
} }
${({ visible }) => (!visible
? css`
visibility: hidden;
`
: '')
}
` `
export const ArrowButtonLeft = styled(ArrowButton)` export const ArrowButtonLeft = styled(ArrowButton)`
@ -276,7 +312,7 @@ export const ExpandButton = styled(ArrowButton)<ExpandButtonProps>`
${Arrow} { ${Arrow} {
left: ${({ isExpanded }) => (isExpanded ? -6 : -2)}px; left: ${({ isExpanded }) => (isExpanded ? -6 : -2)}px;
:last-child { :last-of-type {
margin-left: 7px; margin-left: 7px;
} }
} }

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useEffect } from 'react'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
@ -9,11 +9,11 @@ import { useMatchPageStore } from 'features/MatchPage/store'
import { StatsType, Tabs } from './config' import { StatsType, Tabs } from './config'
export const useTabStats = () => { export const useTabStats = () => {
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS)
const { const {
isEmptyPlayersStats, isEmptyPlayersStats,
profile: matchProfile, profile: matchProfile,
selectedStatsTable: selectedTab,
setSelectedStatsTable: setSelectedTab,
statsType, statsType,
teamsStats, teamsStats,
toggleStatsType, toggleStatsType,
@ -56,7 +56,12 @@ export const useTabStats = () => {
default: default:
} }
}, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab]) }, [
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
setSelectedTab,
])
return { return {
isFinalStatsType, isFinalStatsType,

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

@ -1,5 +1,7 @@
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { TooltipWrapper } from 'features/Tooltip' import { TooltipWrapper } from 'features/Tooltip'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
@ -62,7 +64,7 @@ export const TabTitle = styled.span<TabTitleProps>`
export const Tab = styled.button.attrs({ role: 'tab' })` export const Tab = styled.button.attrs({ role: 'tab' })`
position: relative; position: relative;
display: flex; display: flex;
justify-content: space-between; justify-content: center;
align-items: center; align-items: center;
padding: 0 10px 10px; padding: 0 10px 10px;
font-size: 12px; font-size: 12px;
@ -78,10 +80,36 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
color: ${({ theme }) => theme.colors.white}; 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; 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)` export const SwitchTitle = styled(T9n)`
@ -98,7 +126,6 @@ export const SwitchButton = styled.button<SwitchButtonProps>`
width: 20px; width: 20px;
height: 7px; height: 7px;
margin-left: 5px; margin-left: 5px;
margin-top: 5px;
border-radius: 2px; border-radius: 2px;
border: none; border: none;
border: 1px solid ${({ theme }) => theme.colors.white}; border: 1px solid ${({ theme }) => theme.colors.white};

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

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

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

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

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

@ -9,8 +9,22 @@ import {
import { customScrollbar } from 'features/Common' import { customScrollbar } from 'features/Common'
import { T9n } from 'features/T9n' 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-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 ${isMobileDevice
? css` ? css`
@ -21,6 +35,13 @@ export const Wrapper = styled.div`
${customScrollbar} ${customScrollbar}
` `
: ''}; : ''};
${({ isTourOpen }) => (isTourOpen
? css`
overflow-y: initial;
z-index: 9999;
`
: '')}
` `
export const TabsWrapper = styled.div`` export const TabsWrapper = styled.div``
@ -36,8 +57,6 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
${({ hasLessThanFourTabs }) => (hasLessThanFourTabs ${({ hasLessThanFourTabs }) => (hasLessThanFourTabs
? css` ? css`
padding-top: 10px;
${Tab} { ${Tab} {
justify-content: center; justify-content: center;
flex-direction: row; flex-direction: row;
@ -58,7 +77,7 @@ export const TabTitle = styled(T9n)`
color: ${({ theme }) => theme.colors.white}; color: ${({ theme }) => theme.colors.white};
` `
export const Tab = styled.button.attrs({ role: 'tab' })` export const TabButton = styled.button`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
@ -69,14 +88,20 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
cursor: pointer; cursor: pointer;
border: none; border: none;
background: none; background: none;
`
export const Tab = styled.div.attrs({ role: 'tab' })`
position: relative;
&[aria-pressed="true"], :hover { &[aria-pressed="true"], :hover {
${TabButton} {
opacity: 1; opacity: 1;
${TabTitle} { ${TabTitle} {
font-weight: 600; font-weight: 600;
} }
} }
}
:only-child { :only-child {
cursor: unset; cursor: unset;
@ -107,6 +132,7 @@ export const TabIcon = styled.div<TabIconProps>`
type TContainer = { type TContainer = {
forWatchTab?: boolean, forWatchTab?: boolean,
hasScroll: boolean, hasScroll: boolean,
highlighted?: boolean,
} }
export const Container = styled.div<TContainer>` export const Container = styled.div<TContainer>`
@ -140,6 +166,12 @@ export const Container = styled.div<TContainer>`
} }
` `
: ''}; : ''};
${({ highlighted }) => (highlighted
? css`
overflow-y: initial;
`
: '')}
` `
type ButtonProps = { 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, useRef,
} from 'react' } from 'react'
import { useTour } from '@reactour/tour'
import size from 'lodash/size' import size from 'lodash/size'
import { KEYBOARD_KEYS } from 'config'
import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility' import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility'
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen' import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume' import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
@ -83,6 +87,8 @@ export const useMultiSourcePlayer = ({
setPlayingProgress, setPlayingProgress,
} = useMatchPageStore() } = useMatchPageStore()
const { isOpen } = useTour()
const { profileId, sportType } = usePageParams() const { profileId, sportType } = usePageParams()
/** время для сохранения статистики просмотра матча */ /** время для сохранения статистики просмотра матча */
@ -95,7 +101,7 @@ export const useMultiSourcePlayer = ({
activePlayer, activePlayer,
loadedProgress, loadedProgress,
playedProgress, playedProgress,
playing, playing: statePlaying,
ready, ready,
seek, seek,
seeking, seeking,
@ -121,6 +127,8 @@ export const useMultiSourcePlayer = ({
const duration = useDuration(chapters) const duration = useDuration(chapters)
const playing = Boolean(statePlaying && !isOpen)
const handleError = useCallback(() => { const handleError = useCallback(() => {
onError?.() onError?.()
}, [onError]) }, [onError])
@ -310,8 +318,9 @@ export const useMultiSourcePlayer = ({
useEventListener({ useEventListener({
callback: (e: KeyboardEvent) => { callback: (e: KeyboardEvent) => {
if (e.code === 'ArrowLeft') rewindBackward() if (isOpen) return
else if (e.code === 'ArrowRight') rewindForward() if (e.code === KEYBOARD_KEYS.ArrowLeft) rewindBackward()
else if (e.code === KEYBOARD_KEYS.ArrowRight) rewindForward()
}, },
event: 'keydown', event: 'keydown',
}) })

@ -3,9 +3,36 @@ import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'
import { customScrollbar } from 'features/Common' 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%; width: 100%;
touch-action: ${({ isIOS }) => (isIOS ? 'none' : 'unset')}; 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` export const Main = styled.main`

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

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

@ -9,6 +9,7 @@ type Target = RefObject<HTMLElement> | HTMLElement | Window
type Args<E extends keyof EventMap> = { type Args<E extends keyof EventMap> = {
callback: (e: EventMap[E]) => void, callback: (e: EventMap[E]) => void,
event: E, event: E,
options?: Parameters<(HTMLElement | Window)['addEventListener']>[2],
target?: Target, target?: Target,
} }
@ -19,6 +20,7 @@ type Args<E extends keyof EventMap> = {
export const useEventListener = <E extends keyof EventMap>({ export const useEventListener = <E extends keyof EventMap>({
callback, callback,
event, event,
options,
target = window, target = window,
}: Args<E>) => { }: Args<E>) => {
const callbackRef = useRef(callback) const callbackRef = useRef(callback)
@ -39,9 +41,17 @@ export const useEventListener = <E extends keyof EventMap>({
callbackRef.current(e) callbackRef.current(e)
} }
windowOrElement?.addEventListener(event, listener) windowOrElement?.addEventListener(
event,
listener,
options,
)
return () => { 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_national: string | null,
lastname_rus: string, lastname_rus: string,
national_f_team: number | null, national_f_team: number | null,
national_shirt_num: number, national_shirt_num: number | null,
nickname_eng: string | null, nickname_eng: string | null,
nickname_rus: string | null, nickname_rus: string | null,
num: number | null, num: number | null,

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

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

Loading…
Cancel
Save