From 8a46f0a3636d2134f5810b534b3bbaf16ecf0db6 Mon Sep 17 00:00:00 2001 From: Andrei Dekterev Date: Wed, 29 Mar 2023 17:46:17 +0700 Subject: [PATCH 01/10] fix(#319): check logo for team icon --- src/features/ProfileCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/ProfileCard/index.tsx b/src/features/ProfileCard/index.tsx index a59f7ed1..a887c393 100644 --- a/src/features/ProfileCard/index.tsx +++ b/src/features/ProfileCard/index.tsx @@ -79,7 +79,7 @@ export const ProfileCard = ({ profile }: ProfileType) => {
{name} - + {profile.additionalInfo?.id && } {tournamentId ? ( Date: Wed, 29 Mar 2023 18:38:01 +0700 Subject: [PATCH 02/10] fix(#299): change color for live button progress --- src/features/ProfileCard/index.tsx | 2 +- .../components/Controls/Components/ControlsWeb/index.tsx | 6 +++++- src/features/StreamPlayer/components/Controls/index.tsx | 1 + src/features/StreamPlayer/hooks/index.tsx | 8 ++++++-- src/features/StreamPlayer/index.tsx | 2 ++ src/features/StreamPlayer/styled.tsx | 8 ++++++-- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/features/ProfileCard/index.tsx b/src/features/ProfileCard/index.tsx index a887c393..1354ec00 100644 --- a/src/features/ProfileCard/index.tsx +++ b/src/features/ProfileCard/index.tsx @@ -79,7 +79,7 @@ export const ProfileCard = ({ profile }: ProfileType) => {
{name} - {profile.additionalInfo?.id && } + {profile.additionalInfo?.id && } {tournamentId ? ( isFullscreen, isLastChapterPlaying, isLive, + isLiveTime, isStorage, muted, numberOfChapters = 0, @@ -108,7 +109,10 @@ export const ControlsWeb = (controlsProps: { props: ControlsPropsExtended }) => selectedAudioTrack={selectedAudioTrack!} /> {isLive && ( - + )} diff --git a/src/features/StreamPlayer/components/Controls/index.tsx b/src/features/StreamPlayer/components/Controls/index.tsx index e243e702..b2801a02 100644 --- a/src/features/StreamPlayer/components/Controls/index.tsx +++ b/src/features/StreamPlayer/components/Controls/index.tsx @@ -29,6 +29,7 @@ export type ControlsProps = { isFullscreen: boolean, isLastChapterPlaying?: boolean, isLive?: boolean, + isLiveTime?: boolean, isStorage?: boolean, liveChapters?: LiveChapters, loadedProgress: number, diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index 0c438e15..5ce8444d 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -44,6 +44,7 @@ import { FULL_GAME_KEY } from '../../MatchPage/helpers/buildPlaylists' export type PlayerState = typeof initialState const toMilliSeconds = (seconds: number) => seconds * 1000 +const BUFFERING_TIME = 30 * 1000 const initialState = { activeChapterIndex: 0, @@ -61,6 +62,7 @@ const initialState = { export type Props = { chapters: Chapters, isLive: boolean, + isLiveTime?: boolean, onDurationChange?: (duration: number) => void, onPlayingChange: (playing: boolean) => void, onProgressChange: (seconds: number) => void, @@ -245,7 +247,7 @@ export const useVideoPlayer = ({ setIsLiveTime(true) } - const liveProgressMs = Math.max(fullMatchDuration - 30000, 0) + const liveProgressMs = Math.max(fullMatchDuration - BUFFERING_TIME, 0) setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 }) if (liveProgressMs > 0) setIsLiveTime(false) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -265,7 +267,7 @@ export const useVideoPlayer = ({ setIsPausedTime(true) } - const liveProgressMs = Math.max(duration - 30000, 0) + const liveProgressMs = Math.max(duration - BUFFERING_TIME, 0) setPlayerState({ playedProgress: pausedProgress, seek: pausedProgress / 1000 }) if (liveProgressMs > 0) setIsPausedTime(false) // eslint-disable-next-line @@ -356,6 +358,7 @@ export const useVideoPlayer = ({ ]) useEffect(() => { + setIsLiveTime(playedProgress > duration - BUFFERING_TIME) if ((chapters[0]?.isFullMatchChapter) || isEmpty(chapters)) return const { duration: chapterDuration } = getActiveChapter() @@ -491,6 +494,7 @@ export const useVideoPlayer = ({ isFullscreen, isLastChapterPlaying, isLive, + isLiveTime, loadedProgress, numberOfChapters, onDuration, diff --git a/src/features/StreamPlayer/index.tsx b/src/features/StreamPlayer/index.tsx index a75f3907..4df9e26f 100644 --- a/src/features/StreamPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -55,6 +55,7 @@ export const StreamPlayer = (props: Props) => { hideCenterControls, isFullscreen, isLive, + isLiveTime, loadedProgress, mainControlsVisible, muted, @@ -180,6 +181,7 @@ export const StreamPlayer = (props: Props) => { duration={duration} isFullscreen={isFullscreen} isLive={profile?.live} + isLiveTime={isLiveTime} isStorage={profile?.storage} loadedProgress={loadedProgress} muted={muted} diff --git a/src/features/StreamPlayer/styled.tsx b/src/features/StreamPlayer/styled.tsx index c096488b..6d190334 100644 --- a/src/features/StreamPlayer/styled.tsx +++ b/src/features/StreamPlayer/styled.tsx @@ -323,7 +323,7 @@ export const CenterControls = styled.div` `} ` -export const LiveBtn = styled(ButtonBase)` +export const LiveBtn = styled(ButtonBase)<{isLiveTime?: boolean}>` height: auto; font-weight: bold; font-size: 10px; @@ -331,7 +331,11 @@ export const LiveBtn = styled(ButtonBase)` letter-spacing: 0.05em; text-transform: uppercase; padding: 4.5px 8px; - background-color: #CC0000; + background-color: ${({ isLiveTime }) => ( + isLiveTime + ? '#CC0000' + : 'rgba(130, 130, 130, 0.6)' + )}; border-radius: 1.3px; margin-right: 25px; -- 2.30.2 From 9376098b61d850052412b2285e071661c2a465b2 Mon Sep 17 00:00:00 2001 From: Andrei Dekterev Date: Thu, 30 Mar 2023 00:33:36 +0700 Subject: [PATCH 03/10] fix(#299): fix isLiveTime for check Live --- src/features/StreamPlayer/helpers/index.tsx | 1 + src/features/StreamPlayer/hooks/index.tsx | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/features/StreamPlayer/helpers/index.tsx b/src/features/StreamPlayer/helpers/index.tsx index 854ce2c3..f97e78e4 100644 --- a/src/features/StreamPlayer/helpers/index.tsx +++ b/src/features/StreamPlayer/helpers/index.tsx @@ -11,3 +11,4 @@ export const findChapterByProgress = (chapters: Chapters, progressMs: number) => )) ) } + diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index 5ce8444d..3865ee1a 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -62,7 +62,6 @@ const initialState = { export type Props = { chapters: Chapters, isLive: boolean, - isLiveTime?: boolean, onDurationChange?: (duration: number) => void, onPlayingChange: (playing: boolean) => void, onProgressChange: (seconds: number) => void, @@ -113,7 +112,7 @@ export const useVideoPlayer = ({ resumeFrom, src: url, }) - const [isLiveTime, setIsLiveTime] = useState(false) + const [isLivePlaying, setIsLivePlaying] = useState(false) const [isPausedTime, setIsPausedTime] = useState(false) const [pausedProgress, setPausedProgress] = useState(0) @@ -212,6 +211,10 @@ export const useVideoPlayer = ({ setPlayerState({ playing: true }) } + const checkLive = () => chapters[0]?.isFullMatchChapter + && isLive + && playedProgress > duration - BUFFERING_TIME * 1.5 + const onDuration = (durationSeconds: number) => { setPlayerState({ duration: toMilliSeconds(durationSeconds) }) onDurationChange?.(durationSeconds) @@ -244,12 +247,12 @@ export const useVideoPlayer = ({ if (selectedPlaylist?.id !== FULL_GAME_KEY) { restartVideo() - setIsLiveTime(true) + setIsLivePlaying(true) } const liveProgressMs = Math.max(fullMatchDuration - BUFFERING_TIME, 0) setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 }) - if (liveProgressMs > 0) setIsLiveTime(false) + if (liveProgressMs > 0) setIsLivePlaying(false) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ duration, @@ -287,11 +290,11 @@ export const useVideoPlayer = ({ }, [selectedPlaylist]) useEffect(() => { - if (duration && isLiveTime && chapters[0]?.isFullMatchChapter) { + if (duration && isLivePlaying && chapters[0]?.isFullMatchChapter) { backToLive() } // eslint-disable-next-line - }, [duration, isLiveTime]) + }, [duration, isLivePlaying]) useEffect(() => { if (duration @@ -358,7 +361,6 @@ export const useVideoPlayer = ({ ]) useEffect(() => { - setIsLiveTime(playedProgress > duration - BUFFERING_TIME) if ((chapters[0]?.isFullMatchChapter) || isEmpty(chapters)) return const { duration: chapterDuration } = getActiveChapter() @@ -494,7 +496,7 @@ export const useVideoPlayer = ({ isFullscreen, isLastChapterPlaying, isLive, - isLiveTime, + isLiveTime: checkLive(), loadedProgress, numberOfChapters, onDuration, -- 2.30.2 From b59c5ce832d92539699a97be30c4d94bd9697cdc Mon Sep 17 00:00:00 2001 From: Rakov Date: Fri, 31 Mar 2023 16:05:21 +0300 Subject: [PATCH 04/10] fix(#462): match watch second to LS --- src/config/procedures.tsx | 2 - .../components/LiveMatch/hooks/index.tsx | 24 ++++++----- .../LiveMatch/hooks/useLastPlayPosition.tsx | 37 ---------------- .../hooks/usePlayerProgressReporter.tsx | 32 ++++++++++---- .../LiveMatch/hooks/useUrlParam.tsx | 23 ---------- .../store/hooks/useTournamentData.tsx | 5 ++- .../components/LiveMatchPlaylist/index.tsx | 22 ++++++++-- .../components/LiveMatchPopup/styled.tsx | 1 - src/features/StreamPlayer/hooks/index.tsx | 43 +++++++++++++++---- src/requests/getMatchLastWatchSeconds.tsx | 43 ------------------- src/requests/index.tsx | 2 - src/requests/reportPlayerProgress.tsx | 38 ---------------- 12 files changed, 94 insertions(+), 178 deletions(-) delete mode 100644 src/features/MatchPage/components/LiveMatch/hooks/useLastPlayPosition.tsx delete mode 100644 src/features/MatchPage/components/LiveMatch/hooks/useUrlParam.tsx delete mode 100644 src/requests/getMatchLastWatchSeconds.tsx delete mode 100644 src/requests/reportPlayerProgress.tsx diff --git a/src/config/procedures.tsx b/src/config/procedures.tsx index 834ef537..de9aa619 100644 --- a/src/config/procedures.tsx +++ b/src/config/procedures.tsx @@ -21,7 +21,6 @@ export const PROCEDURES = { get_user_agreemens: 'get_user_agreemens', get_user_favorites: 'get_user_favorites', get_user_info: 'get_user_info', - get_user_match_second: 'get_user_match_second', get_user_payments: 'get_user_payments', get_user_preferences: 'get_user_preferences', get_user_subscribes: 'get_user_subscribes', @@ -37,7 +36,6 @@ export const PROCEDURES = { save_user_custom_subscription: 'save_user_custom_subscription', save_user_favorite: 'save_user_favorite', save_user_info: 'save_user_info', - save_user_match_second: 'save_user_match_second', save_user_page: 'save_user_page', save_user_preferences: 'save_user_preferences', save_user_subscription: 'save_user_subscription', diff --git a/src/features/MatchPage/components/LiveMatch/hooks/index.tsx b/src/features/MatchPage/components/LiveMatch/hooks/index.tsx index e41669e7..1f3a5919 100644 --- a/src/features/MatchPage/components/LiveMatch/hooks/index.tsx +++ b/src/features/MatchPage/components/LiveMatch/hooks/index.tsx @@ -4,11 +4,11 @@ import { API_ROOT } from 'config' import { readToken } from 'helpers/token' -import { usePageParams } from 'hooks/usePageParams' +import { usePageParams } from 'hooks' import { useMatchPageStore } from 'features/MatchPage/store' -import { usePlayerProgressReporter } from './usePlayerProgressReporter' +import { MatchSecondType, usePlayerProgressReporter } from './usePlayerProgressReporter' import { useResumeUrlParam } from './useResumeUrlParam' import { useChapters } from './useChapters' import { usePlaylistLogger } from './usePlaylistLogger' @@ -21,15 +21,17 @@ export const useLiveMatch = () => { setFullMatchPlaylistDuration, } = useMatchPageStore() const { profileId: matchId, sportType } = usePageParams() - const resume = useResumeUrlParam() + const resumeFromParam = useResumeUrlParam() - const fromStartIfStreamPaused = useMemo( - () => (profile && !profile.live ? 0 : undefined), - // deps намеренно оставляем пустым, - // не нужно реагировать на изменение live когда пользователь смотрит матч - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ) + const resumeFromLocalStorage = useMemo(() => { + const lastSecondLS = localStorage.getItem('matchLastWatchSecond') + const matchesLastWatchSecond: MatchSecondType | null = lastSecondLS && JSON.parse(lastSecondLS) + // undefined означает, что юзер будет смотреть лайв + return profile && !profile.live + ? matchesLastWatchSecond?.[sportType]?.[matchId]?.lastWatchSecond || 0 + : undefined + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [profile]) const { chapters } = useChapters({ profile, @@ -69,7 +71,7 @@ export const useLiveMatch = () => { onPlayerProgressChange, onPlayingChange, onPlaylistSelect, - resume: resume ?? fromStartIfStreamPaused, + resume: resumeFromParam ?? resumeFromLocalStorage, streamUrl: `${API_ROOT}/video/stream/${sportType}/${matchId}.m3u8`, } } diff --git a/src/features/MatchPage/components/LiveMatch/hooks/useLastPlayPosition.tsx b/src/features/MatchPage/components/LiveMatch/hooks/useLastPlayPosition.tsx deleted file mode 100644 index 885f0470..00000000 --- a/src/features/MatchPage/components/LiveMatch/hooks/useLastPlayPosition.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from 'react' - -import type { LastPlayPosition } from 'requests' -import { getMatchLastWatchSeconds } from 'requests' - -import { usePageParams } from 'hooks/usePageParams' -import { useRequest } from 'hooks/useRequest' - -const initialPosition = { - half: 0, - second: 0, -} - -export const useLastPlayPosition = () => { - const { profileId: matchId, sportType } = usePageParams() - const [ - lastPlayPosition, - setPosition, - ] = useState(initialPosition) - const { - isFetching: isLastPlayPositionFetching, - request: requestLastPlayPosition, - } = useRequest(getMatchLastWatchSeconds) - - useEffect(() => { - requestLastPlayPosition(sportType, matchId).then(setPosition) - }, [ - sportType, - matchId, - requestLastPlayPosition, - ]) - - return { - isLastPlayPositionFetching, - lastPlayPosition, - } -} diff --git a/src/features/MatchPage/components/LiveMatch/hooks/usePlayerProgressReporter.tsx b/src/features/MatchPage/components/LiveMatch/hooks/usePlayerProgressReporter.tsx index b393f8f3..52272ad9 100644 --- a/src/features/MatchPage/components/LiveMatch/hooks/usePlayerProgressReporter.tsx +++ b/src/features/MatchPage/components/LiveMatch/hooks/usePlayerProgressReporter.tsx @@ -1,25 +1,41 @@ import { useCallback, useRef } from 'react' -import { reportPlayerProgress } from 'requests' - import { usePageParams } from 'hooks/usePageParams' import { useInterval } from 'hooks/useInterval' const reportRequestInterval = 30000 +export type MatchSecondType = { + [sportType: number]: { + [matchId: number]: { + lastWatchSecond: number, + period: number, + }, + }, +} + export const usePlayerProgressReporter = () => { const { profileId: matchId, sportType } = usePageParams() const playerData = useRef({ period: 0, seconds: 0 }) const intervalCallback = () => { const { period, seconds } = playerData.current - reportPlayerProgress({ - half: period, - matchId, - seconds, - sport: sportType, - }) + + const matchSecond = localStorage.getItem('matchLastWatchSecond') + const matchSecondParsed: MatchSecondType = matchSecond ? JSON.parse(matchSecond) : {} + + localStorage.setItem('matchLastWatchSecond', JSON.stringify({ + ...matchSecondParsed, + [sportType]: { + ...matchSecondParsed?.[sportType], + [matchId]: { + lastWatchSecond: seconds, + period, + }, + }, + })) } + const { start, stop } = useInterval({ callback: intervalCallback, intervalDuration: reportRequestInterval, diff --git a/src/features/MatchPage/components/LiveMatch/hooks/useUrlParam.tsx b/src/features/MatchPage/components/LiveMatch/hooks/useUrlParam.tsx deleted file mode 100644 index 158b1fba..00000000 --- a/src/features/MatchPage/components/LiveMatch/hooks/useUrlParam.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useMemo } from 'react' -import { useLocation } from 'react-router' - -import isNumber from 'lodash/isNumber' - -export const RESUME_KEY = 'resume' - -const readResumeParam = (search: string) => { - const params = new URLSearchParams(search) - const rawValue = params.get(RESUME_KEY) - if (!rawValue) return undefined - - const value = JSON.parse(rawValue) - return isNumber(value) ? value : 0 -} - -export const useUrlParam = () => { - const { search } = useLocation() - - const resume = useMemo(() => readResumeParam(search), [search]) - - return resume -} diff --git a/src/features/MatchPage/store/hooks/useTournamentData.tsx b/src/features/MatchPage/store/hooks/useTournamentData.tsx index 5dca4d7c..e2450632 100644 --- a/src/features/MatchPage/store/hooks/useTournamentData.tsx +++ b/src/features/MatchPage/store/hooks/useTournamentData.tsx @@ -11,6 +11,7 @@ import sortBy from 'lodash/sortBy' import type { Match } from 'features/Matches' import { prepareMatches } from 'features/Matches/helpers/prepareMatches' +import { useAuthStore } from 'features/AuthStore' import type { MatchInfo } from 'requests' import { getTournamentMatches } from 'requests' @@ -23,6 +24,7 @@ import { TournamentData } from '../../types' export const useTournamentData = (matchProfile: MatchInfo) => { const { sportType } = usePageParams() + const { user } = useAuthStore() const [tournamentMatches, setTournamentMatches] = useState>([]) const [matchDates, setMatchDates] = useState>([]) @@ -44,7 +46,7 @@ export const useTournamentData = (matchProfile: MatchInfo) => { )).sort((a, b) => a.getTime() - b.getTime()) setMatchDates(sortedUniq(matchDateList.map((date) => format(date, 'yyyy-MM-dd')))) - setTournamentMatches(sortBy(prepareMatches(matchesBySection.broadcast), ['date'])) + setTournamentMatches(sortBy(prepareMatches(matchesBySection.broadcast, user), ['date'])) })() } }, [ @@ -52,6 +54,7 @@ export const useTournamentData = (matchProfile: MatchInfo) => { sportType, matchProfile?.live, matchProfile?.c_match_calc_status, + user, ]) const tournamentData: TournamentData = useMemo(() => ({ diff --git a/src/features/MatchPopup/components/LiveMatchPlaylist/index.tsx b/src/features/MatchPopup/components/LiveMatchPlaylist/index.tsx index 264f1ed6..72c02503 100644 --- a/src/features/MatchPopup/components/LiveMatchPlaylist/index.tsx +++ b/src/features/MatchPopup/components/LiveMatchPlaylist/index.tsx @@ -3,22 +3,38 @@ import { useState, useEffect } from 'react' import { PAGES } from 'config' import { isMobileDevice } from 'config/userAgent' import { getSportLexic } from 'helpers' -import { getMatchLastWatchSeconds, LastPlayPosition } from 'requests' import { useMatchPopupStore } from 'features/MatchPopup/store' +import type { MatchSecondType } from 'features/MatchPage/components/LiveMatch/hooks/usePlayerProgressReporter' + +import { useLocalStore } from 'hooks' import { SimplePlaylistButton } from '../SimplePlaylistButton' import { List, Item } from './styled' +type LastPlayPosition = { + half: number, + second: number, +} + export const LiveMatchPlaylist = () => { const [lastPlayPosition, setLastPlayPosition] = useState(null) const { match } = useMatchPopupStore() + const [lastWatchSecond] = useLocalStore({ + key: 'matchLastWatchSecond', + }) + useEffect(() => { if (match) { - getMatchLastWatchSeconds(match?.sportType, match?.id) - .then((lastPlayPositionSecond) => setLastPlayPosition(lastPlayPositionSecond)) + const matchTime = lastWatchSecond?.[match.sportType]?.[match.id] + + setLastPlayPosition({ + half: matchTime?.period || 0, + second: matchTime?.lastWatchSecond || 0, + }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [match]) if (!match) return null diff --git a/src/features/MatchPopup/components/LiveMatchPopup/styled.tsx b/src/features/MatchPopup/components/LiveMatchPopup/styled.tsx index 08055a9e..1072d709 100644 --- a/src/features/MatchPopup/components/LiveMatchPopup/styled.tsx +++ b/src/features/MatchPopup/components/LiveMatchPopup/styled.tsx @@ -11,7 +11,6 @@ export const Modal = styled(BaseModal)` ${ModalWindow} { width: 27.22rem; - min-height: 14.859rem; padding: 1.416rem 0.71rem; border-radius: 5px; diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index 3865ee1a..ab96e28b 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -1,5 +1,6 @@ -import type { MouseEvent } from 'react' import { + MouseEvent, + useMemo, useRef, useCallback, useEffect, @@ -9,6 +10,7 @@ import { import size from 'lodash/size' import isNumber from 'lodash/isNumber' import isEmpty from 'lodash/isEmpty' +import isUndefined from 'lodash/isUndefined' import Hls from 'hls.js' @@ -105,14 +107,23 @@ export const useVideoPlayer = ({ /** время для сохранения статистики просмотра матча */ const timeForStatistics = useRef(0) + const resumeTimeWithOffset = useMemo(() => { + const chapterWithOffset = chapters[0].startOffsetMs / 1000 + return !isUndefined(resumeFrom) && isLive && chapters[0].isFullMatchChapter + ? resumeFrom + chapterWithOffset + : resumeFrom + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resumeFrom]) + const { url } = chapters[0] ?? { url: '' } const numberOfChapters = size(chapters) const { hls, videoRef } = useHlsPlayer({ isLive, - resumeFrom, + resumeFrom: resumeTimeWithOffset, src: url, }) - const [isLivePlaying, setIsLivePlaying] = useState(false) + // временно закоментил, если ничего не сломается, удалю + // const [isLivePlaying, setIsLivePlaying] = useState(false) const [isPausedTime, setIsPausedTime] = useState(false) const [pausedProgress, setPausedProgress] = useState(0) @@ -247,12 +258,12 @@ export const useVideoPlayer = ({ if (selectedPlaylist?.id !== FULL_GAME_KEY) { restartVideo() - setIsLivePlaying(true) + // setIsLivePlaying(true) } const liveProgressMs = Math.max(fullMatchDuration - BUFFERING_TIME, 0) setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 }) - if (liveProgressMs > 0) setIsLivePlaying(false) + // if (liveProgressMs > 0) setIsLivePlaying(false) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ duration, @@ -290,11 +301,11 @@ export const useVideoPlayer = ({ }, [selectedPlaylist]) useEffect(() => { - if (duration && isLivePlaying && chapters[0]?.isFullMatchChapter) { + if (duration && isUndefined(resumeFrom) && chaptersProps[0]?.isFullMatchChapter) { backToLive() } // eslint-disable-next-line - }, [duration, isLivePlaying]) + }, [chaptersProps]) useEffect(() => { if (duration @@ -321,12 +332,16 @@ export const useVideoPlayer = ({ }, [seek, setPlayerState]) useEffect(() => { - onPlayingChange(playing) + onPlayingChange(selectedPlaylist?.id === FULL_GAME_KEY ? playing : false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playing, selectedPlaylist]) + + useEffect(() => { if (playing) { setPlayerState({ buffering: false }) } // eslint-disable-next-line - }, [playing, onPlayingChange]) + }, [playing]) const regURL = /\d{6,20}/gi @@ -346,6 +361,16 @@ export const useVideoPlayer = ({ && chapters[0]?.url.match(regURL)?.[0] === chaptersProps[0]?.url.match(regURL)?.[0]) || (isEmpty(chapters) || isEmpty(chaptersProps))) return + if (!isUndefined(resumeFrom) && chaptersProps[0].isFullMatchChapter) { + setPlayerState({ + ...initialState, + chapters: chaptersProps, + playing: true, + seek: resumeFrom + chaptersProps[0].startOffsetMs / 1000, + }) + return + } + setPlayerState({ ...initialState, chapters: chaptersProps, diff --git a/src/requests/getMatchLastWatchSeconds.tsx b/src/requests/getMatchLastWatchSeconds.tsx deleted file mode 100644 index 6f976130..00000000 --- a/src/requests/getMatchLastWatchSeconds.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - DATA_URL, - PROCEDURES, - -} from 'config' -import { callApi } from 'helpers' - -const proc = PROCEDURES.get_user_match_second - -type Response = { - _p_half: number | null, - _p_second: number | null, -} - -export type LastPlayPosition = { - half: number, - second: number, -} - -export const getMatchLastWatchSeconds = async ( - sportType: number, - matchId: number, -) => { - const config = { - body: { - params: { - _p_match_id: matchId, - _p_sport: sportType, - }, - proc, - }, - } - - const response: Response = await callApi({ - config, - url: DATA_URL, - }) - - return { - half: response?._p_half ?? 0, - second: response?._p_second ?? 0, - } -} diff --git a/src/requests/index.tsx b/src/requests/index.tsx index 494e9a3a..a2cff564 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -14,10 +14,8 @@ export * from './getUserInfo' export * from './getMatchInfo' export * from './getVideos' export * from './getUnauthenticatedMatch' -export * from './reportPlayerProgress' export * from './saveUserInfo' export * from './getPlayerInfo' -export * from './getMatchLastWatchSeconds' export * from './getMatchesPreviewImages' export * from './getSportActions' export * from './getMatchEvents' diff --git a/src/requests/reportPlayerProgress.tsx b/src/requests/reportPlayerProgress.tsx deleted file mode 100644 index 48155428..00000000 --- a/src/requests/reportPlayerProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - DATA_URL, - PROCEDURES, -} from 'config' -import { callApi } from 'helpers' - -const proc = PROCEDURES.save_user_match_second - -type Args = { - half?: number, - matchId: number, - seconds: number, - sport: number, -} - -export const reportPlayerProgress = ({ - half, - matchId, - seconds, - sport, -}: Args) => { - const config = { - body: { - params: { - _p_half: half, - _p_match_id: matchId, - _p_second: seconds, - _p_sport: sport, - }, - proc, - }, - } - - callApi({ - config, - url: DATA_URL, - }) -} -- 2.30.2 From 5eeaacde4c69ee3524331b34016bc0d8c632b8fd Mon Sep 17 00:00:00 2001 From: Andrei Dekterev Date: Thu, 30 Mar 2023 17:56:30 +0700 Subject: [PATCH 05/10] fix(#479): cancel selection time tooltip --- .../components/Controls/Components/ControlsMobile/index.tsx | 6 +++++- src/features/StreamPlayer/components/ProgressBar/styled.tsx | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/features/StreamPlayer/components/Controls/Components/ControlsMobile/index.tsx b/src/features/StreamPlayer/components/Controls/Components/ControlsMobile/index.tsx index 7bd1442c..2aa8f264 100644 --- a/src/features/StreamPlayer/components/Controls/Components/ControlsMobile/index.tsx +++ b/src/features/StreamPlayer/components/Controls/Components/ControlsMobile/index.tsx @@ -24,6 +24,7 @@ export const ControlsMobile = (controlsProps: {props: ControlsPropsExtended}) => controlsVisible, isFullscreen, isLive, + isLiveTime, onFullscreenClick, onQualitySelect, playBackTime, @@ -50,7 +51,10 @@ export const ControlsMobile = (controlsProps: {props: ControlsPropsExtended}) => selectedAudioTrack={selectedAudioTrack!} /> {isLive && ( - + )} diff --git a/src/features/StreamPlayer/components/ProgressBar/styled.tsx b/src/features/StreamPlayer/components/ProgressBar/styled.tsx index c3af6e4b..298e37f7 100644 --- a/src/features/StreamPlayer/components/ProgressBar/styled.tsx +++ b/src/features/StreamPlayer/components/ProgressBar/styled.tsx @@ -45,6 +45,8 @@ export const Scrubber = styled.div<{isIOS?: boolean}>` background-color: #CC0000; border-radius: 50%; cursor: pointer; + user-select: none; + -webkit-user-select: none; :hover ${Wrapper} { visibility: visible; -- 2.30.2 From e48b890fd71b3899a45adf7436ab76cc1c1d0b16 Mon Sep 17 00:00:00 2001 From: Andrei Dekterev Date: Fri, 31 Mar 2023 19:38:19 +0700 Subject: [PATCH 06/10] feat(errorboundary): add error boundaryComponent --- src/components/Error/index.tsx | 38 ++++++++++++++++++++ src/components/Error/styled.tsx | 17 +++++++++ src/components/ErrorBoundary/index.tsx | 50 ++++++++++++++++++++++++++ src/components/SimplePopup/index.tsx | 10 ++++-- src/config/lexics/indexLexics.tsx | 1 + src/features/App/index.tsx | 11 +++--- 6 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 src/components/Error/index.tsx create mode 100644 src/components/Error/styled.tsx create mode 100644 src/components/ErrorBoundary/index.tsx diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx new file mode 100644 index 00000000..a13e1e81 --- /dev/null +++ b/src/components/Error/index.tsx @@ -0,0 +1,38 @@ +import { useEffect } from 'react' + +import { FadeIn } from 'features/Animation' +import { T9n } from 'features/T9n' + +import { useToggle } from 'hooks' + +import { WrapperError } from './styled' + +type ErrorInfo = { + error?: string, +} +export const Error = ({ error }: ErrorInfo) => { + const { + close, + isOpen, + open, + } = useToggle() + + useEffect(() => { + open() + const timeOut = setTimeout(close, 5000) + return () => { + clearInterval(timeOut) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + isOpen ? ( + + + {error ?? } + + + ) : null + ) +} diff --git a/src/components/Error/styled.tsx b/src/components/Error/styled.tsx new file mode 100644 index 00000000..fde3a01b --- /dev/null +++ b/src/components/Error/styled.tsx @@ -0,0 +1,17 @@ +import styled from 'styled-components/macro' + +export const WrapperError = styled.div` + position: fixed; + top: 20px; + right: 20px; + width: 390px; + max-height: 500px; + border-radius: 10px; + background-color: #333333; + color: white; + padding: 20px; + font-size: 0.8rem; + text-align: center; + z-index: 1000000; + white-space: pre-wrap; +` diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx new file mode 100644 index 00000000..b3a27e45 --- /dev/null +++ b/src/components/ErrorBoundary/index.tsx @@ -0,0 +1,50 @@ +// eslint-disable react/destructuring-assignment + +import { Component } from 'react' + +import type{ ErrorInfo, ReactNode } from 'react' + +import { Error } from '../Error' + +interface Props { + children?: ReactNode, +} + +interface State { + hasError: boolean, +} + +class ErrorBoundary extends Component { + // eslint-disable-next-line react/state-in-constructor + public state: State = { + hasError: false, + } + + public static getDerivedStateFromError(_: Error): State { + // Update state so the next render will show the fallback UI. + return { hasError: true } + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // eslint-disable-next-line no-console + console.error( + 'Uncaught error:', + error, + errorInfo, + ) + } + + public render() { + const { hasError } = this.state + const { children } = this.props + + return ( + <> + {hasError && } + {children} + + ) + } +} + +export default ErrorBoundary diff --git a/src/components/SimplePopup/index.tsx b/src/components/SimplePopup/index.tsx index 1ad02429..e848daff 100644 --- a/src/components/SimplePopup/index.tsx +++ b/src/components/SimplePopup/index.tsx @@ -15,6 +15,7 @@ import { type Props = { buttonName?: string, + children?: ReactNode, headerName?: string, icon?: ReactNode, isModalOpen: boolean, @@ -26,6 +27,7 @@ type Props = { export const SimplePopup = (props: Props) => { const { buttonName, + children, headerName, icon, isModalOpen, @@ -47,9 +49,11 @@ export const SimplePopup = (props: Props) => { - - - + {children || ( + + + + )} {buttonName && ( diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index bb6aea9e..bf1e822a 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -163,6 +163,7 @@ export const indexLexics = { loading: 3527, logout: 4306, lost_connection: 15699, + main_error: 20101, match_status_finished: 12985, match_status_live: 12984, match_status_soon: 12986, diff --git a/src/features/App/index.tsx b/src/features/App/index.tsx index 093c073e..af66da71 100644 --- a/src/features/App/index.tsx +++ b/src/features/App/index.tsx @@ -23,6 +23,7 @@ import { GlobalStyles } from 'features/GlobalStyles' import { Theme } from 'features/Theme' import { UnavailableText } from 'components/UnavailableText' +import ErrorBoundary from 'components/ErrorBoundary' import { AuthenticatedApp } from './AuthenticatedApp' import { useAuthStore } from '../AuthStore' @@ -40,10 +41,12 @@ const Main = () => { // имеется действующий токен return isToken ? ( - - - - + + + + + + ) : null } -- 2.30.2 From c4403ea089549767151cb91f862aaa756db49a49 Mon Sep 17 00:00:00 2001 From: Ruslan Khayrullin Date: Thu, 23 Mar 2023 16:04:27 +0500 Subject: [PATCH 07/10] 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, } -- 2.30.2 From 5f93cc47179101eef90c46a835acc70b44c62645 Mon Sep 17 00:00:00 2001 From: Ruslan Khayrullin <ruslfm08@gmail.com> Date: Thu, 6 Apr 2023 16:27:40 +0500 Subject: [PATCH 08/10] fix(in-424): fix: tour doesn't appear after returning --- src/features/MatchPage/store/hooks/index.tsx | 3 --- src/features/MatchTour/components/ContentComponent/hooks.tsx | 4 ++++ src/helpers/{bodyScroll => bodyScrollLock}/index.tsx | 0 src/helpers/index.tsx | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) rename src/helpers/{bodyScroll => bodyScrollLock}/index.tsx (100%) diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index ecb63fea..0af8572f 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -13,7 +13,6 @@ 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' @@ -26,7 +25,6 @@ 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' @@ -256,7 +254,6 @@ export const useMatchPage = () => { setStatsType(profile?.live ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS) isExpanded && toggleIsExpanded() - setLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY, 'true') } const isStarted = useMemo(() => ( diff --git a/src/features/MatchTour/components/ContentComponent/hooks.tsx b/src/features/MatchTour/components/ContentComponent/hooks.tsx index 66c15559..f0a9fc57 100644 --- a/src/features/MatchTour/components/ContentComponent/hooks.tsx +++ b/src/features/MatchTour/components/ContentComponent/hooks.tsx @@ -13,9 +13,12 @@ import { isMobileDevice, KEYBOARD_KEYS } from 'config' import { useEventListener } from 'hooks' +import { setLocalStorageItem } from 'helpers' + import { useMatchPageStore } from 'features/MatchPage/store' import { Tabs } from 'features/MatchSidePlaylists/config' import { StatsType, Tabs as StatTabs } from 'features/MatchSidePlaylists/components/TabStats/config' +import { TOUR_COMPLETED_STORAGE_KEY } from 'features/MatchTour' import { Steps } from '../../config' @@ -124,6 +127,7 @@ export const useContentComponent = ({ const skipTour = useCallback(() => { setIsOpen(false) + setLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY, 'true') }, [setIsOpen]) useEventListener({ diff --git a/src/helpers/bodyScroll/index.tsx b/src/helpers/bodyScrollLock/index.tsx similarity index 100% rename from src/helpers/bodyScroll/index.tsx rename to src/helpers/bodyScrollLock/index.tsx diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 2bfc875e..6ec4e5c7 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -14,4 +14,5 @@ export * from './getTeamAbbr' export * from './cookie' export * from './isMatchPage' export * from './languageUrlParam' -export * from './bodyScroll' +export * from './bodyScrollLock' +export * from './getLocalStorage' -- 2.30.2 From 37950255197f4cf6858e970c5527edc4c7cd6b3f Mon Sep 17 00:00:00 2001 From: Margarita <margarita.polyakova@instatsport.com> Date: Thu, 30 Mar 2023 15:51:50 +0400 Subject: [PATCH 09/10] feat(in-454): default landing --- src/config/lexics/indexLexics.tsx | 2 + src/config/lexics/landingLexics.tsx | 9 ++ src/features/App/AuthenticatedApp.tsx | 4 +- src/features/AuthStore/hooks/useAuth.tsx | 27 ++-- .../TeamLogoImg/index.tsx | 4 +- .../helpers.tsx | 0 src/features/Landings/hooks.tsx | 141 ++++++++++++++++++ .../{TournamentLanding => Landings}/index.tsx | 55 +++++-- .../styled.tsx | 46 +++--- src/features/MatchPage/store/hooks/index.tsx | 18 ++- src/features/TournamentLanding/hooks.tsx | 96 ------------ src/features/TournamentPage/hooks.tsx | 38 ++++- ...etTournamentLanding.tsx => getLanding.tsx} | 24 ++- src/requests/getLandingStatus.tsx | 20 ++- src/requests/index.tsx | 2 +- 15 files changed, 329 insertions(+), 157 deletions(-) create mode 100644 src/config/lexics/landingLexics.tsx rename src/features/{TournamentLanding => Landings}/TeamLogoImg/index.tsx (83%) rename src/features/{TournamentLanding => Landings}/helpers.tsx (100%) create mode 100644 src/features/Landings/hooks.tsx rename src/features/{TournamentLanding => Landings}/index.tsx (62%) rename src/features/{TournamentLanding => Landings}/styled.tsx (91%) delete mode 100644 src/features/TournamentLanding/hooks.tsx rename src/requests/{getTournamentLanding.tsx => getLanding.tsx} (64%) diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index 95118ce8..7830ed5e 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -3,6 +3,7 @@ import { publicLexics } from './public' import { highlightsPageLexic } from './highlightsPageLexic' import { mailingsLexics } from './mailings' import { sportsLexic } from './sportsLexic' +import { landingLexics } from './landingLexics' const matchPopupLexics = { actions: 1020, @@ -219,4 +220,5 @@ export const indexLexics = { ...paymentLexics, ...sportsLexic, ...sportsPopup, + ...landingLexics, } diff --git a/src/config/lexics/landingLexics.tsx b/src/config/lexics/landingLexics.tsx new file mode 100644 index 00000000..178a8aab --- /dev/null +++ b/src/config/lexics/landingLexics.tsx @@ -0,0 +1,9 @@ +export const landingLexics = { + inactive_button: 20083, + inactive_description_1: 20084, + inactive_description_2: 20086, + inactive_period: 801, + inactive_title_1: 20087, + inactive_title_2: 20088, + +} diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx index 9d010011..cddfc571 100644 --- a/src/features/App/AuthenticatedApp.tsx +++ b/src/features/App/AuthenticatedApp.tsx @@ -35,7 +35,7 @@ const MatchPage = lazy(() => import('features/MatchPage')) const PlayerPage = lazy(() => import('features/PlayerPage')) const TournamentPage = lazy(() => import('features/TournamentPage')) const SystemSettings = lazy(() => import('features/SystemSettings')) -const TournamentLanding = lazy(() => import('features/TournamentLanding')) +const Landings = lazy(() => import('features/Landings')) const HighlightsPage = lazy(() => import('pages/HighlightsPage')) const ThanksPage = lazy(() => import('pages/ThanksPage')) @@ -96,7 +96,7 @@ export const AuthenticatedApp = () => { <HighlightsPage /> </Route> <Route path={`${PAGES.landing}`}> - <TournamentLanding /> + <Landings /> </Route> <Redirect to={PAGES.home} /> </Switch> diff --git a/src/features/AuthStore/hooks/useAuth.tsx b/src/features/AuthStore/hooks/useAuth.tsx index 54e701bf..2ea88c5c 100644 --- a/src/features/AuthStore/hooks/useAuth.tsx +++ b/src/features/AuthStore/hooks/useAuth.tsx @@ -10,6 +10,7 @@ import type { User } from 'oidc-client' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' +import includes from 'lodash/includes' import { PAGES } from 'config' @@ -20,7 +21,6 @@ import { readToken, setCookie, removeCookie, - isMatchPage, TOKEN_KEY, } from 'helpers' @@ -125,15 +125,12 @@ export const useAuth = () => { validator: isBoolean, }) - useEffect(() => { - if (isMatchPage()) setPage(history.location.pathname) - if (history.location.pathname !== page) setIsFromLanding(false) - }, [ - history.location.pathname, - page, - setIsFromLanding, - setPage, - ]) + const [landingUrlFrom, setLandingUrlFrom] = useSessionStore({ + clearOnUnmount: true, + defaultValue: '', + key: 'landingUrlFrom', + validator: isString, + }) const getTemporaryToken = async () => { try { @@ -281,7 +278,11 @@ export const useAuth = () => { setUserInfo(userInfoFetched) - userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) + if (includes(window.location.pathname, PAGES.landing)) { + changeLang(navigator.language.substring(0, 2)) + } else { + userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) + } // eslint-disable-next-line no-empty } catch (error) {} @@ -295,11 +296,13 @@ export const useAuth = () => { fetchUserInfo, isFromLanding, isNewDeviceLogin, + landingUrlFrom, loadingUser, login, logout, page, setIsFromLanding, + setLandingUrlFrom, setPage, setSearch, setUserInfo, @@ -308,6 +311,7 @@ export const useAuth = () => { }), [ fetchUserInfo, isNewDeviceLogin, + landingUrlFrom, logout, user, userInfo, @@ -319,6 +323,7 @@ export const useAuth = () => { page, setIsFromLanding, isFromLanding, + setLandingUrlFrom, ]) return auth diff --git a/src/features/TournamentLanding/TeamLogoImg/index.tsx b/src/features/Landings/TeamLogoImg/index.tsx similarity index 83% rename from src/features/TournamentLanding/TeamLogoImg/index.tsx rename to src/features/Landings/TeamLogoImg/index.tsx index 59af1775..27a2823d 100644 --- a/src/features/TournamentLanding/TeamLogoImg/index.tsx +++ b/src/features/Landings/TeamLogoImg/index.tsx @@ -24,14 +24,16 @@ type Props = { export const TeamLogoImg = ({ src, }: Props) => { - const [isLogoError, setIsImgError] = useState(false) + const [isLogoError, setIsImgError] = useState(true) const onError = () => setIsImgError(true) + const onLoad = () => setIsImgError(false) return ( <LogoImg src={src} onError={onError} + onLoad={onLoad} isLogoError={isLogoError} /> ) diff --git a/src/features/TournamentLanding/helpers.tsx b/src/features/Landings/helpers.tsx similarity index 100% rename from src/features/TournamentLanding/helpers.tsx rename to src/features/Landings/helpers.tsx diff --git a/src/features/Landings/hooks.tsx b/src/features/Landings/hooks.tsx new file mode 100644 index 00000000..84dd9108 --- /dev/null +++ b/src/features/Landings/hooks.tsx @@ -0,0 +1,141 @@ +import { + useEffect, + useState, +} from 'react' + +import size from 'lodash/size' +import includes from 'lodash/includes' + +import type { Landing } from 'requests/getLanding' +import { getLanding } from 'requests/getLanding' + +import { PAGES } from 'config/pages' + +import { redirectToUrl } from 'helpers/redirectToUrl' + +import { useLexicsStore } from 'features/LexicsStore' +import { useAuthStore } from 'features/AuthStore' + +import { getLandingName, isPastLandingDate } from './helpers' +import { getName } from '../Name' + +export const useLandings = () => { + const [tournamentInfo, setTournamentInfo] = useState<Landing | null>(null) + const [isInactiveLanding, setIsInactiveLanding] = useState(false) + + const { addLexicsConfig, suffix } = useLexicsStore() + const { + landingUrlFrom, + setIsFromLanding, + user, + } = useAuthStore() + + const buttonLexic = tournamentInfo?.lexic_button || '' + const period = tournamentInfo?.lexic_period || '' + const title = tournamentInfo?.lexic_title || '' + const description = tournamentInfo?.lexic_description || '' + const gallery = tournamentInfo?.media.gallery + + useEffect(() => { + const lexics = [buttonLexic, period, title, description] + addLexicsConfig(lexics) + }, [ + addLexicsConfig, + buttonLexic, + description, + period, + title, + ]) + + const redirectToHomePage = () => redirectToUrl(PAGES.home) + const onButtonClick = () => { + if (includes(landingUrlFrom, PAGES.match) || includes(landingUrlFrom, PAGES.tournament)) { + setIsFromLanding(true) + redirectToUrl(landingUrlFrom) + sessionStorage.removeItem('landingUrlFrom') + } else { + redirectToUrl(tournamentInfo?.url_button || '') + } + } + + useEffect(() => { + (async () => { + const landingData = sessionStorage.getItem('landingData') + const parseLandingDate = landingData && JSON.parse(landingData) + + try { + const date = landingUrlFrom + ? await getLanding({ + landingName: parseLandingDate.landing_id || parseLandingDate.url_landing, + seasonId: parseLandingDate.season_id, + sportId: parseLandingDate.sport_id, + tournamentId: parseLandingDate.tournament_id, + }) + : await getLanding({ landingName: getLandingName() }) + if (user) return redirectToUrl(date.url_button || '') + if (isPastLandingDate(date.date_to)) setIsInactiveLanding(true) + setTournamentInfo(date) + return sessionStorage.removeItem('landingData') + } catch (err) { + return redirectToHomePage() + } + })() + }, [landingUrlFrom, user]) + + const [sliderItemId, setSliderItemId] = useState(0) + + const onSliderSwitchClick = (itemId: number) => setSliderItemId(itemId) + + const imgCounter = size(gallery) + + useEffect(() => { + if (sliderItemId === imgCounter) { + setSliderItemId(0) + } + const getSliderInterval = setInterval(() => { + setSliderItemId(sliderItemId + 1) + }, 5000) + return () => clearInterval(getSliderInterval) + }, [imgCounter, sliderItemId]) + + const isInactiveLandingData = () => { + if (!tournamentInfo?.tournaments || !isInactiveLanding) return null + + const { + season, + tournament_eng, + tournament_rus, + } = tournamentInfo.tournaments[0] + + const currentTournamentsTitle = { + name_eng: tournament_eng, + name_rus: tournament_rus, + } + + const tournamentsTitle = getName({ nameObj: currentTournamentsTitle, suffix }) + + return { + season, + tournamentsTitle, + } + } + + return { + buttonColor: tournamentInfo?.button_color, + buttonLexic, + description, + gallery, + isInactiveLanding, + isInactiveLandingData: isInactiveLandingData(), + logo: tournamentInfo?.media.logo, + logoInsports: tournamentInfo?.logo_insports, + onButtonClick, + onSliderSwitchClick, + period, + redirectToHomePage, + sliderItemId, + teams: tournamentInfo?.teams, + title, + tournamentInfo, + } +} diff --git a/src/features/TournamentLanding/index.tsx b/src/features/Landings/index.tsx similarity index 62% rename from src/features/TournamentLanding/index.tsx rename to src/features/Landings/index.tsx index 16f57b5d..b6c8ccca 100644 --- a/src/features/TournamentLanding/index.tsx +++ b/src/features/Landings/index.tsx @@ -1,3 +1,5 @@ +import { Fragment } from 'react' + import format from 'date-fns/format' import map from 'lodash/map' @@ -6,7 +8,7 @@ import { isMobileDevice } from 'config/userAgent' import { T9n } from 'features/T9n' -import { useTournamentLanding } from './hooks' +import { useLandings } from './hooks' import { TeamLogoImg } from './TeamLogoImg' import { @@ -36,12 +38,14 @@ import { TournamentInfoContainer, } from './styled' -const TournamentLanding = () => { +const Landings = () => { const { buttonColor, buttonLexic, description, gallery, + isInactiveLanding, + isInactiveLandingData, logo, logoInsports, onButtonClick, @@ -52,7 +56,7 @@ const TournamentLanding = () => { teams, title, tournamentInfo, - } = useTournamentLanding() + } = useLandings() if (!tournamentInfo) return null @@ -67,7 +71,7 @@ const TournamentLanding = () => { <MainInfoContainer> <BlockWrapper> { - gallery + gallery && !isInactiveLanding ? ( <SliderWrapper> <SliderContainer> @@ -99,18 +103,47 @@ const TournamentLanding = () => { } <TournamentInfoContainer> <TournamentInfo> - <DateInfo t={period} /> - <TournamentTitle t={title} /> - <TournamentDescription t={description} /> + {isInactiveLanding + ? ( + <Fragment> + <DateInfo> + <T9n t='inactive_period' />  + {isInactiveLandingData?.season} + </DateInfo> + <TournamentTitle> + <T9n t='inactive_title_1' />  + {isInactiveLandingData?.tournamentsTitle}  + <T9n t='inactive_title_2' /> + </TournamentTitle> + <TournamentDescription> + <T9n t='inactive_description_1' />  + {isInactiveLandingData?.tournamentsTitle}  + <T9n t='inactive_description_2' /> + </TournamentDescription> + </Fragment> + ) + : ( + <Fragment> + <DateInfo> + <T9n t={period} /> + </DateInfo> + <TournamentTitle> + <T9n t={title} /> + </TournamentTitle> + <TournamentDescription> + <T9n t={description} /> + </TournamentDescription> + </Fragment> + )} <TournamentButton buttonColor={buttonColor} onClick={onButtonClick} > - <T9n t={buttonLexic} /> + <T9n t={isInactiveLanding ? 'inactive_button' : buttonLexic} /> </TournamentButton> </TournamentInfo> <TournamentMedia> - {gallery && <TournamentLogo src={logo} />} + {(gallery && !isInactiveLanding) && <TournamentLogo src={logo} />} {teams && ( <TeamsLogo> {map(teams, (item) => ( @@ -121,7 +154,7 @@ const TournamentLanding = () => { ))} </TeamsLogo> )} - {logoInsports && <InsportsImg src='/images/insports-logo.svg' />} + {(logoInsports && !isInactiveLanding) && <InsportsImg src='/images/insports-logo.svg' />} </TournamentMedia> </TournamentInfoContainer> </BlockWrapper> @@ -131,4 +164,4 @@ const TournamentLanding = () => { ) } -export default TournamentLanding +export default Landings diff --git a/src/features/TournamentLanding/styled.tsx b/src/features/Landings/styled.tsx similarity index 91% rename from src/features/TournamentLanding/styled.tsx rename to src/features/Landings/styled.tsx index d67717db..ce9733f6 100644 --- a/src/features/TournamentLanding/styled.tsx +++ b/src/features/Landings/styled.tsx @@ -4,7 +4,6 @@ import { isMobileDevice } from 'config/userAgent' import { ButtonSolid } from 'features/Common' import { Logo } from 'features/Logo' -import { T9n } from 'features/T9n' type ButtonProps = { buttonColor?: string, @@ -24,6 +23,7 @@ export const Wrapper = styled.div` color: white; display: flex; flex-direction: column; + justify-content: space-between; ` export const HeaderWrapper = styled.div` @@ -56,7 +56,7 @@ export const InsportsLogo = styled(Logo)` ` export const MainInfoContainer = styled.div` - height: 100%; + height: 87%; ${isMobileDevice ? css` @@ -101,6 +101,7 @@ export const MainLogoWrapper = styled.div` width: 50%; position: relative; align-items: center; + margin-right: 1%; ${isMobileDevice ? css` @@ -113,8 +114,8 @@ export const MainLogoWrapper = styled.div` ` export const MainLogoImg = styled.img` - width: 35%; - height: 35%; + width: 48%; + height: 48%; position: relative; ${isMobileDevice @@ -198,7 +199,7 @@ export const SliderSwitchItem = styled.div<SliderSwitchProps>` height: 4px; border-radius: 2px; background-color: white; - opacity: ${({ slideOpacity }) => (slideOpacity ? '1' : '.3')};; + opacity: ${({ slideOpacity }) => (slideOpacity ? '1' : '.3')}; margin-right: 10px; cursor: pointer; transition: .7s; @@ -234,14 +235,14 @@ export const TournamentInfo = styled.div` margin-top: ${(isMobileDevice ? 'none' : '90px')}; ` -export const DateInfo = styled(T9n)` +export const DateInfo = styled.div` text-transform: uppercase; background-color: rgba(0, 0, 0, 0.4); padding: 8px 25px; color: #B9B9B9; width: fit-content; border-radius: 5px; - font-size: 13px; + font-size: .62rem; font-weight: 600; ${isMobileDevice @@ -249,14 +250,14 @@ export const DateInfo = styled(T9n)` font-size: 10px; border-radius: 3px; background-color: rgba(0, 0, 0, 0.7); - padding: 5px 10px;` + padding: 3px 10px;` : ''}; ` -export const TournamentTitle = styled(T9n)` +export const TournamentTitle = styled.div` font-weight: 700; - font-size: 50px; - margin-top: 50px; + font-size: 2.36rem; + margin-top: 2.4rem; ${isMobileDevice ? css` @@ -271,11 +272,13 @@ export const TournamentTitle = styled(T9n)` export const TournamentButton = styled(ButtonSolid)<ButtonProps>` width: 320px; height: fit-content; - font-size: 24px; + font-size: 1.13rem; font-weight: 600; border-radius: 5px; - margin-bottom: 90px; - padding: 20px 0; + margin-bottom: 4.25rem; + padding: 0.94rem 3rem; + display: flex; + justify-content: center; background-color: ${({ buttonColor }) => (buttonColor ? `${buttonColor}` : '#294FC3')}; ${isMobileDevice @@ -288,16 +291,17 @@ export const TournamentButton = styled(ButtonSolid)<ButtonProps>` : ''}; ` -export const TournamentDescription = styled(T9n)` - max-width: 400px; - margin: 50px 0; - font-size: 17px; +export const TournamentDescription = styled.div` + max-width: 26.6rem; + margin: 2.4rem 0; + font-size: 0.8rem; + letter-spacing: 0.1px; + line-height: 150%; ${isMobileDevice ? css` + max-width: 100%; font-size: 12px; - line-height: 150%; - letter-spacing: 0.1px; margin: 25px 0 30px; ` : ''}; @@ -307,7 +311,7 @@ export const TournamentMedia = styled.div` display: flex; align-items: center; height: 130px; - margin-bottom: 25px; + margin-bottom: 1.2rem; ${isMobileDevice ? css` diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index 0af8572f..f6730768 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -3,6 +3,7 @@ import { useState, useMemo, } from 'react' +import { useHistory } from 'react-router' import includes from 'lodash/includes' import filter from 'lodash/filter' @@ -79,10 +80,13 @@ export const useMatchPage = () => { const { isFromLanding, + landingUrlFrom, setIsFromLanding, + setLandingUrlFrom, user, userInfo, } = useAuthStore() + const history = useHistory() const { close: hideProfileCard, @@ -138,14 +142,22 @@ export const useMatchPage = () => { })) useEffect(() => { - if (user || isFromLanding) return + if (user || isFromLanding || history.length > 1) return getLandingStatus({ matchId, sportType }) - .then(({ landing_id }) => { + .then((data) => { + setLandingUrlFrom(window.location.pathname) + sessionStorage.setItem( + 'landingData', + JSON.stringify(data), + ) setIsFromLanding(false) - if (landing_id) redirectToUrl(`${PAGES.landing}/${landing_id}`) + if (data.landing_id) redirectToUrl(`${PAGES.landing}/${data.landing_id}`) }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ + setLandingUrlFrom, isFromLanding, + landingUrlFrom, matchId, setIsFromLanding, sportType, diff --git a/src/features/TournamentLanding/hooks.tsx b/src/features/TournamentLanding/hooks.tsx deleted file mode 100644 index 52d3d493..00000000 --- a/src/features/TournamentLanding/hooks.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { - useEffect, - useState, -} from 'react' - -import size from 'lodash/size' -import includes from 'lodash/includes' - -import type { TournamentLanding } from 'requests/getTournamentLanding' -import { getTournamentLanding } from 'requests/getTournamentLanding' - -import { PAGES } from 'config/pages' - -import { redirectToUrl } from 'helpers/redirectToUrl' - -import { useLexicsStore } from 'features/LexicsStore' -import { useAuthStore } from 'features/AuthStore' - -import { getLandingName, isPastLandingDate } from './helpers' - -export const useTournamentLanding = () => { - const [tournamentInfo, setTournamentInfo] = useState<TournamentLanding | null>(null) - - const { addLexicsConfig } = useLexicsStore() - const { page, setIsFromLanding } = useAuthStore() - - const buttonLexic = tournamentInfo?.lexic_button || '' - const period = tournamentInfo?.lexic_period || '' - const title = tournamentInfo?.lexic_title || '' - const description = tournamentInfo?.lexic_description || '' - const gallery = tournamentInfo?.media.gallery - - useEffect(() => { - const lexics = [buttonLexic, period, title, description] - addLexicsConfig(lexics) - }, [ - addLexicsConfig, - buttonLexic, - description, - period, - title, - ]) - - const redirectToHomePage = () => redirectToUrl(PAGES.home) - const onButtonClick = () => { - if (includes(page, 'matches')) { - setIsFromLanding(true) - redirectToUrl(page) - } else { - redirectToUrl(tournamentInfo?.url_button || '') - } - } - - useEffect(() => { - getTournamentLanding(getLandingName()) - .then((data) => ( - isPastLandingDate(data.date_to) - ? redirectToHomePage() - : setTournamentInfo(data) - )) - .catch(redirectToHomePage) - }, []) - - const [sliderItemId, setSliderItemId] = useState(0) - - const onSliderSwitchClick = (itemId: number) => setSliderItemId(itemId) - - const imgCounter = size(gallery) - - useEffect(() => { - if (sliderItemId === imgCounter) { - setSliderItemId(0) - } - const getSliderInterval = setInterval(() => { - setSliderItemId(sliderItemId + 1) - }, 5000) - return () => clearInterval(getSliderInterval) - }, [imgCounter, sliderItemId]) - - return { - buttonColor: tournamentInfo?.button_color, - buttonLexic, - description, - gallery, - logo: tournamentInfo?.media.logo, - logoInsports: tournamentInfo?.logo_insports, - onButtonClick, - onSliderSwitchClick, - period, - redirectToHomePage, - sliderItemId, - teams: tournamentInfo?.teams, - title, - tournamentInfo, - } -} diff --git a/src/features/TournamentPage/hooks.tsx b/src/features/TournamentPage/hooks.tsx index 13a7dcbd..5e78d5b6 100644 --- a/src/features/TournamentPage/hooks.tsx +++ b/src/features/TournamentPage/hooks.tsx @@ -11,13 +11,18 @@ import { getTournamentMatches, } from 'requests' +import { openSubscribePopup, redirectToUrl } from 'helpers' + +import { PAGES } from 'config/pages' + +import { useName } from 'features/Name' + import { checkUrlParams } from 'helpers/parseUrlParams/parseUrlParams' import { usePageParams } from 'hooks/usePageParams' -import { openSubscribePopup } from 'helpers' -import { useName } from 'features/Name' import { isPermittedTournament } from '../../helpers/isPermittedTournament' +import { getLandingStatus } from '../../requests/getLandingStatus' import { useProfileCard } from '../ProfileCard/hooks' import { useBuyMatchPopupStore } from '../BuyMatchPopup' import { MATCH_CONFIG } from '../BuyMatchPopup/store/hooks/useSubscriptions' @@ -30,7 +35,12 @@ export const useTournamentPage = () => { const country = useName(tournamentProfile?.country || {}) const history = useHistory() - const { user } = useAuthStore() + const { + isFromLanding, + setIsFromLanding, + setLandingUrlFrom, + user, + } = useAuthStore() const { isFavorite, toggleFavorites } = useProfileCard() @@ -47,6 +57,28 @@ export const useTournamentPage = () => { tournamentId, ]) + useEffect(() => { + if (user || isFromLanding || history.length > 1) return + getLandingStatus({ sportType, tournamentId }) + .then((data) => { + setLandingUrlFrom(window.location.pathname) + sessionStorage.setItem( + 'landingData', + JSON.stringify(data), + ) + setIsFromLanding(false) + if (data.landing_id) redirectToUrl(`${PAGES.landing}/${data.landing_id}`) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + setLandingUrlFrom, + isFromLanding, + tournamentId, + setIsFromLanding, + sportType, + user, + ]) + useEffect(() => { !isFavorite && checkUrlParams('from') === 'landing' diff --git a/src/requests/getTournamentLanding.tsx b/src/requests/getLanding.tsx similarity index 64% rename from src/requests/getTournamentLanding.tsx rename to src/requests/getLanding.tsx index 905f0513..6947aa1d 100644 --- a/src/requests/getTournamentLanding.tsx +++ b/src/requests/getLanding.tsx @@ -1,3 +1,5 @@ +import isUndefined from 'lodash/isUndefined' + import { API_ROOT } from 'config' import { callApi } from 'helpers' @@ -25,7 +27,7 @@ type Gallery = { url: string, } -export type TournamentLanding = { +export type Landing = { button_color?: string, date_from: string, date_to: string, @@ -42,19 +44,31 @@ export type TournamentLanding = { }, name: string, teams: Array<Teams>, - tournaments: Array<Tournaments>, + tournaments: Array<Tournaments> | null, url_button?: string, } -export const getTournamentLanding = async ( +type Args = { landingName: number | string, -): Promise<TournamentLanding> => { + seasonId?: number, + sportId?: number, + tournamentId?: number, +} + +export const getLanding = async ({ + landingName, + seasonId, + sportId, + tournamentId, +}: Args): Promise<Landing> => { const config = { method: 'GET', } return callApi({ config, - url: `${API_ROOT}/v1/landings/${landingName}`, + url: `${API_ROOT}/v1/landings/${landingName}${isUndefined(seasonId) + ? '' + : `?season_id=${seasonId}&sport_id=${sportId}&tournament_id=${tournamentId}`}`, }) } diff --git a/src/requests/getLandingStatus.tsx b/src/requests/getLandingStatus.tsx index d4ef5879..80e6d564 100644 --- a/src/requests/getLandingStatus.tsx +++ b/src/requests/getLandingStatus.tsx @@ -1,21 +1,35 @@ +import isUndefined from 'lodash/isUndefined' + import { API_ROOT } from 'config' import { callApi } from 'helpers' type Args = { - matchId: number, + matchId?: number, sportType: number, + tournamentId?: number, +} + +type LandingStatus = { + landing_id: number, + season_id: number, + sport_id: number, + tournament_id: number, + url_landing: string, } export const getLandingStatus = async ({ matchId, sportType, -}: Args): Promise<{landing_id: number | null}> => { + tournamentId, +}: Args): Promise<LandingStatus> => { const config = { method: 'GET', } return callApi({ config, - url: `${API_ROOT}/v1/landings/${sportType}/${matchId}/status`, + url: `${API_ROOT}/v1/landings/status/${sportType}?${ + isUndefined(matchId) ? '' : `match_id=${matchId}`}${ + isUndefined(tournamentId) ? '' : `tournament_id=${tournamentId}`}`, }) } diff --git a/src/requests/index.tsx b/src/requests/index.tsx index a2cff564..0613c71a 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -8,7 +8,7 @@ export * from './getUserSportFavs' export * from './modifyUserSportFavs' export * from './getSportTournaments' export * from './getTournamentInfo' -export * from './getTournamentLanding' +export * from './getLanding' export * from './getTeamInfo' export * from './getUserInfo' export * from './getMatchInfo' -- 2.30.2 From a910921bdede56d87716ca1d57bfe12a72a3003a Mon Sep 17 00:00:00 2001 From: "andrey.dekterev" <andrey.dekterev@insports.tv> Date: Thu, 6 Apr 2023 13:59:16 +0000 Subject: [PATCH 10/10] revert 37950255197f4cf6858e970c5527edc4c7cd6b3f revert feat(in-454): default landing --- src/config/lexics/indexLexics.tsx | 2 - src/config/lexics/landingLexics.tsx | 9 -- src/features/App/AuthenticatedApp.tsx | 4 +- src/features/AuthStore/hooks/useAuth.tsx | 27 ++-- src/features/Landings/hooks.tsx | 141 ------------------ src/features/MatchPage/store/hooks/index.tsx | 18 +-- .../TeamLogoImg/index.tsx | 4 +- .../helpers.tsx | 0 src/features/TournamentLanding/hooks.tsx | 96 ++++++++++++ .../{Landings => TournamentLanding}/index.tsx | 55 ++----- .../styled.tsx | 46 +++--- src/features/TournamentPage/hooks.tsx | 38 +---- src/requests/getLandingStatus.tsx | 20 +-- ...etLanding.tsx => getTournamentLanding.tsx} | 24 +-- src/requests/index.tsx | 2 +- 15 files changed, 157 insertions(+), 329 deletions(-) delete mode 100644 src/config/lexics/landingLexics.tsx delete mode 100644 src/features/Landings/hooks.tsx rename src/features/{Landings => TournamentLanding}/TeamLogoImg/index.tsx (83%) rename src/features/{Landings => TournamentLanding}/helpers.tsx (100%) create mode 100644 src/features/TournamentLanding/hooks.tsx rename src/features/{Landings => TournamentLanding}/index.tsx (62%) rename src/features/{Landings => TournamentLanding}/styled.tsx (91%) rename src/requests/{getLanding.tsx => getTournamentLanding.tsx} (64%) diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index 7830ed5e..95118ce8 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -3,7 +3,6 @@ import { publicLexics } from './public' import { highlightsPageLexic } from './highlightsPageLexic' import { mailingsLexics } from './mailings' import { sportsLexic } from './sportsLexic' -import { landingLexics } from './landingLexics' const matchPopupLexics = { actions: 1020, @@ -220,5 +219,4 @@ export const indexLexics = { ...paymentLexics, ...sportsLexic, ...sportsPopup, - ...landingLexics, } diff --git a/src/config/lexics/landingLexics.tsx b/src/config/lexics/landingLexics.tsx deleted file mode 100644 index 178a8aab..00000000 --- a/src/config/lexics/landingLexics.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export const landingLexics = { - inactive_button: 20083, - inactive_description_1: 20084, - inactive_description_2: 20086, - inactive_period: 801, - inactive_title_1: 20087, - inactive_title_2: 20088, - -} diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx index cddfc571..9d010011 100644 --- a/src/features/App/AuthenticatedApp.tsx +++ b/src/features/App/AuthenticatedApp.tsx @@ -35,7 +35,7 @@ const MatchPage = lazy(() => import('features/MatchPage')) const PlayerPage = lazy(() => import('features/PlayerPage')) const TournamentPage = lazy(() => import('features/TournamentPage')) const SystemSettings = lazy(() => import('features/SystemSettings')) -const Landings = lazy(() => import('features/Landings')) +const TournamentLanding = lazy(() => import('features/TournamentLanding')) const HighlightsPage = lazy(() => import('pages/HighlightsPage')) const ThanksPage = lazy(() => import('pages/ThanksPage')) @@ -96,7 +96,7 @@ export const AuthenticatedApp = () => { <HighlightsPage /> </Route> <Route path={`${PAGES.landing}`}> - <Landings /> + <TournamentLanding /> </Route> <Redirect to={PAGES.home} /> </Switch> diff --git a/src/features/AuthStore/hooks/useAuth.tsx b/src/features/AuthStore/hooks/useAuth.tsx index 2ea88c5c..54e701bf 100644 --- a/src/features/AuthStore/hooks/useAuth.tsx +++ b/src/features/AuthStore/hooks/useAuth.tsx @@ -10,7 +10,6 @@ import type { User } from 'oidc-client' import isString from 'lodash/isString' import isBoolean from 'lodash/isBoolean' -import includes from 'lodash/includes' import { PAGES } from 'config' @@ -21,6 +20,7 @@ import { readToken, setCookie, removeCookie, + isMatchPage, TOKEN_KEY, } from 'helpers' @@ -125,12 +125,15 @@ export const useAuth = () => { validator: isBoolean, }) - const [landingUrlFrom, setLandingUrlFrom] = useSessionStore({ - clearOnUnmount: true, - defaultValue: '', - key: 'landingUrlFrom', - validator: isString, - }) + useEffect(() => { + if (isMatchPage()) setPage(history.location.pathname) + if (history.location.pathname !== page) setIsFromLanding(false) + }, [ + history.location.pathname, + page, + setIsFromLanding, + setPage, + ]) const getTemporaryToken = async () => { try { @@ -278,11 +281,7 @@ export const useAuth = () => { setUserInfo(userInfoFetched) - if (includes(window.location.pathname, PAGES.landing)) { - changeLang(navigator.language.substring(0, 2)) - } else { - userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) - } + userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso) // eslint-disable-next-line no-empty } catch (error) {} @@ -296,13 +295,11 @@ export const useAuth = () => { fetchUserInfo, isFromLanding, isNewDeviceLogin, - landingUrlFrom, loadingUser, login, logout, page, setIsFromLanding, - setLandingUrlFrom, setPage, setSearch, setUserInfo, @@ -311,7 +308,6 @@ export const useAuth = () => { }), [ fetchUserInfo, isNewDeviceLogin, - landingUrlFrom, logout, user, userInfo, @@ -323,7 +319,6 @@ export const useAuth = () => { page, setIsFromLanding, isFromLanding, - setLandingUrlFrom, ]) return auth diff --git a/src/features/Landings/hooks.tsx b/src/features/Landings/hooks.tsx deleted file mode 100644 index 84dd9108..00000000 --- a/src/features/Landings/hooks.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { - useEffect, - useState, -} from 'react' - -import size from 'lodash/size' -import includes from 'lodash/includes' - -import type { Landing } from 'requests/getLanding' -import { getLanding } from 'requests/getLanding' - -import { PAGES } from 'config/pages' - -import { redirectToUrl } from 'helpers/redirectToUrl' - -import { useLexicsStore } from 'features/LexicsStore' -import { useAuthStore } from 'features/AuthStore' - -import { getLandingName, isPastLandingDate } from './helpers' -import { getName } from '../Name' - -export const useLandings = () => { - const [tournamentInfo, setTournamentInfo] = useState<Landing | null>(null) - const [isInactiveLanding, setIsInactiveLanding] = useState(false) - - const { addLexicsConfig, suffix } = useLexicsStore() - const { - landingUrlFrom, - setIsFromLanding, - user, - } = useAuthStore() - - const buttonLexic = tournamentInfo?.lexic_button || '' - const period = tournamentInfo?.lexic_period || '' - const title = tournamentInfo?.lexic_title || '' - const description = tournamentInfo?.lexic_description || '' - const gallery = tournamentInfo?.media.gallery - - useEffect(() => { - const lexics = [buttonLexic, period, title, description] - addLexicsConfig(lexics) - }, [ - addLexicsConfig, - buttonLexic, - description, - period, - title, - ]) - - const redirectToHomePage = () => redirectToUrl(PAGES.home) - const onButtonClick = () => { - if (includes(landingUrlFrom, PAGES.match) || includes(landingUrlFrom, PAGES.tournament)) { - setIsFromLanding(true) - redirectToUrl(landingUrlFrom) - sessionStorage.removeItem('landingUrlFrom') - } else { - redirectToUrl(tournamentInfo?.url_button || '') - } - } - - useEffect(() => { - (async () => { - const landingData = sessionStorage.getItem('landingData') - const parseLandingDate = landingData && JSON.parse(landingData) - - try { - const date = landingUrlFrom - ? await getLanding({ - landingName: parseLandingDate.landing_id || parseLandingDate.url_landing, - seasonId: parseLandingDate.season_id, - sportId: parseLandingDate.sport_id, - tournamentId: parseLandingDate.tournament_id, - }) - : await getLanding({ landingName: getLandingName() }) - if (user) return redirectToUrl(date.url_button || '') - if (isPastLandingDate(date.date_to)) setIsInactiveLanding(true) - setTournamentInfo(date) - return sessionStorage.removeItem('landingData') - } catch (err) { - return redirectToHomePage() - } - })() - }, [landingUrlFrom, user]) - - const [sliderItemId, setSliderItemId] = useState(0) - - const onSliderSwitchClick = (itemId: number) => setSliderItemId(itemId) - - const imgCounter = size(gallery) - - useEffect(() => { - if (sliderItemId === imgCounter) { - setSliderItemId(0) - } - const getSliderInterval = setInterval(() => { - setSliderItemId(sliderItemId + 1) - }, 5000) - return () => clearInterval(getSliderInterval) - }, [imgCounter, sliderItemId]) - - const isInactiveLandingData = () => { - if (!tournamentInfo?.tournaments || !isInactiveLanding) return null - - const { - season, - tournament_eng, - tournament_rus, - } = tournamentInfo.tournaments[0] - - const currentTournamentsTitle = { - name_eng: tournament_eng, - name_rus: tournament_rus, - } - - const tournamentsTitle = getName({ nameObj: currentTournamentsTitle, suffix }) - - return { - season, - tournamentsTitle, - } - } - - return { - buttonColor: tournamentInfo?.button_color, - buttonLexic, - description, - gallery, - isInactiveLanding, - isInactiveLandingData: isInactiveLandingData(), - logo: tournamentInfo?.media.logo, - logoInsports: tournamentInfo?.logo_insports, - onButtonClick, - onSliderSwitchClick, - period, - redirectToHomePage, - sliderItemId, - teams: tournamentInfo?.teams, - title, - tournamentInfo, - } -} diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index f6730768..0af8572f 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -3,7 +3,6 @@ import { useState, useMemo, } from 'react' -import { useHistory } from 'react-router' import includes from 'lodash/includes' import filter from 'lodash/filter' @@ -80,13 +79,10 @@ export const useMatchPage = () => { const { isFromLanding, - landingUrlFrom, setIsFromLanding, - setLandingUrlFrom, user, userInfo, } = useAuthStore() - const history = useHistory() const { close: hideProfileCard, @@ -142,22 +138,14 @@ export const useMatchPage = () => { })) useEffect(() => { - if (user || isFromLanding || history.length > 1) return + if (user || isFromLanding) return getLandingStatus({ matchId, sportType }) - .then((data) => { - setLandingUrlFrom(window.location.pathname) - sessionStorage.setItem( - 'landingData', - JSON.stringify(data), - ) + .then(({ landing_id }) => { setIsFromLanding(false) - if (data.landing_id) redirectToUrl(`${PAGES.landing}/${data.landing_id}`) + if (landing_id) redirectToUrl(`${PAGES.landing}/${landing_id}`) }) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - setLandingUrlFrom, isFromLanding, - landingUrlFrom, matchId, setIsFromLanding, sportType, diff --git a/src/features/Landings/TeamLogoImg/index.tsx b/src/features/TournamentLanding/TeamLogoImg/index.tsx similarity index 83% rename from src/features/Landings/TeamLogoImg/index.tsx rename to src/features/TournamentLanding/TeamLogoImg/index.tsx index 27a2823d..59af1775 100644 --- a/src/features/Landings/TeamLogoImg/index.tsx +++ b/src/features/TournamentLanding/TeamLogoImg/index.tsx @@ -24,16 +24,14 @@ type Props = { export const TeamLogoImg = ({ src, }: Props) => { - const [isLogoError, setIsImgError] = useState(true) + const [isLogoError, setIsImgError] = useState(false) const onError = () => setIsImgError(true) - const onLoad = () => setIsImgError(false) return ( <LogoImg src={src} onError={onError} - onLoad={onLoad} isLogoError={isLogoError} /> ) diff --git a/src/features/Landings/helpers.tsx b/src/features/TournamentLanding/helpers.tsx similarity index 100% rename from src/features/Landings/helpers.tsx rename to src/features/TournamentLanding/helpers.tsx diff --git a/src/features/TournamentLanding/hooks.tsx b/src/features/TournamentLanding/hooks.tsx new file mode 100644 index 00000000..52d3d493 --- /dev/null +++ b/src/features/TournamentLanding/hooks.tsx @@ -0,0 +1,96 @@ +import { + useEffect, + useState, +} from 'react' + +import size from 'lodash/size' +import includes from 'lodash/includes' + +import type { TournamentLanding } from 'requests/getTournamentLanding' +import { getTournamentLanding } from 'requests/getTournamentLanding' + +import { PAGES } from 'config/pages' + +import { redirectToUrl } from 'helpers/redirectToUrl' + +import { useLexicsStore } from 'features/LexicsStore' +import { useAuthStore } from 'features/AuthStore' + +import { getLandingName, isPastLandingDate } from './helpers' + +export const useTournamentLanding = () => { + const [tournamentInfo, setTournamentInfo] = useState<TournamentLanding | null>(null) + + const { addLexicsConfig } = useLexicsStore() + const { page, setIsFromLanding } = useAuthStore() + + const buttonLexic = tournamentInfo?.lexic_button || '' + const period = tournamentInfo?.lexic_period || '' + const title = tournamentInfo?.lexic_title || '' + const description = tournamentInfo?.lexic_description || '' + const gallery = tournamentInfo?.media.gallery + + useEffect(() => { + const lexics = [buttonLexic, period, title, description] + addLexicsConfig(lexics) + }, [ + addLexicsConfig, + buttonLexic, + description, + period, + title, + ]) + + const redirectToHomePage = () => redirectToUrl(PAGES.home) + const onButtonClick = () => { + if (includes(page, 'matches')) { + setIsFromLanding(true) + redirectToUrl(page) + } else { + redirectToUrl(tournamentInfo?.url_button || '') + } + } + + useEffect(() => { + getTournamentLanding(getLandingName()) + .then((data) => ( + isPastLandingDate(data.date_to) + ? redirectToHomePage() + : setTournamentInfo(data) + )) + .catch(redirectToHomePage) + }, []) + + const [sliderItemId, setSliderItemId] = useState(0) + + const onSliderSwitchClick = (itemId: number) => setSliderItemId(itemId) + + const imgCounter = size(gallery) + + useEffect(() => { + if (sliderItemId === imgCounter) { + setSliderItemId(0) + } + const getSliderInterval = setInterval(() => { + setSliderItemId(sliderItemId + 1) + }, 5000) + return () => clearInterval(getSliderInterval) + }, [imgCounter, sliderItemId]) + + return { + buttonColor: tournamentInfo?.button_color, + buttonLexic, + description, + gallery, + logo: tournamentInfo?.media.logo, + logoInsports: tournamentInfo?.logo_insports, + onButtonClick, + onSliderSwitchClick, + period, + redirectToHomePage, + sliderItemId, + teams: tournamentInfo?.teams, + title, + tournamentInfo, + } +} diff --git a/src/features/Landings/index.tsx b/src/features/TournamentLanding/index.tsx similarity index 62% rename from src/features/Landings/index.tsx rename to src/features/TournamentLanding/index.tsx index b6c8ccca..16f57b5d 100644 --- a/src/features/Landings/index.tsx +++ b/src/features/TournamentLanding/index.tsx @@ -1,5 +1,3 @@ -import { Fragment } from 'react' - import format from 'date-fns/format' import map from 'lodash/map' @@ -8,7 +6,7 @@ import { isMobileDevice } from 'config/userAgent' import { T9n } from 'features/T9n' -import { useLandings } from './hooks' +import { useTournamentLanding } from './hooks' import { TeamLogoImg } from './TeamLogoImg' import { @@ -38,14 +36,12 @@ import { TournamentInfoContainer, } from './styled' -const Landings = () => { +const TournamentLanding = () => { const { buttonColor, buttonLexic, description, gallery, - isInactiveLanding, - isInactiveLandingData, logo, logoInsports, onButtonClick, @@ -56,7 +52,7 @@ const Landings = () => { teams, title, tournamentInfo, - } = useLandings() + } = useTournamentLanding() if (!tournamentInfo) return null @@ -71,7 +67,7 @@ const Landings = () => { <MainInfoContainer> <BlockWrapper> { - gallery && !isInactiveLanding + gallery ? ( <SliderWrapper> <SliderContainer> @@ -103,47 +99,18 @@ const Landings = () => { } <TournamentInfoContainer> <TournamentInfo> - {isInactiveLanding - ? ( - <Fragment> - <DateInfo> - <T9n t='inactive_period' />  - {isInactiveLandingData?.season} - </DateInfo> - <TournamentTitle> - <T9n t='inactive_title_1' />  - {isInactiveLandingData?.tournamentsTitle}  - <T9n t='inactive_title_2' /> - </TournamentTitle> - <TournamentDescription> - <T9n t='inactive_description_1' />  - {isInactiveLandingData?.tournamentsTitle}  - <T9n t='inactive_description_2' /> - </TournamentDescription> - </Fragment> - ) - : ( - <Fragment> - <DateInfo> - <T9n t={period} /> - </DateInfo> - <TournamentTitle> - <T9n t={title} /> - </TournamentTitle> - <TournamentDescription> - <T9n t={description} /> - </TournamentDescription> - </Fragment> - )} + <DateInfo t={period} /> + <TournamentTitle t={title} /> + <TournamentDescription t={description} /> <TournamentButton buttonColor={buttonColor} onClick={onButtonClick} > - <T9n t={isInactiveLanding ? 'inactive_button' : buttonLexic} /> + <T9n t={buttonLexic} /> </TournamentButton> </TournamentInfo> <TournamentMedia> - {(gallery && !isInactiveLanding) && <TournamentLogo src={logo} />} + {gallery && <TournamentLogo src={logo} />} {teams && ( <TeamsLogo> {map(teams, (item) => ( @@ -154,7 +121,7 @@ const Landings = () => { ))} </TeamsLogo> )} - {(logoInsports && !isInactiveLanding) && <InsportsImg src='/images/insports-logo.svg' />} + {logoInsports && <InsportsImg src='/images/insports-logo.svg' />} </TournamentMedia> </TournamentInfoContainer> </BlockWrapper> @@ -164,4 +131,4 @@ const Landings = () => { ) } -export default Landings +export default TournamentLanding diff --git a/src/features/Landings/styled.tsx b/src/features/TournamentLanding/styled.tsx similarity index 91% rename from src/features/Landings/styled.tsx rename to src/features/TournamentLanding/styled.tsx index ce9733f6..d67717db 100644 --- a/src/features/Landings/styled.tsx +++ b/src/features/TournamentLanding/styled.tsx @@ -4,6 +4,7 @@ import { isMobileDevice } from 'config/userAgent' import { ButtonSolid } from 'features/Common' import { Logo } from 'features/Logo' +import { T9n } from 'features/T9n' type ButtonProps = { buttonColor?: string, @@ -23,7 +24,6 @@ export const Wrapper = styled.div` color: white; display: flex; flex-direction: column; - justify-content: space-between; ` export const HeaderWrapper = styled.div` @@ -56,7 +56,7 @@ export const InsportsLogo = styled(Logo)` ` export const MainInfoContainer = styled.div` - height: 87%; + height: 100%; ${isMobileDevice ? css` @@ -101,7 +101,6 @@ export const MainLogoWrapper = styled.div` width: 50%; position: relative; align-items: center; - margin-right: 1%; ${isMobileDevice ? css` @@ -114,8 +113,8 @@ export const MainLogoWrapper = styled.div` ` export const MainLogoImg = styled.img` - width: 48%; - height: 48%; + width: 35%; + height: 35%; position: relative; ${isMobileDevice @@ -199,7 +198,7 @@ export const SliderSwitchItem = styled.div<SliderSwitchProps>` height: 4px; border-radius: 2px; background-color: white; - opacity: ${({ slideOpacity }) => (slideOpacity ? '1' : '.3')}; + opacity: ${({ slideOpacity }) => (slideOpacity ? '1' : '.3')};; margin-right: 10px; cursor: pointer; transition: .7s; @@ -235,14 +234,14 @@ export const TournamentInfo = styled.div` margin-top: ${(isMobileDevice ? 'none' : '90px')}; ` -export const DateInfo = styled.div` +export const DateInfo = styled(T9n)` text-transform: uppercase; background-color: rgba(0, 0, 0, 0.4); padding: 8px 25px; color: #B9B9B9; width: fit-content; border-radius: 5px; - font-size: .62rem; + font-size: 13px; font-weight: 600; ${isMobileDevice @@ -250,14 +249,14 @@ export const DateInfo = styled.div` font-size: 10px; border-radius: 3px; background-color: rgba(0, 0, 0, 0.7); - padding: 3px 10px;` + padding: 5px 10px;` : ''}; ` -export const TournamentTitle = styled.div` +export const TournamentTitle = styled(T9n)` font-weight: 700; - font-size: 2.36rem; - margin-top: 2.4rem; + font-size: 50px; + margin-top: 50px; ${isMobileDevice ? css` @@ -272,13 +271,11 @@ export const TournamentTitle = styled.div` export const TournamentButton = styled(ButtonSolid)<ButtonProps>` width: 320px; height: fit-content; - font-size: 1.13rem; + font-size: 24px; font-weight: 600; border-radius: 5px; - margin-bottom: 4.25rem; - padding: 0.94rem 3rem; - display: flex; - justify-content: center; + margin-bottom: 90px; + padding: 20px 0; background-color: ${({ buttonColor }) => (buttonColor ? `${buttonColor}` : '#294FC3')}; ${isMobileDevice @@ -291,17 +288,16 @@ export const TournamentButton = styled(ButtonSolid)<ButtonProps>` : ''}; ` -export const TournamentDescription = styled.div` - max-width: 26.6rem; - margin: 2.4rem 0; - font-size: 0.8rem; - letter-spacing: 0.1px; - line-height: 150%; +export const TournamentDescription = styled(T9n)` + max-width: 400px; + margin: 50px 0; + font-size: 17px; ${isMobileDevice ? css` - max-width: 100%; font-size: 12px; + line-height: 150%; + letter-spacing: 0.1px; margin: 25px 0 30px; ` : ''}; @@ -311,7 +307,7 @@ export const TournamentMedia = styled.div` display: flex; align-items: center; height: 130px; - margin-bottom: 1.2rem; + margin-bottom: 25px; ${isMobileDevice ? css` diff --git a/src/features/TournamentPage/hooks.tsx b/src/features/TournamentPage/hooks.tsx index 5e78d5b6..13a7dcbd 100644 --- a/src/features/TournamentPage/hooks.tsx +++ b/src/features/TournamentPage/hooks.tsx @@ -11,18 +11,13 @@ import { getTournamentMatches, } from 'requests' -import { openSubscribePopup, redirectToUrl } from 'helpers' - -import { PAGES } from 'config/pages' - -import { useName } from 'features/Name' - import { checkUrlParams } from 'helpers/parseUrlParams/parseUrlParams' import { usePageParams } from 'hooks/usePageParams' +import { openSubscribePopup } from 'helpers' +import { useName } from 'features/Name' import { isPermittedTournament } from '../../helpers/isPermittedTournament' -import { getLandingStatus } from '../../requests/getLandingStatus' import { useProfileCard } from '../ProfileCard/hooks' import { useBuyMatchPopupStore } from '../BuyMatchPopup' import { MATCH_CONFIG } from '../BuyMatchPopup/store/hooks/useSubscriptions' @@ -35,12 +30,7 @@ export const useTournamentPage = () => { const country = useName(tournamentProfile?.country || {}) const history = useHistory() - const { - isFromLanding, - setIsFromLanding, - setLandingUrlFrom, - user, - } = useAuthStore() + const { user } = useAuthStore() const { isFavorite, toggleFavorites } = useProfileCard() @@ -57,28 +47,6 @@ export const useTournamentPage = () => { tournamentId, ]) - useEffect(() => { - if (user || isFromLanding || history.length > 1) return - getLandingStatus({ sportType, tournamentId }) - .then((data) => { - setLandingUrlFrom(window.location.pathname) - sessionStorage.setItem( - 'landingData', - JSON.stringify(data), - ) - setIsFromLanding(false) - if (data.landing_id) redirectToUrl(`${PAGES.landing}/${data.landing_id}`) - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - setLandingUrlFrom, - isFromLanding, - tournamentId, - setIsFromLanding, - sportType, - user, - ]) - useEffect(() => { !isFavorite && checkUrlParams('from') === 'landing' diff --git a/src/requests/getLandingStatus.tsx b/src/requests/getLandingStatus.tsx index 80e6d564..d4ef5879 100644 --- a/src/requests/getLandingStatus.tsx +++ b/src/requests/getLandingStatus.tsx @@ -1,35 +1,21 @@ -import isUndefined from 'lodash/isUndefined' - import { API_ROOT } from 'config' import { callApi } from 'helpers' type Args = { - matchId?: number, + matchId: number, sportType: number, - tournamentId?: number, -} - -type LandingStatus = { - landing_id: number, - season_id: number, - sport_id: number, - tournament_id: number, - url_landing: string, } export const getLandingStatus = async ({ matchId, sportType, - tournamentId, -}: Args): Promise<LandingStatus> => { +}: Args): Promise<{landing_id: number | null}> => { const config = { method: 'GET', } return callApi({ config, - url: `${API_ROOT}/v1/landings/status/${sportType}?${ - isUndefined(matchId) ? '' : `match_id=${matchId}`}${ - isUndefined(tournamentId) ? '' : `tournament_id=${tournamentId}`}`, + url: `${API_ROOT}/v1/landings/${sportType}/${matchId}/status`, }) } diff --git a/src/requests/getLanding.tsx b/src/requests/getTournamentLanding.tsx similarity index 64% rename from src/requests/getLanding.tsx rename to src/requests/getTournamentLanding.tsx index 6947aa1d..905f0513 100644 --- a/src/requests/getLanding.tsx +++ b/src/requests/getTournamentLanding.tsx @@ -1,5 +1,3 @@ -import isUndefined from 'lodash/isUndefined' - import { API_ROOT } from 'config' import { callApi } from 'helpers' @@ -27,7 +25,7 @@ type Gallery = { url: string, } -export type Landing = { +export type TournamentLanding = { button_color?: string, date_from: string, date_to: string, @@ -44,31 +42,19 @@ export type Landing = { }, name: string, teams: Array<Teams>, - tournaments: Array<Tournaments> | null, + tournaments: Array<Tournaments>, url_button?: string, } -type Args = { +export const getTournamentLanding = async ( landingName: number | string, - seasonId?: number, - sportId?: number, - tournamentId?: number, -} - -export const getLanding = async ({ - landingName, - seasonId, - sportId, - tournamentId, -}: Args): Promise<Landing> => { +): Promise<TournamentLanding> => { const config = { method: 'GET', } return callApi({ config, - url: `${API_ROOT}/v1/landings/${landingName}${isUndefined(seasonId) - ? '' - : `?season_id=${seasonId}&sport_id=${sportId}&tournament_id=${tournamentId}`}`, + url: `${API_ROOT}/v1/landings/${landingName}`, }) } diff --git a/src/requests/index.tsx b/src/requests/index.tsx index 0613c71a..a2cff564 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -8,7 +8,7 @@ export * from './getUserSportFavs' export * from './modifyUserSportFavs' export * from './getSportTournaments' export * from './getTournamentInfo' -export * from './getLanding' +export * from './getTournamentLanding' export * from './getTeamInfo' export * from './getUserInfo' export * from './getMatchInfo' -- 2.30.2