From c4403ea089549767151cb91f862aaa756db49a49 Mon Sep 17 00:00:00 2001 From: Ruslan Khayrullin Date: Thu, 23 Mar 2023 16:04:27 +0500 Subject: [PATCH] feat(in-424): statistics tour --- package-lock.json | 45 + package.json | 2 + src/components/Overlay/index.tsx | 9 + src/config/index.tsx | 1 + src/config/keyboardKeys.tsx | 3 + src/config/lexics/indexLexics.tsx | 17 + .../components/MatchDescription/index.tsx | 11 +- src/features/MatchPage/index.tsx | 32 +- src/features/MatchPage/store/hooks/index.tsx | 37 +- .../MatchPage/store/hooks/useFakeData.tsx | 3513 +++++++++++++++++ .../MatchPage/store/hooks/usePlayersStats.tsx | 59 +- .../MatchPage/store/hooks/useStatsTab.tsx | 16 +- .../MatchPage/store/hooks/useTeamsStats.tsx | 63 +- src/features/MatchPage/styled.tsx | 15 +- .../components/PlayersTable/hooks/index.tsx | 2 + .../PlayersTable/hooks/useTable.tsx | 42 +- .../components/PlayersTable/index.tsx | 42 +- .../components/PlayersTable/styled.tsx | 42 +- .../components/TabStats/hooks.tsx | 13 +- .../components/TabStats/index.tsx | 25 +- .../components/TabStats/styled.tsx | 33 +- .../components/TeamsStatsTable/Cell.tsx | 28 +- .../components/TeamsStatsTable/hooks.tsx | 2 + .../components/TeamsStatsTable/index.tsx | 13 +- .../components/TeamsStatsTable/styled.tsx | 15 +- src/features/MatchSidePlaylists/index.tsx | 76 +- src/features/MatchSidePlaylists/styled.tsx | 46 +- src/features/MatchTour/TourProvider.tsx | 198 + .../components/ContentComponent/hooks.tsx | 161 + .../components/ContentComponent/index.tsx | 123 + .../components/ContentComponent/styled.tsx | 158 + .../MatchTour/components/Spotlight/index.tsx | 100 + src/features/MatchTour/components/index.tsx | 2 + src/features/MatchTour/config.tsx | 13 + src/features/MatchTour/index.tsx | 3 + .../MultiSourcePlayer/hooks/index.tsx | 15 +- src/features/PageLayout/styled.tsx | 29 +- src/features/StreamPlayer/hooks/index.tsx | 15 +- src/helpers/bodyScroll/index.tsx | 267 ++ src/helpers/index.tsx | 1 + src/hooks/index.tsx | 1 + src/hooks/useEventListener.tsx | 16 +- src/requests/getMatchParticipants.tsx | 2 +- src/requests/getPlayersStats.tsx | 2 +- src/requests/getTeamsStats.tsx | 4 +- 45 files changed, 5187 insertions(+), 125 deletions(-) create mode 100644 src/components/Overlay/index.tsx create mode 100644 src/features/MatchPage/store/hooks/useFakeData.tsx create mode 100644 src/features/MatchTour/TourProvider.tsx create mode 100644 src/features/MatchTour/components/ContentComponent/hooks.tsx create mode 100644 src/features/MatchTour/components/ContentComponent/index.tsx create mode 100644 src/features/MatchTour/components/ContentComponent/styled.tsx create mode 100644 src/features/MatchTour/components/Spotlight/index.tsx create mode 100644 src/features/MatchTour/components/index.tsx create mode 100644 src/features/MatchTour/config.tsx create mode 100644 src/features/MatchTour/index.tsx create mode 100644 src/helpers/bodyScroll/index.tsx diff --git a/package-lock.json b/package-lock.json index cb4ca879..fd7b1084 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2982,6 +2982,41 @@ "react-lifecycles-compat": "^3.0.4" } }, + "@reactour/mask": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@reactour/mask/-/mask-1.0.5.tgz", + "integrity": "sha512-SMakvPUsH83j4MAq87jBMpdzoVQ+amZTQ6rYsDBqN1Hcz+A8JN9IDgaA49UaLcRPZq+ioQrmos8mBTe8uEtPeQ==", + "requires": { + "@reactour/utils": "*" + } + }, + "@reactour/popover": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@reactour/popover/-/popover-1.0.5.tgz", + "integrity": "sha512-d6BMcyXGj3RdSc2huiU6v/wG2XG1ad+lAFmjyFerlZNS1ccp/49HvLUnqxE0Td+86e7RPrdEzpZb5PKtBybPrA==", + "requires": { + "@reactour/utils": "*" + } + }, + "@reactour/tour": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@reactour/tour/-/tour-3.3.0.tgz", + "integrity": "sha512-Dx/jDKEZ29fSOmnc07zCgHS6lmEKCNreyvFhhPQTI1OAG6MTTWuQJhKUmrytECncFJb+oH95zvE9mf137rSBtA==", + "requires": { + "@reactour/mask": "*", + "@reactour/popover": "*", + "@reactour/utils": "*" + } + }, + "@reactour/utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@reactour/utils/-/utils-0.4.7.tgz", + "integrity": "sha512-d+/Xhi2nKCc6OrDEFGg15iN8ZyWDTdOrwIKkndJXrnWiN6b+nqoS2Tb7hZvt79rqCyUsQxekUndPFPQyIW81EA==", + "requires": { + "@rooks/use-mutation-observer": "^4.11.2", + "resize-observer-polyfill": "^1.5.1" + } + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3030,6 +3065,11 @@ } } }, + "@rooks/use-mutation-observer": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@rooks/use-mutation-observer/-/use-mutation-observer-4.11.2.tgz", + "integrity": "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A==" + }, "@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -24912,6 +24952,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", diff --git a/package.json b/package.json index 182a89be..058308d2 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "start": "react-scripts start", + "start-https": "export HTTPS=true&&SSL_CRT_FILE=cert.pem&&SSL_KEY_FILE=key.pem react-scripts start", "build": "GENERATE_SOURCEMAP=false react-scripts build && gzipper --verbose ./build", "test": "react-scripts test --testMatch '**/__tests__/*' --passWithNoTests --watchAll=false", "test:watch": "react-scripts test --testMatch '**/__tests__/*'", @@ -21,6 +22,7 @@ "insports": "REACT_APP_CLIENT=insports react-scripts start" }, "dependencies": { + "@reactour/tour": "^3.3.0", "@stripe/react-stripe-js": "^1.4.0", "@stripe/stripe-js": "^1.13.2", "babel-polyfill": "^6.26.0", diff --git a/src/components/Overlay/index.tsx b/src/components/Overlay/index.tsx new file mode 100644 index 00000000..a4bee595 --- /dev/null +++ b/src/components/Overlay/index.tsx @@ -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; +` diff --git a/src/config/index.tsx b/src/config/index.tsx index e94d0b69..a5fd70ae 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -11,3 +11,4 @@ export * from './env' export * from './userAgent' export * from './queries' export * from './keyboardKeys' +export * from './clients' diff --git a/src/config/keyboardKeys.tsx b/src/config/keyboardKeys.tsx index 97d3b125..ead63a10 100644 --- a/src/config/keyboardKeys.tsx +++ b/src/config/keyboardKeys.tsx @@ -1,3 +1,6 @@ export enum KEYBOARD_KEYS { + ArrowLeft = 'ArrowLeft', + ArrowRight = 'ArrowRight', Enter = 'Enter', + Esc = 'Escape', } diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index bf1e822a..95118ce8 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -7,12 +7,20 @@ import { sportsLexic } from './sportsLexic' const matchPopupLexics = { actions: 1020, apply: 13491, + back: 696, + blue_stats_are_clickable: 20071, + check_out_the_stats: 20066, choose_fav_team: 19776, + click_to_see_full_time_stats: 20074, + click_to_see_stats_in_real_time: 20075, + click_to_watch_playlist: 20072, commentators: 15424, + compare_teams_across_multiple_metrics: 20070, continue_watching: 20007, current_stats: 19592, display_all_stats: 19932, display_stats_according_to_video: 19931, + end_tour: 20076, episode_duration: 13410, events: 1020, final_stats: 19591, @@ -24,9 +32,11 @@ const matchPopupLexics = { go_back_to_match: 13405, group: 7850, half_time: 1033, + here_you_will_discover_tons: 20067, languages: 15030, match_interviews: 13031, match_settings: 13490, + next_step: 15156, no_data: 15397, other_games: 19997, others: 19902, @@ -38,12 +48,18 @@ const matchPopupLexics = { sec_60: 20006, sec_after: 13412, sec_before: 13411, + see_interactive_game_stats: 20069, selected_player_actions: 13413, + show_less_stats: 20064, + show_more_stats: 20063, sign_in: 20003, sign_in_full_game: 20004, + skip_tour: 20065, + start_tour: 20062, started_streaming_at: 16042, stats: 18179, streamed_live_on: 16043, + team_players_stats: 20073, video: 1017, views: 13440, watch: 818, @@ -51,6 +67,7 @@ const matchPopupLexics = { watch_live_stream: 13020, watch_players_episodes: 14052, watching_now: 16041, + welcom_to_stats_tab: 20068, } const filterPopup = { diff --git a/src/features/MatchPage/components/MatchDescription/index.tsx b/src/features/MatchPage/components/MatchDescription/index.tsx index 4648d086..6f1df809 100644 --- a/src/features/MatchPage/components/MatchDescription/index.tsx +++ b/src/features/MatchPage/components/MatchDescription/index.tsx @@ -2,6 +2,8 @@ import { useCallback } from 'react' import { useQuery } from 'react-query' +import { useTour } from '@reactour/tour' + import { format } from 'date-fns' import includes from 'lodash/includes' @@ -28,6 +30,8 @@ import { usePageParams } from 'hooks/usePageParams' import { getMatchScore } from 'requests' +import { Steps } from 'features/MatchTour' + import { Description, DescriptionInnerBlock, @@ -48,6 +52,7 @@ export const MatchDescription = () => { const { isScoreHidden } = useMatchSwitchesStore() const { suffix } = useLexicsStore() const { profile, profileCardShown } = useMatchPageStore() + const { isOpen } = useTour() const getTeamName = useCallback((team: Team) => ( isMobileDevice @@ -68,6 +73,8 @@ export const MatchDescription = () => { refetchInterval: 5000, }) + if (isOpen && !isMobileDevice) return null + if (!profile) return const { @@ -88,7 +95,9 @@ export const MatchDescription = () => { return ( - + <Title + data-step={isMobileDevice && isOpen ? Steps.Start : undefined} + > <StyledLink id={team1.id} profileType={ProfileTypes.TEAMS} diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 72066bb6..bdbc7e86 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -1,6 +1,8 @@ import { useEffect } from 'react' import { useHistory } from 'react-router' +import { useTour } from '@reactour/tour' + import { useTheme } from 'styled-components' import { ProfileHeader } from 'features/ProfileHeader' @@ -13,20 +15,24 @@ import { import { FavoritesActions } from 'requests' -import { ProfileTypes } from 'config' -import { client } from 'config/clients' -import { isIOS } from 'config/userAgent' +import { + ProfileTypes, + isIOS, + client, +} from 'config' -import { usePageLogger } from 'hooks/usePageLogger' -import { usePageParams } from 'hooks/usePageParams' +import { usePageLogger, usePageParams } from 'hooks' import { checkUrlParams } from 'helpers/parseUrlParams/parseUrlParams' + +import { TourProvider } from 'features/MatchTour' + import { MatchPageStore, useMatchPageStore } from './store' import { SubscriptionGuard } from './components/SubscriptionGuard' import { LiveMatch } from './components/LiveMatch' -import { Wrapper } from './styled' import { FinishedMatch } from './components/FinishedMatch' import { FavouriteTeamPopup } from './components/FavouriteTeam' +import { Wrapper } from './styled' const MatchPageComponent = () => { usePageLogger() @@ -39,6 +45,7 @@ const MatchPageComponent = () => { profile, user, } = useMatchPageStore() + const isFavorite = profile && userFavorites?.find((fav) => fav.id === profile?.tournament.id) const { @@ -46,6 +53,8 @@ const MatchPageComponent = () => { sportType, } = usePageParams() + const { isOpen } = useTour() + useEffect(() => { let timer = 0 timer = window.setTimeout(() => { @@ -91,12 +100,15 @@ const MatchPageComponent = () => { } return ( - <PageWrapper isIOS={isIOS}> + <PageWrapper + isIOS={isIOS} + isTourOpen={Boolean(isOpen)} + > <ProfileHeader color={colors.matchHeaderBackground} height={client.name === 'facr' ? 5 : 4.5} /> <Main> <UserFavorites /> <SubscriptionGuard> - <Wrapper> + <Wrapper isTourOpen={Boolean(isOpen)}> {playFromOTT && ( <LiveMatch /> )} @@ -118,7 +130,9 @@ const MatchPageComponent = () => { const MatchPage = () => ( <MatchPageStore> - <MatchPageComponent /> + <TourProvider> + <MatchPageComponent /> + </TourProvider> </MatchPageStore> ) diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index 3e12ba13..ecb63fea 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -12,6 +12,8 @@ import { useAuthStore } from 'features/AuthStore' import { Tabs } from 'features/MatchSidePlaylists/config' import { initialCircleAnimation } from 'features/CircleAnimationBar' import type { TCircleAnimation } from 'features/CircleAnimationBar' +import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' +import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour' import { PAGES } from 'config/pages' @@ -24,6 +26,7 @@ import { usePageParams, useToggle } from 'hooks' import { redirectToUrl } from 'helpers/redirectToUrl' import { parseDate } from 'helpers/parseDate' +import { setLocalStorageItem } from 'helpers/getLocalStorage' import { useTournamentData } from './useTournamentData' import { useMatchData } from './useMatchData' @@ -192,19 +195,25 @@ export const useMatchPage = () => { const { circleAnimation: statsCircleAnimation, filteredEvents: statsFilteredEvents, + isExpanded, isPlayersStatsFetching, isPlayFilterEpisodes: isStatsPlayFilterEpisodes, isTeamsStatsFetching, plaingOrder: statsPlaingOrder, playEpisodes: playStatsEpisodes, playNextEpisode: playStatsNextEpisode, + reduceTable, + selectedStatsTable, setCircleAnimation: setStatsCircleAnimation, setIsPlayersStatsFetching, setIsPlayingFiltersEpisodes: setStatsIsPlayinFiltersEpisodes, setIsTeamsStatsFetching, setPlaingOrder: setStatsPlaingOrder, + setSelectedStatsTable, + setStatsType, setWatchAllEpisodesTimer: setStatsWatchAllEpisodesTimer, statsType, + toggleIsExpanded, toggleStatsType, watchAllEpisodesTimer: statsWatchAllEpisodesTimer, } = useStatsTab({ @@ -214,7 +223,12 @@ export const useMatchPage = () => { selectedPlaylist, }) - const { teamsStats } = useTeamsStats({ + const { + beforeCloseTourCallback: beforeCloseTourCallbackTeams, + getFirstClickableParam, + isClickable, + teamsStats, + } = useTeamsStats({ matchProfile, playingProgress, selectedPlaylist, @@ -223,6 +237,8 @@ export const useMatchPage = () => { }) const { + beforeCloseTourCallback: beforeCloseTourCallbackPlayers, + getParams, isEmptyPlayersStats, playersData, playersStats, @@ -234,6 +250,15 @@ export const useMatchPage = () => { statsType, }) + const beforeCloseTourCallback = () => { + beforeCloseTourCallbackPlayers() + beforeCloseTourCallbackTeams() + + setStatsType(profile?.live ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS) + isExpanded && toggleIsExpanded() + setLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY, 'true') + } + const isStarted = useMemo(() => ( profile?.date ? parseDate(profile.date) < new Date() @@ -310,17 +335,22 @@ export const useMatchPage = () => { allActionsToggle, allPlayersToggle, applyFilters, + beforeCloseTourCallback, circleAnimation: isStatsTab ? statsCircleAnimation : circleAnimation, closePopup, countOfFilters, disablePlayingEpisodes, events, filteredEvents: isStatsTab ? statsFilteredEvents : filteredEvents, + getFirstClickableParam, + getParams, handlePlaylistClick, hideProfileCard, isAllActionsChecked, + isClickable, isEmptyFilters, isEmptyPlayersStats, + isExpanded, isFirstTeamPlayersChecked, isLiveMatch, isOpenFiltersPopup, @@ -342,8 +372,10 @@ export const useMatchPage = () => { playingProgress, profile, profileCardShown, + reduceTable, reversedGroupEvents, selectedPlaylist, + selectedStatsTable, selectedTab, setCircleAnimation: isStatsTab ? setStatsCircleAnimation : setCircleAnimation, setFullMatchPlaylistDuration, @@ -354,7 +386,9 @@ export const useMatchPage = () => { setPlayingData, setPlayingProgress, setReversed, + setSelectedStatsTable, setSelectedTab, + setStatsType, setUnreversed, setWatchAllEpisodesTimer: isStatsTab ? setStatsWatchAllEpisodesTimer : setWatchAllEpisodesTimer, showProfileCard, @@ -362,6 +396,7 @@ export const useMatchPage = () => { teamsStats, toggleActiveEvents, toggleActivePlayers, + toggleIsExpanded, togglePopup, toggleStatsType, tournamentData, diff --git a/src/features/MatchPage/store/hooks/useFakeData.tsx b/src/features/MatchPage/store/hooks/useFakeData.tsx new file mode 100644 index 00000000..ac5c477e --- /dev/null +++ b/src/features/MatchPage/store/hooks/useFakeData.tsx @@ -0,0 +1,3513 @@ +/* eslint-disable sort-keys */ +import { useMemo } from 'react' + +import type { MatchInfo } from 'requests' + +export const useFakeData = (matchProfile: MatchInfo) => useMemo(() => { + if (!matchProfile?.team1.id) { + return { + playersData: [ + { + players: [], + }, + { + players: [], + }, + ], + playersStats: {}, + teamsStats: {}, + } + } + + return { + playersData: [ + { + team_id: matchProfile.team1.id, + players: [ + { + id: 121186, + firstname_eng: 'Gianfranco', + firstname_rus: 'Джанфранко', + lastname_eng: 'Gazzaniga Farias', + lastname_rus: 'Газзанига Фариас', + nickname_eng: 'Gianfranco Gazzaniga', + nickname_rus: 'Gianfranco Gazzaniga', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 186, + weight: 85, + is_gk: true, + c_country: 12, + birthday: '1993-11-22', + firstname_national: 'Gianfranco', + lastname_national: 'Gazzaniga Farías', + club_shirt_num: 13, + national_shirt_num: null, + num: 13, + ord: 1, + }, + { + id: 1206209, + firstname_eng: 'Brais', + firstname_rus: 'Брайс', + lastname_eng: 'Martinez Prado', + lastname_rus: 'Мартинез Прадо', + nickname_eng: 'Brais Martinez', + nickname_rus: 'Брейс Мартинез', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 175, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2001-12-13', + firstname_national: null, + lastname_national: null, + club_shirt_num: 18, + national_shirt_num: null, + num: 18, + ord: 2, + }, + { + id: 202317, + firstname_eng: 'Quique', + firstname_rus: 'Энрике', + lastname_eng: 'Fornos Dominguez', + lastname_rus: 'Форнос Домингуез', + nickname_eng: 'Quique Fornos', + nickname_rus: 'Энрике Форнос', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 179, + weight: 69, + is_gk: false, + c_country: 77, + birthday: '1997-01-01', + firstname_national: 'Enrique', + lastname_national: 'Fornos Domínguez', + club_shirt_num: 5, + national_shirt_num: 15, + num: 5, + ord: 3, + }, + { + id: 571713, + firstname_eng: 'Tomas Enrique', + firstname_rus: 'Томас Энрике', + lastname_eng: 'Bourdel', + lastname_rus: 'Бурдель', + nickname_eng: 'Tomas Bourdal', + nickname_rus: 'Tomas Bourdal', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 192, + weight: null, + is_gk: false, + c_country: 77, + birthday: '1998-04-27', + firstname_national: null, + lastname_national: null, + club_shirt_num: 2, + national_shirt_num: null, + num: 2, + ord: 4, + }, + { + id: 287877, + firstname_eng: 'Aitor', + firstname_rus: 'Айтор', + lastname_eng: 'Pascual Sejias', + lastname_rus: 'Паскуаль Съежиась', + nickname_eng: 'Aitor Pascual', + nickname_rus: 'Aitor Pascual', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 179, + weight: 74, + is_gk: false, + c_country: 77, + birthday: '1998-10-10', + firstname_national: 'Aitor', + lastname_national: 'Pascual', + club_shirt_num: 3, + national_shirt_num: null, + num: 3, + ord: 5, + }, + { + id: 27677, + firstname_eng: 'Alejandro', + firstname_rus: 'Алекс Санчес', + lastname_eng: 'Lopez Sanchez', + lastname_rus: 'Лопез Санчез', + nickname_eng: 'Alex Lopez', + nickname_rus: 'Алекс Лопез', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 178, + weight: 71, + is_gk: false, + c_country: 77, + birthday: '1988-01-11', + firstname_national: 'Alejandro', + lastname_national: 'López Sánchez', + club_shirt_num: 8, + national_shirt_num: null, + num: 8, + ord: 6, + }, + { + id: 470300, + firstname_eng: 'Jesus Jose', + firstname_rus: 'Хесус Жозе', + lastname_eng: 'Bernal Villarig', + lastname_rus: 'Берналь Виллариг', + nickname_eng: 'Jesus Bernal', + nickname_rus: 'Jesus Bernal', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 183, + weight: null, + is_gk: false, + c_country: 77, + birthday: '1996-12-25', + firstname_national: null, + lastname_national: null, + club_shirt_num: 6, + national_shirt_num: null, + num: 6, + ord: 7, + }, + { + id: 259653, + firstname_eng: 'Francisco', + firstname_rus: 'Франсиско', + lastname_eng: 'Lopez de la Manzanara Delgado', + lastname_rus: 'Лопез Де Ла Манзанара Дельгадо', + nickname_eng: 'Fran Manzanara', + nickname_rus: 'Fran Manzanara', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 183, + weight: 75, + is_gk: false, + c_country: 77, + birthday: '1996-09-12', + firstname_national: 'Francisco', + lastname_national: 'López de la Manzanara', + club_shirt_num: 16, + national_shirt_num: null, + num: 16, + ord: 8, + }, + { + id: 498854, + firstname_eng: 'Jorge', + firstname_rus: 'Жорже', + lastname_eng: 'Padilla Soler', + lastname_rus: 'Падилья', + nickname_eng: null, + nickname_rus: null, + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 175, + weight: 66, + is_gk: false, + c_country: 77, + birthday: '2001-04-23', + firstname_national: null, + lastname_national: null, + club_shirt_num: 19, + national_shirt_num: null, + num: 19, + ord: 9, + }, + { + id: 254024, + firstname_eng: 'Heber', + firstname_rus: 'Хебер', + lastname_eng: 'Pena Picos', + lastname_rus: 'Пена Пикус', + nickname_eng: 'Heber Pena', + nickname_rus: 'Heber Pena', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 176, + weight: 68, + is_gk: false, + c_country: 77, + birthday: '1990-01-16', + firstname_national: 'Héber', + lastname_national: 'Pena Picos', + club_shirt_num: 7, + national_shirt_num: null, + num: 7, + ord: 10, + }, + { + id: 463488, + firstname_eng: 'Luis', + firstname_rus: 'Луис', + lastname_eng: 'Rodriguez Chacon', + lastname_rus: 'Родригес Чакон', + nickname_eng: 'Luis Chacon', + nickname_rus: 'Luis Chacon', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: null, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2000-05-30', + firstname_national: null, + lastname_national: null, + club_shirt_num: 11, + national_shirt_num: null, + num: 11, + ord: 11, + }, + { + id: 303350, + firstname_eng: 'David', + firstname_rus: 'Давид', + lastname_eng: 'Del Pozo Guillen', + lastname_rus: 'Дел Посо Гильен', + nickname_eng: 'David Del Pozo', + nickname_rus: 'David Del Pozo', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 178, + weight: null, + is_gk: false, + c_country: 77, + birthday: '1997-06-05', + firstname_national: 'David', + lastname_national: 'Del Pozo Guillén', + club_shirt_num: 14, + national_shirt_num: null, + num: 14, + ord: 12, + }, + { + id: 923500, + firstname_eng: 'Carlos', + firstname_rus: 'Карлос', + lastname_eng: 'Vicente Robles', + lastname_rus: 'Висенте Роблес', + nickname_eng: 'Carlos Vicente', + nickname_rus: 'Carlos Vicente', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 179, + weight: null, + is_gk: false, + c_country: 77, + birthday: '1999-04-23', + firstname_national: null, + lastname_national: null, + club_shirt_num: 23, + national_shirt_num: null, + num: 23, + ord: 13, + }, + { + id: 1081850, + firstname_eng: 'Jaume', + firstname_rus: 'Жауме', + lastname_eng: 'Jardi Poyato', + lastname_rus: 'Джарди Поято', + nickname_eng: 'Jaume Jardi', + nickname_rus: 'Jaume Jardi', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 175, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2002-04-07', + firstname_national: null, + lastname_national: null, + club_shirt_num: 21, + national_shirt_num: null, + num: 21, + ord: 14, + }, + { + id: 44982, + firstname_eng: 'Jose Luis', + firstname_rus: 'Жозе Луис', + lastname_eng: 'Gomez Perez', + lastname_rus: 'Гомес Перес', + nickname_eng: 'Joselu', + nickname_rus: 'Хоселу', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 182, + weight: 78, + is_gk: false, + c_country: 77, + birthday: '1987-06-10', + firstname_national: 'José Luis', + lastname_national: 'Gómez Pérez', + club_shirt_num: 22, + national_shirt_num: null, + num: 22, + ord: 15, + }, + { + id: 469074, + firstname_eng: 'Manuel', + firstname_rus: 'Мануэль', + lastname_eng: 'Justo Roman', + lastname_rus: 'Юсто Роман', + nickname_eng: 'Manu Justo', + nickname_rus: 'Manu Justo', + club_f_team: 6330, + national_f_team: null, + c_gender: 1, + height: 174, + weight: 77, + is_gk: false, + c_country: 77, + birthday: '1996-02-09', + firstname_national: null, + lastname_national: null, + club_shirt_num: 9, + national_shirt_num: null, + num: 9, + ord: 16, + }, + ], + }, + { + team_id: matchProfile.team2.id, + players: [ + { + id: 954413, + firstname_eng: 'Ernestas', + firstname_rus: 'Эрнестас', + lastname_eng: 'Juskevicius', + lastname_rus: 'Юшкявичюс', + nickname_eng: 'Ernestas Juskevicius', + nickname_rus: 'Эрнестас Юшкявичюс', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 188, + weight: null, + is_gk: true, + c_country: 77, + birthday: '1999-04-16', + firstname_national: null, + lastname_national: null, + club_shirt_num: 1, + national_shirt_num: null, + num: 1, + ord: 1, + }, + { + id: 1206758, + firstname_eng: 'Ivan', + firstname_rus: 'Иван', + lastname_eng: 'Serrano Garcia', + lastname_rus: 'Серрано Гарсиа', + nickname_eng: 'Ivan Serrano', + nickname_rus: 'Иван Серрано', + club_f_team: 7240, + national_f_team: null, + c_gender: 1, + height: null, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2001-01-07', + firstname_national: null, + lastname_national: null, + club_shirt_num: 11, + national_shirt_num: null, + num: 3, + ord: 2, + }, + { + id: 484081, + firstname_eng: 'Eduardo Viana', + firstname_rus: 'Эдуардо Виана', + lastname_eng: 'Viana Campuzano', + lastname_rus: 'Виана Кампусано', + nickname_eng: 'Edu Viana', + nickname_rus: 'Edu Viana', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 174, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2000-02-24', + firstname_national: null, + lastname_national: null, + club_shirt_num: 3, + national_shirt_num: null, + num: 20, + ord: 3, + }, + { + id: 161842, + firstname_eng: 'Emmanuel', + firstname_rus: 'Эммануэль', + lastname_eng: 'Gonzalez Rodriguez', + lastname_rus: 'Гонзалез Родригес', + nickname_eng: 'Lolo Gonzalez', + nickname_rus: 'Лоло Гонзалез', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 186, + weight: 83, + is_gk: false, + c_country: 77, + birthday: '1991-07-22', + firstname_national: 'Emmanuel', + lastname_national: 'González Rodríguez', + club_shirt_num: 5, + national_shirt_num: null, + num: 5, + ord: 4, + }, + { + id: 121819, + firstname_eng: 'Jose Antonio', + firstname_rus: 'Жозе Антонио', + lastname_eng: 'Caro Martinez', + lastname_rus: 'Каро Мартинез', + nickname_eng: 'Jose Caro', + nickname_rus: 'Жозе Каро', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 183, + weight: 70, + is_gk: false, + c_country: 77, + birthday: '1993-03-08', + firstname_national: 'José Antonio', + lastname_national: 'Caro Martínez', + club_shirt_num: 16, + national_shirt_num: null, + num: 16, + ord: 5, + }, + { + id: 911431, + firstname_eng: 'Javier', + firstname_rus: 'Хавьер', + lastname_eng: 'Duarte Egea', + lastname_rus: 'Дуарте Егеа', + nickname_eng: 'Javi Duarte', + nickname_rus: 'Хави Дуарте', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 175, + weight: 64, + is_gk: false, + c_country: 77, + birthday: '1997-12-22', + firstname_national: null, + lastname_national: null, + club_shirt_num: 15, + national_shirt_num: null, + num: 15, + ord: 6, + }, + { + id: 1095160, + firstname_eng: 'Aitor', + firstname_rus: 'Айтор', + lastname_eng: 'Gelardo Vegara', + lastname_rus: 'Гелардо Вегара', + nickname_eng: null, + nickname_rus: null, + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 180, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2002-06-26', + firstname_national: null, + lastname_national: null, + club_shirt_num: 19, + national_shirt_num: 6, + num: 19, + ord: 7, + }, + { + id: 322813, + firstname_eng: 'Alberto', + firstname_rus: 'Алберто', + lastname_eng: 'Rodriguez Exposito', + lastname_rus: 'Родригес Экспосито', + nickname_eng: 'Rodri', + nickname_rus: 'Rodri', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: null, + weight: null, + is_gk: false, + c_country: 77, + birthday: '1989-02-28', + firstname_national: null, + lastname_national: null, + club_shirt_num: 8, + national_shirt_num: null, + num: 8, + ord: 8, + }, + { + id: 473579, + firstname_eng: 'Francisco', + firstname_rus: 'Франсиско', + lastname_eng: 'Callejon Segura', + lastname_rus: 'Кальехон Сегура', + nickname_eng: 'Fran Callejon', + nickname_rus: 'Fran Callejon', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 182, + weight: 68, + is_gk: false, + c_country: 77, + birthday: '1998-05-15', + firstname_national: null, + lastname_national: null, + club_shirt_num: 21, + national_shirt_num: null, + num: 21, + ord: 9, + }, + { + id: 1410436, + firstname_eng: 'Fermin', + firstname_rus: 'Фермин', + lastname_eng: 'Lopez Martin', + lastname_rus: 'Лопез Марин', + nickname_eng: 'Fermin', + nickname_rus: 'Фермин', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: null, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2003-05-11', + firstname_national: null, + lastname_national: null, + club_shirt_num: 10, + national_shirt_num: null, + num: 10, + ord: 10, + }, + { + id: 473112, + firstname_eng: 'Francisco', + firstname_rus: 'Франсиско', + lastname_eng: 'Garcia Murillo', + lastname_rus: 'Гарсиа Мурильо', + nickname_eng: null, + nickname_rus: null, + club_f_team: 35953, + national_f_team: null, + c_gender: 1, + height: 173, + weight: null, + is_gk: false, + c_country: 77, + birthday: '1993-03-19', + firstname_national: null, + lastname_national: null, + club_shirt_num: 9, + national_shirt_num: null, + num: 9, + ord: 11, + }, + { + id: 423681, + firstname_eng: 'Samuel', + firstname_rus: 'Самуэль', + lastname_eng: 'Corral Valero', + lastname_rus: 'Коррал Валеро', + nickname_eng: 'Samu Corral', + nickname_rus: 'Samu Corral', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 184, + weight: 81, + is_gk: false, + c_country: 77, + birthday: '1992-04-03', + firstname_national: 'Samuel', + lastname_national: 'Corral Valero', + club_shirt_num: 14, + national_shirt_num: null, + num: 14, + ord: 12, + }, + { + id: 1269294, + firstname_eng: 'Antonio Jesus', + firstname_rus: 'Антонио Хесус', + lastname_eng: 'Canete Vidal', + lastname_rus: 'Каньете Видаль', + nickname_eng: 'Antonio Canete', + nickname_rus: 'Антонио Каньете', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 197, + weight: null, + is_gk: false, + c_country: 77, + birthday: '2002-03-05', + firstname_national: null, + lastname_national: null, + club_shirt_num: 4, + national_shirt_num: null, + num: 4, + ord: 13, + }, + { + id: 918048, + firstname_eng: 'Alejandro San', + firstname_rus: 'Александр Сан -', + lastname_eng: 'Cristobal Sanchez', + lastname_rus: 'Кристобаль Санчез', + nickname_eng: 'Alex Sancris', + nickname_rus: 'Alex Sancris', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 177, + weight: 72, + is_gk: false, + c_country: 77, + birthday: '1997-01-18', + firstname_national: null, + lastname_national: null, + club_shirt_num: 22, + national_shirt_num: null, + num: 22, + ord: 14, + }, + { + id: 406433, + firstname_eng: 'Alfonso Jesus', + firstname_rus: 'Альфонсо Хесус', + lastname_eng: 'Fernandez', + lastname_rus: 'Фернандез', + nickname_eng: 'Alfonso Fernandez', + nickname_rus: 'Alfonso Fernandez', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 179, + weight: 66, + is_gk: false, + c_country: 77, + birthday: '1994-11-25', + firstname_national: 'Alfonso', + lastname_national: 'Fernández', + club_shirt_num: 11, + national_shirt_num: null, + num: 11, + ord: 15, + }, + { + id: 121632, + firstname_eng: 'Antonio Luis', + firstname_rus: 'Антонио Луис', + lastname_eng: 'Diaz Sanchez', + lastname_rus: 'Диаз Санчез', + nickname_eng: 'Hugo Diaz', + nickname_rus: 'Хьюго Диаз', + club_f_team: 9114, + national_f_team: null, + c_gender: 1, + height: 174, + weight: 68, + is_gk: false, + c_country: 77, + birthday: '1988-02-09', + firstname_national: 'Antonio Luis', + lastname_national: 'Díaz Sánchez', + club_shirt_num: 7, + national_shirt_num: 7, + num: 7, + ord: 16, + }, + ], + }, + ], + playersStats: { + 27677: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 7, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 1, + markers: [ + 30264092, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: null, + markers: null, + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: 1, + markers: [ + 30264092, + ], + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 44982: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 71, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: 1, + markers: [ + 30262274, + ], + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 2, + markers: [ + 30262274, + 30262309, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: 1, + markers: [ + 30262274, + ], + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: null, + markers: null, + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: 1, + markers: [ + 30263391, + ], + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: 1, + markers: [ + 30262309, + ], + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: 1, + markers: [ + 30262274, + ], + }, + }, + 121186: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 94, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: null, + markers: null, + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 202317: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 94, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: 1, + markers: [ + 30263621, + ], + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 2, + markers: [ + 30263621, + 30263758, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: 1, + markers: [ + 30263621, + ], + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 3, + markers: [ + 30262523, + 30262835, + 30264010, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 2, + markers: [ + 30262186, + 30262708, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: 1, + markers: [ + 30263758, + ], + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: 1, + markers: [ + 30263621, + ], + }, + }, + 254024: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 56, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 1, + markers: [ + 30263304, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 3, + markers: [ + 30262104, + 30262400, + 30262692, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: 1, + markers: [ + 30262169, + ], + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 259653: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 87, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 1, + markers: [ + 30262272, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: 1, + markers: [ + 30262272, + ], + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 3, + markers: [ + 30263446, + 30263578, + 30263953, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 3, + markers: [ + 30262494, + 30262907, + 30263396, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: 1, + markers: [ + 30263579, + ], + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 287877: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 94, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 1, + markers: [ + 30264052, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 2, + markers: [ + 30263382, + 30263541, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 303350: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 23, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: null, + markers: null, + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 463488: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 71, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 4, + markers: [ + 30262551, + 30262855, + 30263387, + 30263413, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: 2, + markers: [ + 30262551, + 30263387, + ], + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 1, + markers: [ + 30263410, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: 1, + markers: [ + 30263413, + ], + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: 1, + markers: [ + 30262855, + ], + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 469074: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 23, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: null, + markers: null, + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 470300: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 94, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 2, + markers: [ + 30262139, + 30262481, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 1, + markers: [ + 30262116, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: 1, + markers: [ + 30262482, + ], + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 498854: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 23, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 1, + markers: [ + 30263877, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 3, + markers: [ + 30263652, + 30263713, + 30263841, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 571713: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 94, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: 1, + markers: [ + 30263613, + ], + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 1, + markers: [ + 30263656, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: null, + markers: null, + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 1, + markers: [ + 30262648, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: 1, + markers: [ + 30263656, + ], + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 923500: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 71, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 1, + markers: [ + 30262232, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 1, + markers: [ + 30262953, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 1, + markers: [ + 30262746, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: 1, + markers: [ + 30262232, + ], + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 1081850: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 39, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: null, + markers: null, + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 1, + markers: [ + 30263523, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 2, + markers: [ + 30263750, + 30263985, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: null, + markers: null, + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + 1206209: { + 1: { + id: 1, + name_en: 'Minutes played', + name_ru: null, + lexic: 3172, + clickable: false, + data_type: 'num', + lexica_short: 3093, + val: 94, + markers: null, + }, + 2: { + id: 2, + name_en: 'Goals', + name_ru: null, + lexic: 3173, + clickable: true, + data_type: 'num', + lexica_short: 3094, + val: null, + markers: null, + }, + 3: { + id: 3, + name_en: 'Assists', + name_ru: null, + lexic: 3174, + clickable: true, + data_type: 'num', + lexica_short: 3095, + val: null, + markers: null, + }, + 4: { + id: 4, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: 3102, + val: 1, + markers: [ + 30263608, + ], + }, + 5: { + id: 5, + name_en: 'shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: 3103, + val: null, + markers: null, + }, + 6: { + id: 6, + name_en: 'Fouls', + name_ru: null, + lexic: 413, + clickable: true, + data_type: 'num', + lexica_short: 3136, + val: 3, + markers: [ + 30261973, + 30262023, + 30262763, + ], + }, + 7: { + id: 7, + name_en: 'Fouls suffered', + name_ru: null, + lexic: 4861, + clickable: true, + data_type: 'num', + lexica_short: 3137, + val: 2, + markers: [ + 30262041, + 30263335, + ], + }, + 8: { + id: 8, + name_en: 'Yellow cards - К (605)', + name_ru: null, + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: null, + markers: null, + }, + 10: { + id: 10, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: 7258, + val: null, + markers: null, + }, + 44: { + id: 44, + name_en: 'Shots wide', + name_ru: 'Удар мимо', + lexic: 535, + clickable: true, + data_type: 'num', + lexica_short: 3104, + val: null, + markers: null, + }, + 45: { + id: 45, + name_en: 'Shots blocked', + name_ru: 'Удары перехваченные', + lexic: 1048, + clickable: true, + data_type: 'num', + lexica_short: 6295, + val: 1, + markers: [ + 30263608, + ], + }, + 51: { + id: 51, + name_en: 'Chanses', + name_ru: 'ГМ', + lexic: 3925, + clickable: true, + data_type: 'num', + lexica_short: 6291, + val: null, + markers: null, + }, + }, + match_calc_status: 2, + }, + teamsStats: { + [matchProfile.team1.id]: [ + { + name_en: 'Goals', + name_ru: 'Голы', + lexic: 3173, + order: 1, + param1: { + id: 1, + name_en: 'Goals', + name_ru: null, + lexic: 6142, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 2, + markers: [ + 30262274, + 30263621, + ], + }, + param2: null, + }, + { + name_en: 'Chances', + name_ru: 'Опасные моменты', + lexic: 3925, + order: 2, + param1: { + id: 2, + name_en: 'Chances', + name_ru: null, + lexic: 11841, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 7, + markers: [ + 30262234, + 30262278, + 30262308, + 30262680, + 30263620, + 30263757, + 30264006, + ], + }, + param2: null, + }, + { + name_en: 'Chances, % of conversion', + name_ru: 'Команда-Реализация моментов', + lexic: 4135, + order: 3, + param1: { + id: 4, + name_en: 'Chances, % of conversion', + name_ru: null, + lexic: 4135, + clickable: false, + data_type: 'pct', + lexica_short: null, + val: 29, + markers: null, + }, + param2: null, + }, + { + name_en: 'Shots', + name_ru: 'Удары', + lexic: 3178, + order: 4, + param1: { + id: 5, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 13, + markers: [ + 30262232, + 30262272, + 30262274, + 30262309, + 30262551, + 30262855, + 30263387, + 30263413, + 30263608, + 30263621, + 30263656, + 30263758, + 30264092, + ], + }, + param2: null, + }, + { + name_en: 'Shots on target', + name_ru: 'Удары в створ', + lexic: 2131, + order: 5, + param1: { + id: 6, + name_en: 'Shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 5, + markers: [ + 30262272, + 30262274, + 30262551, + 30263387, + 30263621, + ], + }, + param2: null, + }, + { + name_en: 'Shots on target, %', + name_ru: 'Точные удары, %', + lexic: 3179, + order: 6, + param1: { + id: 7, + name_en: 'Shots on target, %', + name_ru: null, + lexic: 3179, + clickable: false, + data_type: 'pct', + lexica_short: null, + val: 38, + markers: null, + }, + param2: null, + }, + { + name_en: 'Corners', + name_ru: 'Угловые', + lexic: 64, + order: 7, + param1: { + id: 8, + name_en: 'Corners', + name_ru: null, + lexic: 64, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 4, + markers: [ + 35604927, + 35604928, + 35604931, + 35604933, + ], + }, + param2: null, + }, + { + name_en: 'Offsides', + name_ru: 'Офсайды', + lexic: 63, + order: 8, + param1: { + id: 9, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 2, + markers: [ + 30262169, + 30263391, + ], + }, + param2: null, + }, + { + name_en: 'Fouls', + name_ru: 'Фолы', + lexic: 413, + order: 9, + param1: { + id: 10, + name_en: 'Fouls', + name_ru: null, + lexic: 701, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 16, + markers: [ + 30261973, + 30262023, + 30262139, + 30262481, + 30262523, + 30262763, + 30262835, + 30262953, + 30263304, + 30263446, + 30263523, + 30263578, + 30263877, + 30263953, + 30264010, + 30264052, + ], + }, + param2: null, + }, + { + name_en: 'Yellow cards', + name_ru: 'ЖК', + lexic: 605, + order: 10, + param1: { + id: 36, + name_en: 'Yellow cards', + name_ru: 'ЖК', + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: 2, + markers: [ + 30262482, + 30263579, + ], + }, + param2: null, + }, + ], + [matchProfile.team2.id]: [ + { + name_en: 'Goals', + name_ru: 'Голы', + lexic: 3173, + order: 1, + param1: { + id: 1, + name_en: 'Goals', + name_ru: null, + lexic: 6142, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 0, + markers: [], + }, + param2: null, + }, + { + name_en: 'Chances', + name_ru: 'Опасные моменты', + lexic: 3925, + order: 2, + param1: { + id: 2, + name_en: 'Chances', + name_ru: null, + lexic: 11841, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 2, + markers: [ + 30261991, + 30263264, + ], + }, + param2: null, + }, + { + name_en: 'Chances, % of conversion', + name_ru: 'Команда-Реализация моментов', + lexic: 4135, + order: 3, + param1: { + id: 4, + name_en: 'Chances, % of conversion', + name_ru: null, + lexic: 4135, + clickable: false, + data_type: 'pct', + lexica_short: null, + val: 0, + markers: null, + }, + param2: null, + }, + { + name_en: 'Shots', + name_ru: 'Удары', + lexic: 3178, + order: 4, + param1: { + id: 5, + name_en: 'Shots', + name_ru: null, + lexic: 56, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 5, + markers: [ + 30261988, + 30262433, + 30263588, + 30263954, + 30264122, + ], + }, + param2: null, + }, + { + name_en: 'Shots on target', + name_ru: 'Удары в створ', + lexic: 2131, + order: 5, + param1: { + id: 6, + name_en: 'Shots on target', + name_ru: null, + lexic: 2131, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 1, + markers: [ + 30264122, + ], + }, + param2: null, + }, + { + name_en: 'Shots on target, %', + name_ru: 'Точные удары, %', + lexic: 3179, + order: 6, + param1: { + id: 7, + name_en: 'Shots on target, %', + name_ru: null, + lexic: 3179, + clickable: false, + data_type: 'pct', + lexica_short: null, + val: 20, + markers: null, + }, + param2: null, + }, + { + name_en: 'Corners', + name_ru: 'Угловые', + lexic: 64, + order: 7, + param1: { + id: 8, + name_en: 'Corners', + name_ru: null, + lexic: 64, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 5, + markers: [ + 35604929, + 35604930, + 35604932, + 35604934, + 35604935, + ], + }, + param2: null, + }, + { + name_en: 'Offsides', + name_ru: 'Офсайды', + lexic: 63, + order: 8, + param1: { + id: 9, + name_en: 'Offsides', + name_ru: null, + lexic: 63, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 3, + markers: [ + 30262254, + 30262350, + 30263819, + ], + }, + param2: null, + }, + { + name_en: 'Fouls', + name_ru: 'Фолы', + lexic: 413, + order: 9, + param1: { + id: 10, + name_en: 'Fouls', + name_ru: null, + lexic: 701, + clickable: true, + data_type: 'num', + lexica_short: null, + val: 22, + markers: [ + 30262041, + 30262104, + 30262116, + 30262186, + 30262400, + 30262494, + 30262648, + 30262692, + 30262708, + 30262746, + 30262907, + 30263335, + 30263382, + 30263396, + 30263410, + 30263541, + 30263652, + 30263713, + 30263750, + 30263841, + 30263985, + 30264083, + ], + }, + param2: null, + }, + { + name_en: 'Yellow cards', + name_ru: 'ЖК', + lexic: 605, + order: 10, + param1: { + id: 36, + name_en: 'Yellow cards', + name_ru: 'ЖК', + lexic: 605, + clickable: true, + data_type: 'num', + lexica_short: 3096, + val: 1, + markers: [ + 30263986, + ], + }, + param2: null, + }, + ], + match_calc_status: 2, + last_event_id: 30264158, + }, + } +}, [matchProfile?.team1.id, matchProfile?.team2.id]) diff --git a/src/features/MatchPage/store/hooks/usePlayersStats.tsx b/src/features/MatchPage/store/hooks/usePlayersStats.tsx index 4249f023..f8505e5b 100644 --- a/src/features/MatchPage/store/hooks/usePlayersStats.tsx +++ b/src/features/MatchPage/store/hooks/usePlayersStats.tsx @@ -3,6 +3,7 @@ import { useMemo, useEffect, useState, + useCallback, } from 'react' import { useQueryClient } from 'react-query' @@ -11,10 +12,14 @@ import isEmpty from 'lodash/isEmpty' import every from 'lodash/every' import find from 'lodash/find' import isUndefined from 'lodash/isUndefined' +import flatMapDepth from 'lodash/flatMapDepth' +import uniqBy from 'lodash/uniqBy' +import values from 'lodash/values' +import size from 'lodash/size' import { querieKeys } from 'config' -import type { MatchScore } from 'requests' +import type { MatchScore, PlayerParam } from 'requests' import { MatchInfo, PlayersStats, @@ -23,12 +28,20 @@ import { getMatchParticipants, } from 'requests' +import { getLocalStorageItem } from 'helpers/getLocalStorage' + import { useObjectState, usePageParams } from 'hooks' import type{ PlaylistOption } from 'features/MatchPage/types' import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' +import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour' + +import { DISPLAYED_PARAMS_COLUMNS } from 'features/MatchSidePlaylists/components/PlayersTable/config' +import { useFakeData } from './useFakeData' + +type HeaderParam = Pick<PlayerParam, 'id' | 'lexica_short' | 'lexic'> const REQUEST_DELAY = 3000 const STATS_POLL_INTERVAL = 30000 @@ -62,6 +75,8 @@ export const usePlayersStats = ({ sportType, } = usePageParams() + const fakeData = useFakeData(matchProfile) + const client = useQueryClient() const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) @@ -74,6 +89,10 @@ export const usePlayersStats = ({ || isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2']) ) + const getParams = useCallback((stats: PlayersStats) => ( + uniqBy(flatMapDepth(stats, values), 'id') as unknown as Record<string, HeaderParam> + ), []) + const fetchPlayers = useMemo(() => throttle(async (second?: number) => { const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds @@ -116,12 +135,11 @@ export const usePlayersStats = ({ } catch (e) { return Promise.reject(e) } - // eslint-disable-next-line react-hooks/exhaustive-deps }), [ - matchId, + matchScore?.video_bounds, + matchProfile, sportName, - matchProfile?.team1.id, - matchProfile?.team2.id, + matchId, ]) const fetchData = useMemo(() => throttle(async (second?: number) => { @@ -139,14 +157,29 @@ export const usePlayersStats = ({ const team1Players = find(res1, { team_id: matchProfile?.team1.id })?.players || [] const team2Players = find(res1, { team_id: matchProfile?.team2.id })?.players || [] + const needUseFakeData = getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) !== 'true' + && ( + isEmpty(team1Players) + || isEmpty(res2) + || size(getParams(res2)) <= DISPLAYED_PARAMS_COLUMNS + ) + setPlayersData({ - team1: team1Players, - team2: team2Players, + team1: needUseFakeData ? fakeData.playersData[0].players : team1Players, + team2: needUseFakeData ? fakeData.playersData[1].players : team2Players, }) setPlayersStats({ - ...(matchProfile?.team1.id && res2 && { [matchProfile.team1.id]: res2 }), - ...(matchProfile?.team2.id && res3 && { [matchProfile.team2.id]: res3 }), + ...(matchProfile?.team1.id && res2 && { + [matchProfile.team1.id]: needUseFakeData + ? fakeData.playersStats as unknown as PlayersStats + : res2, + }), + ...(matchProfile?.team2.id && res3 && { + [matchProfile.team2.id]: needUseFakeData + ? fakeData.playersStats as unknown as PlayersStats + : res3, + }), }) setIsPlayersStatsFetching(false) @@ -160,8 +193,14 @@ export const usePlayersStats = ({ matchProfile?.live, matchProfile?.c_match_calc_status, setIsPlayersStatsFetching, + getParams, + fakeData, ]) + const beforeCloseTourCallback = () => { + isCurrentStats ? fetchData(playingProgress) : fetchData() + } + useEffect(() => { let interval: NodeJS.Timeout @@ -194,6 +233,8 @@ export const usePlayersStats = ({ ]) return { + beforeCloseTourCallback, + getParams, isEmptyPlayersStats, playersData, playersStats, diff --git a/src/features/MatchPage/store/hooks/useStatsTab.tsx b/src/features/MatchPage/store/hooks/useStatsTab.tsx index 333296d8..fa6762fc 100644 --- a/src/features/MatchPage/store/hooks/useStatsTab.tsx +++ b/src/features/MatchPage/store/hooks/useStatsTab.tsx @@ -14,7 +14,8 @@ import type { EventPlaylistOption, PlaylistOption } from 'features/MatchPage/typ import type { TCircleAnimation } from 'features/CircleAnimationBar' import { initialCircleAnimation } from 'features/CircleAnimationBar' import { PlaylistTypes } from 'features/MatchPage/types' -import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' +import { StatsType, Tabs } from 'features/MatchSidePlaylists/components/TabStats/config' +import { useToggle } from 'hooks' type UseStatsTabArgs = { disablePlayingEpisodes: () => void, @@ -55,6 +56,13 @@ export const useStatsTab = ({ const [isPlayFilterEpisodes, setIsPlayingFiltersEpisodes] = useState(false) const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false) const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation) + const [selectedStatsTable, setSelectedStatsTable] = useState<Tabs>(Tabs.TEAMS) + + const { + close: reduceTable, + isOpen: isExpanded, + toggle: toggleIsExpanded, + } = useToggle() const isFinalStatsType = statsType === StatsType.FINAL_STATS @@ -125,19 +133,25 @@ export const useStatsTab = ({ return { circleAnimation, filteredEvents, + isExpanded, isPlayFilterEpisodes, isPlayersStatsFetching, isTeamsStatsFetching, plaingOrder, playEpisodes, playNextEpisode, + reduceTable, + selectedStatsTable, setCircleAnimation, setIsPlayersStatsFetching, setIsPlayingFiltersEpisodes, setIsTeamsStatsFetching, setPlaingOrder, + setSelectedStatsTable, + setStatsType, setWatchAllEpisodesTimer, statsType, + toggleIsExpanded, toggleStatsType, watchAllEpisodesTimer, } diff --git a/src/features/MatchPage/store/hooks/useTeamsStats.tsx b/src/features/MatchPage/store/hooks/useTeamsStats.tsx index f5f6c589..8017b99d 100644 --- a/src/features/MatchPage/store/hooks/useTeamsStats.tsx +++ b/src/features/MatchPage/store/hooks/useTeamsStats.tsx @@ -3,23 +3,35 @@ import { useEffect, useState, useMemo, + useCallback, } from 'react' import { useQueryClient } from 'react-query' import throttle from 'lodash/throttle' import isUndefined from 'lodash/isUndefined' +import find from 'lodash/find' +import isEmpty from 'lodash/isEmpty' import { querieKeys } from 'config' -import type { MatchInfo, MatchScore } from 'requests' +import type { + MatchInfo, + MatchScore, + Param, +} from 'requests' import { getTeamsStats, TeamStatItem } from 'requests' import { usePageParams } from 'hooks' +import { getLocalStorageItem } from 'helpers/getLocalStorage' + import type { PlaylistOption } from 'features/MatchPage/types' import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config' import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists' import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' +import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour' + +import { useFakeData } from './useFakeData' const REQUEST_DELAY = 3000 const STATS_POLL_INTERVAL = 30000 @@ -32,6 +44,10 @@ type UseTeamsStatsArgs = { statsType: StatsType, } +type TeamsStats = { + [teamId: number]: Array<TeamStatItem>, +} + export const useTeamsStats = ({ matchProfile, playingProgress, @@ -39,18 +55,42 @@ export const useTeamsStats = ({ setIsTeamsStatsFetching, statsType, }: UseTeamsStatsArgs) => { - const [teamsStats, setTeamsStats] = useState<{ - [teamId: string]: Array<TeamStatItem>, - }>({}) + const [teamsStats, setTeamsStats] = useState<TeamsStats>({}) const { profileId: matchId, sportName } = usePageParams() + const fakeData = useFakeData(matchProfile) + const client = useQueryClient() const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) const isCurrentStats = statsType === StatsType.CURRENT_STATS + const isClickable = (param: Param) => ( + Boolean(param.val) && param.clickable + ) + + const getFirstClickableParam = useCallback((stats: TeamsStats) => { + if (isEmpty(stats)) return null + + const statItem = (matchProfile?.team1.id && find( + stats[matchProfile.team1.id], + ({ param1, param2 }) => isClickable(param1) || Boolean(param2 && isClickable(param1)), + )) || (matchProfile?.team2.id && find( + stats[matchProfile.team2.id], + ({ param1, param2 }) => isClickable(param1) || Boolean(param2 && isClickable(param1)), + )) + + if (!statItem) return null + + if (isClickable(statItem.param1)) return statItem.param1 + + return statItem.param2 && isClickable(statItem.param2) + ? statItem.param2 + : null + }, [matchProfile?.team1.id, matchProfile?.team2.id]) + const fetchTeamsStats = useMemo(() => throttle(async (second?: number) => { const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds @@ -68,7 +108,11 @@ export const useTeamsStats = ({ ...(!isUndefined(second) && getHalfTime(videoBounds, second)), }) - setTeamsStats(data) + const needUseFakeData = getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) !== 'true' && !getFirstClickableParam(data) + + const stats = needUseFakeData ? fakeData.teamsStats : data + + setTeamsStats(stats) setIsTeamsStatsFetching(false) // eslint-disable-next-line no-empty @@ -82,8 +126,14 @@ export const useTeamsStats = ({ matchId, setIsTeamsStatsFetching, sportName, + fakeData, + getFirstClickableParam, ]) + const beforeCloseTourCallback = () => { + isCurrentStats ? fetchTeamsStats(playingProgress) : fetchTeamsStats() + } + useEffect(() => { let interval: NodeJS.Timeout @@ -107,6 +157,9 @@ export const useTeamsStats = ({ }, [fetchTeamsStats, playingProgress, isCurrentStats]) return { + beforeCloseTourCallback, + getFirstClickableParam, + isClickable, statsType, teamsStats, } diff --git a/src/features/MatchPage/styled.tsx b/src/features/MatchPage/styled.tsx index 921d44d8..affa412e 100644 --- a/src/features/MatchPage/styled.tsx +++ b/src/features/MatchPage/styled.tsx @@ -1,9 +1,12 @@ import styled, { css } from 'styled-components/macro' -import { devices } from 'config/devices' -import { isMobileDevice } from 'config/userAgent' +import { isMobileDevice, devices } from 'config' -export const Wrapper = styled.div` +type WrapperProps = { + isTourOpen?: boolean, +} + +export const Wrapper = styled.div<WrapperProps>` width: 100%; height: calc(100vh - 115px); margin: 20px 0px 0 10px; @@ -24,6 +27,12 @@ export const Wrapper = styled.div` } ` : ''}; + + ${({ isTourOpen }) => (isTourOpen && isMobileDevice + ? css` + padding: 0 5px; + ` + : '')}; ` export const Container = styled.div` diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx index 8854e436..0380d6da 100644 --- a/src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx @@ -30,6 +30,7 @@ export const usePlayersTable = ({ teamId }: PlayersTableProps) => { isExpanded, paramColumnWidth, params, + paramsCount, showExpandButton, showLeftArrow, showRightArrow, @@ -60,6 +61,7 @@ export const usePlayersTable = ({ teamId }: PlayersTableProps) => { isExpanded, paramColumnWidth, params, + paramsCount, players, showExpandButton, showLeftArrow, diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx index 65c38b1f..5db4b71b 100644 --- a/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx @@ -16,20 +16,15 @@ import { useQueryClient } from 'react-query' import size from 'lodash/size' import isNil from 'lodash/isNil' import reduce from 'lodash/reduce' -import forEach from 'lodash/forEach' import values from 'lodash/values' import map from 'lodash/map' import { isMobileDevice, querieKeys } from 'config' -import type { - PlayerParam, - PlayersStats, - MatchScore, -} from 'requests' +import type { PlayerParam, MatchScore } from 'requests' import { getStatsEvents } from 'requests' -import { usePageParams, useToggle } from 'hooks' +import { usePageParams } from 'hooks' import { useMatchPageStore } from 'features/MatchPage/store' import { useLexicsConfig } from 'features/LexicsStore' @@ -63,47 +58,27 @@ export const useTable = ({ const [paramColumnWidth, setParamColumnWidth] = useState(PARAM_COLUMN_WIDTH_DEFAULT) const { - close: reduceTable, - isOpen: isExpanded, - toggle: toggleIsExpanded, - } = useToggle() - const { + getParams, + isExpanded, playersStats, playingProgress, playStatsEpisodes, profile, + reduceTable, setIsPlayingFiltersEpisodes, setPlayingData, setWatchAllEpisodesTimer, statsType, + toggleIsExpanded, } = useMatchPageStore() + const { profileId, sportType } = usePageParams() const client = useQueryClient() const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) - const params = useMemo(() => ( - reduce<PlayersStats, Record<string, HeaderParam>>( - playersStats[teamId], - (acc, curr) => { - forEach(values(curr), ({ - id, - lexic, - lexica_short, - }) => { - acc[id] = acc[id] || { - id, - lexic, - lexica_short, - } - }) - - return acc - }, - {}, - ) - ), [playersStats, teamId]) + const params = useMemo(() => getParams(playersStats[teamId]), [getParams, playersStats, teamId]) const lexics = useMemo(() => ( reduce<HeaderParam, Array<number>>( @@ -251,6 +226,7 @@ export const useTable = ({ isExpanded, paramColumnWidth, params, + paramsCount, showExpandButton: !isMobileDevice && paramsCount > DISPLAYED_PARAMS_COLUMNS, showLeftArrow, showRightArrow, diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx index 1061daeb..f96d52ad 100644 --- a/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersTable/index.tsx @@ -1,5 +1,7 @@ import { Fragment } from 'react' +import { useTour } from '@reactour/tour' + import map from 'lodash/map' import includes from 'lodash/includes' @@ -11,9 +13,14 @@ import { useLexicsStore } from 'features/LexicsStore' import { useMatchPageStore } from 'features/MatchPage/store' import { Loader } from 'features/Loader' import { defaultTheme } from 'features/Theme/config' +import { Spotlight, Steps } from 'features/MatchTour' import type { PlayersTableProps } from './types' -import { FIRST_COLUMN_WIDTH_DEFAULT, FIRST_COLUMN_WIDTH_EXPANDED } from './config' +import { + DISPLAYED_PARAMS_COLUMNS, + FIRST_COLUMN_WIDTH_DEFAULT, + FIRST_COLUMN_WIDTH_EXPANDED, +} from './config' import { usePlayersTable } from './hooks' import { Cell } from './Cell' import { @@ -44,6 +51,7 @@ export const PlayersTable = (props: PlayersTableProps) => { isExpanded, paramColumnWidth, params, + paramsCount, players, showExpandButton, showLeftArrow, @@ -61,6 +69,7 @@ export const PlayersTable = (props: PlayersTableProps) => { playingData, watchAllEpisodesTimer, } = useMatchPageStore() + const { currentStep, isOpen } = useTour() const firstColumnWidth = isExpanded ? FIRST_COLUMN_WIDTH_EXPANDED : FIRST_COLUMN_WIDTH_DEFAULT @@ -75,21 +84,28 @@ export const PlayersTable = (props: PlayersTableProps) => { <TableWrapper ref={tableWrapperRef} isExpanded={isExpanded} + isOpenTour={Boolean(isOpen)} onScroll={handleScroll} > - {!isExpanded && ( + {!isExpanded && paramsCount > DISPLAYED_PARAMS_COLUMNS && ( <Fragment> - {showRightArrow && ( - <ArrowButtonRight - aria-label='Scroll to right' - onClick={slideRight} - > - <Arrow direction='right' /> - </ArrowButtonRight> - )} + <ArrowButtonRight + aria-label='Scroll to right' + onClick={slideRight} + visible={showRightArrow} + > + <Arrow direction='right' data-step={Steps.ShowMoreStats} /> + {Boolean(currentStep === Steps.ShowMoreStats && isOpen) && ( + <Spotlight /> + )} + </ArrowButtonRight> </Fragment> )} - <Table role='marquee' aria-live='off'> + <Table + role='marquee' + aria-live='off' + fullWidth={paramsCount <= DISPLAYED_PARAMS_COLUMNS} + > <Header> <Row> <Cell @@ -109,9 +125,13 @@ export const PlayersTable = (props: PlayersTableProps) => { isExpanded={isExpanded} aria-label={isExpanded ? 'Reduce' : 'Expand'} onClick={toggleIsExpanded} + data-step={Steps.ShowLessStats} > <Arrow direction={isExpanded ? 'right' : 'left'} /> <Arrow direction={isExpanded ? 'right' : 'left'} /> + {Boolean(currentStep === Steps.ShowLessStats && isOpen) && ( + <Spotlight /> + )} </ExpandButton> )} </Cell> diff --git a/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx b/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx index 84d1721b..aa70dc05 100644 --- a/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx +++ b/src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx @@ -29,6 +29,7 @@ export const Container = styled.div<ContainerProps>` type TableWrapperProps = { isExpanded?: boolean, + isOpenTour?: boolean, } export const TableWrapper = styled.div<TableWrapperProps>` @@ -54,6 +55,19 @@ export const TableWrapper = styled.div<TableWrapperProps>` ` : '')} + + ${({ isOpenTour }) => (isOpenTour + ? css` + clip-path: none; + ` + : '')} + + ${({ isExpanded, isOpenTour }) => (isOpenTour && isExpanded + ? css` + overflow-x: initial; + ` + : '')} + ${isMobileDevice ? '' : css` @@ -67,12 +81,23 @@ export const TableWrapper = styled.div<TableWrapperProps>` : ''}; ` -export const Table = styled.table` +type TableProps = { + fullWidth?: boolean, +} + +export const Table = styled.table<TableProps>` border-radius: 5px; border-spacing: 0; border-collapse: collapse; letter-spacing: -0.078px; table-layout: fixed; + + ${({ fullWidth }) => (fullWidth + ? css` + width: 100%; + ` + : '') +} ` type ParamShortTitleProps = { @@ -251,7 +276,11 @@ const ArrowButton = styled(ArrowButtonBase)` : ''}; ` -export const ArrowButtonRight = styled(ArrowButton)` +type ArrowButtonRightProps = { + visible?: boolean, +} + +export const ArrowButtonRight = styled(ArrowButton)<ArrowButtonRightProps>` right: 0; border-top-right-radius: 5px; @@ -259,6 +288,13 @@ export const ArrowButtonRight = styled(ArrowButton)` left: auto; right: 7px; } + + ${({ visible }) => (!visible + ? css` + visibility: hidden; + ` + : '') +} ` export const ArrowButtonLeft = styled(ArrowButton)` @@ -276,7 +312,7 @@ export const ExpandButton = styled(ArrowButton)<ExpandButtonProps>` ${Arrow} { left: ${({ isExpanded }) => (isExpanded ? -6 : -2)}px; - :last-child { + :last-of-type { margin-left: 7px; } } diff --git a/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx b/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx index ea8df099..79f8c957 100644 --- a/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx +++ b/src/features/MatchSidePlaylists/components/TabStats/hooks.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import isEmpty from 'lodash/isEmpty' @@ -9,11 +9,11 @@ import { useMatchPageStore } from 'features/MatchPage/store' import { StatsType, Tabs } from './config' export const useTabStats = () => { - const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS) - const { isEmptyPlayersStats, profile: matchProfile, + selectedStatsTable: selectedTab, + setSelectedStatsTable: setSelectedTab, statsType, teamsStats, toggleStatsType, @@ -56,7 +56,12 @@ export const useTabStats = () => { default: } - }, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab]) + }, [ + isVisibleTeam1PlayersTab, + isVisibleTeam2PlayersTab, + isVisibleTeamsTab, + setSelectedTab, + ]) return { isFinalStatsType, diff --git a/src/features/MatchSidePlaylists/components/TabStats/index.tsx b/src/features/MatchSidePlaylists/components/TabStats/index.tsx index 297e4465..e059deff 100644 --- a/src/features/MatchSidePlaylists/components/TabStats/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabStats/index.tsx @@ -1,6 +1,10 @@ import type { ComponentProps } from 'react' import { createPortal } from 'react-dom' +import { useTour } from '@reactour/tour' + +import includes from 'lodash/includes' + import { isMobileDevice } from 'config' import { getTeamAbbr } from 'helpers' @@ -11,6 +15,7 @@ import { T9n } from 'features/T9n' import { useMatchPageStore } from 'features/MatchPage/store' import { Name } from 'features/Name' import { useLexicsStore } from 'features/LexicsStore' +import { Spotlight, Steps } from 'features/MatchTour' import { Tabs } from './config' import { useTabStats } from './hooks' @@ -59,6 +64,8 @@ export const TabStats = () => { const modalRoot = useModalRoot() + const { currentStep, isOpen } = useTour() + const TabPane = tabPanes[selectedTab] if (!matchProfile) return null @@ -73,16 +80,21 @@ export const TabStats = () => { <Tab aria-pressed={selectedTab === Tabs.TEAMS} onClick={() => setSelectedTab(Tabs.TEAMS)} + data-step={Steps.TeamsTab} > <TabTitle> <T9n t='team' /> </TabTitle> + {Boolean(currentStep === Steps.TeamsTab && isOpen) && ( + <Spotlight /> + )} </Tab> )} {isVisibleTeam1PlayersTab && ( <Tab aria-pressed={selectedTab === Tabs.TEAM1} onClick={() => setSelectedTab(Tabs.TEAM1)} + data-step={Steps.PlayersTab} > <TabTitle teamColor={team1.shirt_color} @@ -104,6 +116,9 @@ export const TabStats = () => { }} /> </TabTitle> + {Boolean(currentStep === Steps.PlayersTab && isOpen) && ( + <Spotlight /> + )} </Tab> )} {isVisibleTeam2PlayersTab && ( @@ -134,7 +149,15 @@ export const TabStats = () => { </Tab> )} </TabList> - <Switch> + <Switch + data-step={Steps.FinalStats} + highlighted={Boolean(isOpen) && includes( + isMobileDevice + ? [Steps.ShowLessStats, Steps.FinalStats] + : [Steps.FinalStats, Steps.CurrentStats], + currentStep, + )} + > <SwitchTitle t={switchTitleLexic} /> <SwitchButton id='switchButton' diff --git a/src/features/MatchSidePlaylists/components/TabStats/styled.tsx b/src/features/MatchSidePlaylists/components/TabStats/styled.tsx index a69c70c1..0f053b2f 100644 --- a/src/features/MatchSidePlaylists/components/TabStats/styled.tsx +++ b/src/features/MatchSidePlaylists/components/TabStats/styled.tsx @@ -1,5 +1,7 @@ import styled, { css } from 'styled-components/macro' +import { isMobileDevice } from 'config' + import { TooltipWrapper } from 'features/Tooltip' import { T9n } from 'features/T9n' @@ -62,7 +64,7 @@ export const TabTitle = styled.span<TabTitleProps>` export const Tab = styled.button.attrs({ role: 'tab' })` position: relative; display: flex; - justify-content: space-between; + justify-content: center; align-items: center; padding: 0 10px 10px; font-size: 12px; @@ -78,10 +80,36 @@ export const Tab = styled.button.attrs({ role: 'tab' })` color: ${({ theme }) => theme.colors.white}; } } + + ${isMobileDevice + ? css` + width: 20vw; + ` + : '' +} ` -export const Switch = styled.div` +type SwitchProps = { + highlighted?: boolean, +} + +export const Switch = styled.div<SwitchProps>` + position: relative; display: flex; + justify-content: center; + align-items: center; + height: 27px; + margin-top: -5px; + padding: 0 5px; + border: 1px solid transparent; + + ${({ highlighted }) => (highlighted + ? css` + border-radius: 6px; + border-color: #0057FF; + background: radial-gradient(50% 50% at 50% 50%, rgba(0, 87, 255, 0) 70.25%, rgba(0, 87, 255, 0.4) 100%); + ` + : '')} ` export const SwitchTitle = styled(T9n)` @@ -98,7 +126,6 @@ export const SwitchButton = styled.button<SwitchButtonProps>` width: 20px; height: 7px; margin-left: 5px; - margin-top: 5px; border-radius: 2px; border: none; border: 1px solid ${({ theme }) => theme.colors.white}; diff --git a/src/features/MatchSidePlaylists/components/TeamsStatsTable/Cell.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/Cell.tsx index 4f0a183d..83975631 100644 --- a/src/features/MatchSidePlaylists/components/TeamsStatsTable/Cell.tsx +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/Cell.tsx @@ -1,6 +1,8 @@ import { Fragment, useRef } from 'react' import { useQueryClient } from 'react-query' +import { useTour } from '@reactour/tour' + import isNumber from 'lodash/isNumber' import { KEYBOARD_KEYS, querieKeys } from 'config' @@ -16,6 +18,7 @@ import { usePageParams, useEventListener } from 'hooks' import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime' import { useMatchPageStore } from 'features/MatchPage/store' +import { Spotlight, Steps } from 'features/MatchTour' import { StatsType } from '../TabStats/config' import { CircleAnimationBar } from '../CircleAnimationBar' @@ -28,19 +31,22 @@ import { } from './styled' type CellProps = { + firstClickableParam: Param | null, teamId: number, teamStatItem: TeamStatItem | null, } export const Cell = ({ + firstClickableParam, teamId, teamStatItem, }: CellProps) => { - const paramValueContainerRef = useRef(null) + const paramValueContainerRef = useRef<HTMLDivElement>(null) const { profileId, sportType } = usePageParams() const { + isClickable, playingData, playingProgress, playStatsEpisodes, @@ -52,14 +58,12 @@ export const Cell = ({ watchAllEpisodesTimer, } = useMatchPageStore() + const { currentStep, isOpen } = useTour() + const client = useQueryClient() const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore) - const isClickable = (param: Param) => ( - Boolean(param.val) && param.clickable - ) - const getDisplayedValue = (val: number | null) => ( isNumber(val) ? String(val) : '-' ) @@ -137,8 +141,15 @@ export const Cell = ({ onClick={() => onParamClick(teamStatItem.param1)} data-param-id={teamStatItem.param1.id} hasValue={Boolean(teamStatItem.param1.val)} + // eslint-disable-next-line react/jsx-props-no-spreading + {...firstClickableParam === teamStatItem.param1 && { + 'data-step': Steps.ClickToWatchPlaylist, + }} > {getDisplayedValue(teamStatItem.param1.val)} + {firstClickableParam === teamStatItem.param1 + && Boolean(currentStep === Steps.ClickToWatchPlaylist && isOpen) + && <Spotlight />} </ParamValue> )} @@ -163,8 +174,15 @@ export const Cell = ({ onClick={() => onParamClick(teamStatItem.param2!)} data-param-id={teamStatItem.param2.id} hasValue={Boolean(teamStatItem.param2.val)} + // eslint-disable-next-line react/jsx-props-no-spreading + {...firstClickableParam === teamStatItem.param2 && { + 'data-step': Steps.ClickToWatchPlaylist, + }} > {getDisplayedValue(teamStatItem.param2.val)} + {firstClickableParam === teamStatItem.param2 + && Boolean(currentStep === Steps.ClickToWatchPlaylist && isOpen) + && <Spotlight />} </ParamValue> </Fragment> )} diff --git a/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx index 035d2eee..257b8ca2 100644 --- a/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx @@ -6,6 +6,7 @@ import { useMatchPageStore } from 'features/MatchPage/store' export const useTeamsStatsTable = () => { const { + getFirstClickableParam, plaingOrder, profile, setCircleAnimation, @@ -26,6 +27,7 @@ export const useTeamsStatsTable = () => { }, [setCircleAnimation, plaingOrder]) return { + firstClickableParam: getFirstClickableParam(teamsStats), getStatItemById, } } diff --git a/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx index a67c3284..c1c05cde 100644 --- a/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx @@ -1,3 +1,5 @@ +import { useTour } from '@reactour/tour' + import map from 'lodash/map' import { useMatchPageStore } from 'features/MatchPage/store' @@ -25,10 +27,15 @@ export const TeamsStatsTable = () => { teamsStats, } = useMatchPageStore() - const { getStatItemById } = useTeamsStatsTable() + const { + firstClickableParam, + getStatItemById, + } = useTeamsStatsTable() const { shortSuffix } = useLexicsStore() + const { isOpen } = useTour() + if (!profile) return null if (isTeamsStatsFetching) { @@ -39,7 +46,7 @@ export const TeamsStatsTable = () => { return ( <Container> - <TableWrapper> + <TableWrapper isOpenTour={Boolean(isOpen)}> <Table role='marquee' aria-live='off'> <Header> <Row> @@ -69,6 +76,7 @@ export const TeamsStatsTable = () => { <Cell teamStatItem={team1StatItem} teamId={profile.team1.id} + firstClickableParam={firstClickableParam} /> <CellContainer> @@ -78,6 +86,7 @@ export const TeamsStatsTable = () => { <Cell teamStatItem={team2StatItem} teamId={profile.team2.id} + firstClickableParam={firstClickableParam} /> </Row> ) diff --git a/src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx b/src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx index 42b6713c..dc990ed2 100644 --- a/src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx +++ b/src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx @@ -7,7 +7,11 @@ import { customScrollbar } from 'features/Common' export const Container = styled.div`` -export const TableWrapper = styled.div` +type TableWrapperProps = { + isOpenTour?: boolean, +} + +export const TableWrapper = styled.div<TableWrapperProps>` width: 100%; overflow: auto; font-size: 11px; @@ -20,6 +24,13 @@ export const TableWrapper = styled.div` max-height: calc(100vh - 203px); `}; + ${({ isOpenTour }) => (isOpenTour + ? css` + clip-path: none; + overflow: initial; + ` + : '')} + ${customScrollbar} ` @@ -90,11 +101,11 @@ type TParamValue = { export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({ ...clickable && { tabIndex: 0 }, }))<TParamValue>` + position: relative; display: inline-block; width: 15px; height: 15px; text-align: center; - position: relative; font-weight: ${({ clickable }) => (clickable ? 700 : 400)}; color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)}; diff --git a/src/features/MatchSidePlaylists/index.tsx b/src/features/MatchSidePlaylists/index.tsx index bb67f45e..22ab2bf7 100644 --- a/src/features/MatchSidePlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/index.tsx @@ -3,14 +3,26 @@ import { useRef, useState, } from 'react' +import { createPortal } from 'react-dom' + +import { useTour } from '@reactour/tour' import type { PlaylistOption } from 'features/MatchPage/types' import { useMatchPageStore } from 'features/MatchPage/store' +import { + Spotlight, + Steps, + TOUR_COMPLETED_STORAGE_KEY, +} from 'features/MatchTour' + +import { Overlay } from 'components/Overlay' -import { useEventListener } from 'hooks' +import { useEventListener, useModalRoot } from 'hooks' import { isIOS } from 'config/userAgent' +import { getLocalStorageItem } from 'helpers/getLocalStorage' + import { Tabs } from './config' import { TabEvents } from './components/TabEvents' import { TabWatch } from './components/TabWatch' @@ -25,6 +37,7 @@ import { TabIcon, TabTitle, Container, + TabButton, } from './styled' const tabPanes = { @@ -61,6 +74,14 @@ export const MatchSidePlaylists = ({ onTabClick, } = useMatchSidePlaylists() + const { + currentStep, + isOpen, + setIsOpen, + } = useTour() + + const modalRoot = useModalRoot() + const TabPane = tabPanes[selectedTab] const containerRef = useRef<HTMLDivElement | null>(null) @@ -82,6 +103,19 @@ export const MatchSidePlaylists = ({ tabPaneContainerRef.current?.clientHeight, ]) + useEffect(() => { + if ( + getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) === 'true' + || isOpen + || !isStatsTabVisible + || Number(profile?.c_match_calc_status) < 2 + ) return undefined + + const timer = setTimeout(() => setIsOpen(true), 1500) + + return () => clearTimeout(timer) + }, [isStatsTabVisible, setIsOpen, profile?.c_match_calc_status, isOpen]) + useEventListener({ callback: () => { const screenLandscape = isIOS ? window.orientation : window.screen.orientation.type @@ -98,7 +132,12 @@ export const MatchSidePlaylists = ({ }) return ( - <Wrapper ref={containerRef}> + <Wrapper + ref={containerRef} + data-step={Steps.Welcome} + highlighted={Boolean(isOpen) && currentStep === Steps.Welcome} + isTourOpen={Boolean(isOpen)} + > <TabsWrapper> <TabsGroup hasLessThanFourTabs={hasLessThanFourTabs}> {isWatchTabVisible ? ( @@ -106,8 +145,10 @@ export const MatchSidePlaylists = ({ aria-pressed={selectedTab === Tabs.WATCH} onClick={() => onTabClick(Tabs.WATCH)} > - <TabIcon icon='watch' /> - <TabTitle t='watch' /> + <TabButton> + <TabIcon icon='watch' /> + <TabTitle t='watch' /> + </TabButton> </Tab> ) : null} {isEventTabVisible ? ( @@ -115,8 +156,10 @@ export const MatchSidePlaylists = ({ aria-pressed={selectedTab === Tabs.EVENTS} onClick={() => onTabClick(Tabs.EVENTS)} > - <TabIcon icon='plays' /> - <TabTitle t='actions' /> + <TabButton> + <TabIcon icon='plays' /> + <TabTitle t='actions' /> + </TabButton> </Tab> ) : null} {isPlayersTabVisible ? ( @@ -124,17 +167,25 @@ export const MatchSidePlaylists = ({ aria-pressed={selectedTab === Tabs.PLAYERS} onClick={() => onTabClick(Tabs.PLAYERS)} > - <TabIcon icon='players' /> - <TabTitle t='players' /> + <TabButton> + <TabIcon icon='players' /> + <TabTitle t='players' /> + </TabButton> </Tab> ) : null} {isStatsTabVisible ? ( <Tab aria-pressed={selectedTab === Tabs.STATS} onClick={() => onTabClick(Tabs.STATS)} + data-step={Steps.Start} > - <TabIcon icon='stats' /> - <TabTitle t='stats' /> + {Boolean(currentStep === Steps.Start && isOpen) && ( + <Spotlight /> + )} + <TabButton> + <TabIcon icon='stats' /> + <TabTitle t='stats' /> + </TabButton> </Tab> ) : null} </TabsGroup> @@ -144,6 +195,7 @@ export const MatchSidePlaylists = ({ hasScroll={hasTabPaneScroll} ref={tabPaneContainerRef} forWatchTab={selectedTab === Tabs.WATCH} + highlighted={Boolean(isOpen)} > <TabPane tournamentData={tournamentData} @@ -153,6 +205,10 @@ export const MatchSidePlaylists = ({ selectedPlaylist={selectedPlaylist} /> </Container> + {modalRoot.current && isOpen && createPortal( + <Overlay />, + modalRoot.current, + )} </Wrapper> ) } diff --git a/src/features/MatchSidePlaylists/styled.tsx b/src/features/MatchSidePlaylists/styled.tsx index 6410245e..9c582b10 100644 --- a/src/features/MatchSidePlaylists/styled.tsx +++ b/src/features/MatchSidePlaylists/styled.tsx @@ -9,8 +9,22 @@ import { import { customScrollbar } from 'features/Common' import { T9n } from 'features/T9n' -export const Wrapper = styled.div` +type WrapperProps = { + highlighted?: boolean, + isTourOpen?: boolean, +} + +export const Wrapper = styled.div<WrapperProps>` padding-right: 14px; + padding-top: 10px; + + ${({ highlighted }) => (highlighted + ? css` + border: 1px solid #0057FF; + border-radius: 5px; + box-shadow: 0px 0px 66px 16px rgba(0, 87, 255, 0.8); + ` + : '')} ${isMobileDevice ? css` @@ -21,6 +35,13 @@ export const Wrapper = styled.div` ${customScrollbar} ` : ''}; + + ${({ isTourOpen }) => (isTourOpen + ? css` + overflow-y: initial; + z-index: 9999; + ` + : '')} ` export const TabsWrapper = styled.div`` @@ -36,8 +57,6 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>` ${({ hasLessThanFourTabs }) => (hasLessThanFourTabs ? css` - padding-top: 10px; - ${Tab} { justify-content: center; flex-direction: row; @@ -58,7 +77,7 @@ export const TabTitle = styled(T9n)` color: ${({ theme }) => theme.colors.white}; ` -export const Tab = styled.button.attrs({ role: 'tab' })` +export const TabButton = styled.button` display: flex; flex-direction: column; justify-content: space-between; @@ -69,12 +88,18 @@ export const Tab = styled.button.attrs({ role: 'tab' })` cursor: pointer; border: none; background: none; +` + +export const Tab = styled.div.attrs({ role: 'tab' })` + position: relative; &[aria-pressed="true"], :hover { - opacity: 1; + ${TabButton} { + opacity: 1; - ${TabTitle} { - font-weight: 600; + ${TabTitle} { + font-weight: 600; + } } } @@ -107,6 +132,7 @@ export const TabIcon = styled.div<TabIconProps>` type TContainer = { forWatchTab?: boolean, hasScroll: boolean, + highlighted?: boolean, } export const Container = styled.div<TContainer>` @@ -140,6 +166,12 @@ export const Container = styled.div<TContainer>` } ` : ''}; + + ${({ highlighted }) => (highlighted + ? css` + overflow-y: initial; + ` + : '')} ` type ButtonProps = { diff --git a/src/features/MatchTour/TourProvider.tsx b/src/features/MatchTour/TourProvider.tsx new file mode 100644 index 00000000..1651a719 --- /dev/null +++ b/src/features/MatchTour/TourProvider.tsx @@ -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> + ) +} diff --git a/src/features/MatchTour/components/ContentComponent/hooks.tsx b/src/features/MatchTour/components/ContentComponent/hooks.tsx new file mode 100644 index 00000000..66c15559 --- /dev/null +++ b/src/features/MatchTour/components/ContentComponent/hooks.tsx @@ -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, + } +} diff --git a/src/features/MatchTour/components/ContentComponent/index.tsx b/src/features/MatchTour/components/ContentComponent/index.tsx new file mode 100644 index 00000000..f9e03494 --- /dev/null +++ b/src/features/MatchTour/components/ContentComponent/index.tsx @@ -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> + ) +} + diff --git a/src/features/MatchTour/components/ContentComponent/styled.tsx b/src/features/MatchTour/components/ContentComponent/styled.tsx new file mode 100644 index 00000000..85da4902 --- /dev/null +++ b/src/features/MatchTour/components/ContentComponent/styled.tsx @@ -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; +` diff --git a/src/features/MatchTour/components/Spotlight/index.tsx b/src/features/MatchTour/components/Spotlight/index.tsx new file mode 100644 index 00000000..eade3547 --- /dev/null +++ b/src/features/MatchTour/components/Spotlight/index.tsx @@ -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) + diff --git a/src/features/MatchTour/components/index.tsx b/src/features/MatchTour/components/index.tsx new file mode 100644 index 00000000..1ca579ee --- /dev/null +++ b/src/features/MatchTour/components/index.tsx @@ -0,0 +1,2 @@ +export * from './ContentComponent' +export * from './Spotlight' diff --git a/src/features/MatchTour/config.tsx b/src/features/MatchTour/config.tsx new file mode 100644 index 00000000..02435c26 --- /dev/null +++ b/src/features/MatchTour/config.tsx @@ -0,0 +1,13 @@ +export enum Steps { + Start, + Welcome, + TeamsTab, + ClickToWatchPlaylist, + PlayersTab, + ShowMoreStats, + ShowLessStats, + FinalStats, + CurrentStats, +} + +export const TOUR_COMPLETED_STORAGE_KEY = 'tour_completed' diff --git a/src/features/MatchTour/index.tsx b/src/features/MatchTour/index.tsx new file mode 100644 index 00000000..4ae12cb7 --- /dev/null +++ b/src/features/MatchTour/index.tsx @@ -0,0 +1,3 @@ +export * from './config' +export * from './TourProvider' +export * from './components' diff --git a/src/features/MultiSourcePlayer/hooks/index.tsx b/src/features/MultiSourcePlayer/hooks/index.tsx index 8421f4ac..08ac5a6d 100644 --- a/src/features/MultiSourcePlayer/hooks/index.tsx +++ b/src/features/MultiSourcePlayer/hooks/index.tsx @@ -5,8 +5,12 @@ import { useRef, } from 'react' +import { useTour } from '@reactour/tour' + import size from 'lodash/size' +import { KEYBOARD_KEYS } from 'config' + import { useControlsVisibility } from 'features/StreamPlayer/hooks/useControlsVisibility' import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen' import { useVolume } from 'features/VideoPlayer/hooks/useVolume' @@ -83,6 +87,8 @@ export const useMultiSourcePlayer = ({ setPlayingProgress, } = useMatchPageStore() + const { isOpen } = useTour() + const { profileId, sportType } = usePageParams() /** время для сохранения статистики просмотра матча */ @@ -95,7 +101,7 @@ export const useMultiSourcePlayer = ({ activePlayer, loadedProgress, playedProgress, - playing, + playing: statePlaying, ready, seek, seeking, @@ -121,6 +127,8 @@ export const useMultiSourcePlayer = ({ const duration = useDuration(chapters) + const playing = Boolean(statePlaying && !isOpen) + const handleError = useCallback(() => { onError?.() }, [onError]) @@ -310,8 +318,9 @@ export const useMultiSourcePlayer = ({ useEventListener({ callback: (e: KeyboardEvent) => { - if (e.code === 'ArrowLeft') rewindBackward() - else if (e.code === 'ArrowRight') rewindForward() + if (isOpen) return + if (e.code === KEYBOARD_KEYS.ArrowLeft) rewindBackward() + else if (e.code === KEYBOARD_KEYS.ArrowRight) rewindForward() }, event: 'keydown', }) diff --git a/src/features/PageLayout/styled.tsx b/src/features/PageLayout/styled.tsx index 832a2d9e..3c1b32ed 100644 --- a/src/features/PageLayout/styled.tsx +++ b/src/features/PageLayout/styled.tsx @@ -3,9 +3,36 @@ import styled, { css } from 'styled-components/macro' import { isMobileDevice } from 'config/userAgent' import { customScrollbar } from 'features/Common' -export const PageWrapper = styled.div<{isIOS?: boolean}>` +type PageWrapperProps = { + isIOS?: boolean, + isTourOpen?: boolean, +} + +export const PageWrapper = styled.div<PageWrapperProps>` width: 100%; touch-action: ${({ isIOS }) => (isIOS ? 'none' : 'unset')}; + + ${({ isTourOpen }) => (isTourOpen + ? css` + pointer-events: none; + overflow: hidden; + ` + : '')} + + ~ .reactour__popover { + ${isMobileDevice + ? css` + @media screen and (orientation: landscape) { + /* добавлен important чтобы переопределить стили либы */ + left: 50% !important; + top: auto !important; + bottom: 0; + width: 70vw !important; + transform: translate(-50%, 0) !important; + }` + : ''}; + + } ` export const Main = styled.main` diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index ab96e28b..0aebd25a 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -7,6 +7,8 @@ import { useState, } from 'react' +import { useTour } from '@reactour/tour' + import size from 'lodash/size' import isNumber from 'lodash/isNumber' import isEmpty from 'lodash/isEmpty' @@ -14,7 +16,7 @@ import isUndefined from 'lodash/isUndefined' import Hls from 'hls.js' -import { isIOS } from 'config/userAgent' +import { isIOS, KEYBOARD_KEYS } from 'config' import { useObjectState, @@ -86,7 +88,7 @@ export const useVideoPlayer = ({ duration: fullMatchDuration, loadedProgress, playedProgress, - playing, + playing: statePlaying, ready, seek, seeking, @@ -104,6 +106,10 @@ export const useVideoPlayer = ({ setPlayingProgress, } = useMatchPageStore() + const { isOpen } = useTour() + + const playing = Boolean(statePlaying && !isOpen) + /** время для сохранения статистики просмотра матча */ const timeForStatistics = useRef(0) @@ -319,8 +325,9 @@ export const useVideoPlayer = ({ useEventListener({ callback: (e: KeyboardEvent) => { - if (e.code === 'ArrowLeft') rewindBackward() - else if (e.code === 'ArrowRight') rewindForward() + if (isOpen) return + if (e.code === KEYBOARD_KEYS.ArrowLeft) rewindBackward() + else if (e.code === KEYBOARD_KEYS.ArrowRight) rewindForward() }, event: 'keydown', }) diff --git a/src/helpers/bodyScroll/index.tsx b/src/helpers/bodyScroll/index.tsx new file mode 100644 index 00000000..e8b56014 --- /dev/null +++ b/src/helpers/bodyScroll/index.tsx @@ -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 + } + } +} diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index ce271b5d..2bfc875e 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -14,3 +14,4 @@ export * from './getTeamAbbr' export * from './cookie' export * from './isMatchPage' export * from './languageUrlParam' +export * from './bodyScroll' diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 6e2f4ec7..ab04bf68 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -7,3 +7,4 @@ export * from './useObjectState' export * from './usePageParams' export * from './useTooltip' export * from './useModalRoot' +export * from './usePageLogger' diff --git a/src/hooks/useEventListener.tsx b/src/hooks/useEventListener.tsx index 3d0e4680..e35a859c 100644 --- a/src/hooks/useEventListener.tsx +++ b/src/hooks/useEventListener.tsx @@ -9,6 +9,7 @@ type Target = RefObject<HTMLElement> | HTMLElement | Window type Args<E extends keyof EventMap> = { callback: (e: EventMap[E]) => void, event: E, + options?: Parameters<(HTMLElement | Window)['addEventListener']>[2], target?: Target, } @@ -19,6 +20,7 @@ type Args<E extends keyof EventMap> = { export const useEventListener = <E extends keyof EventMap>({ callback, event, + options, target = window, }: Args<E>) => { const callbackRef = useRef(callback) @@ -39,9 +41,17 @@ export const useEventListener = <E extends keyof EventMap>({ callbackRef.current(e) } - windowOrElement?.addEventListener(event, listener) + windowOrElement?.addEventListener( + event, + listener, + options, + ) return () => { - windowOrElement?.removeEventListener(event, listener) + windowOrElement?.removeEventListener( + event, + listener, + options, + ) } - }, [event, target]) + }, [event, target, options]) } diff --git a/src/requests/getMatchParticipants.tsx b/src/requests/getMatchParticipants.tsx index edd0e7d1..1be5bd60 100644 --- a/src/requests/getMatchParticipants.tsx +++ b/src/requests/getMatchParticipants.tsx @@ -20,7 +20,7 @@ export type Player = { lastname_national: string | null, lastname_rus: string, national_f_team: number | null, - national_shirt_num: number, + national_shirt_num: number | null, nickname_eng: string | null, nickname_rus: string | null, num: number | null, diff --git a/src/requests/getPlayersStats.tsx b/src/requests/getPlayersStats.tsx index af898d49..3980c694 100644 --- a/src/requests/getPlayersStats.tsx +++ b/src/requests/getPlayersStats.tsx @@ -12,7 +12,7 @@ export type PlayerParam = { lexica_short: number | null, markers: Array<number> | null, name_en: string, - name_ru: string, + name_ru: string | null, val: number | null, } diff --git a/src/requests/getTeamsStats.tsx b/src/requests/getTeamsStats.tsx index ecf7db2f..ec8fd690 100644 --- a/src/requests/getTeamsStats.tsx +++ b/src/requests/getTeamsStats.tsx @@ -9,9 +9,9 @@ export type Param = { data_type: string, id: number, lexic: number, - markers: Array<number>, + markers: Array<number> | null, name_en: string, - name_ru: string, + name_ru: string | null, val: number | null, }