develop #99

Merged
andrey.dekterev merged 7 commits from develop into master 3 years ago
  1. 1
      .eslintrc
  2. 38828
      package-lock.json
  3. 1
      package.json
  4. 1
      src/components/ItemInfo/ItemInfo.tsx
  5. 1
      src/config/clients/types.tsx
  6. 1
      src/config/index.tsx
  7. 1
      src/config/pages.tsx
  8. 4
      src/config/queries.tsx
  9. 1
      src/features/AirPlay/index.tsx
  10. 4
      src/features/App/AuthenticatedApp.tsx
  11. 12
      src/features/App/index.tsx
  12. 34
      src/features/AuthStore/hooks/useAuth.tsx
  13. 1
      src/features/ChromeCast/index.tsx
  14. 1
      src/features/Combobox/helpers/index.tsx
  15. 2
      src/features/HeaderFilters/store/hooks/index.tsx
  16. 5
      src/features/Icon/index.tsx
  17. 9
      src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx
  18. 9
      src/features/MatchCard/CardFrontside/index.tsx
  19. 6
      src/features/MatchCard/index.tsx
  20. 17
      src/features/MatchPage/components/FavouriteTeam/hooks.tsx
  21. 32
      src/features/MatchPage/components/MatchDescription/index.tsx
  22. 42
      src/features/MatchPage/store/hooks/index.tsx
  23. 1
      src/features/MatchPage/styled.tsx
  24. 31
      src/features/MatchesGrid/index.tsx
  25. 1
      src/features/MultiSourcePlayer/components/ProgressBar/hooks.tsx
  26. 2
      src/features/Name/index.tsx
  27. 7
      src/features/ProfileHeader/styled.tsx
  28. 2
      src/features/SportsFilter/components/SelectSportPopup/index.tsx
  29. 1
      src/features/StreamPlayer/components/ProgressBar/hooks.tsx
  30. 44
      src/features/StreamPlayer/hooks/index.tsx
  31. 2
      src/features/StreamPlayer/hooks/useSlider.tsx
  32. 38
      src/features/TournamentLanding/TeamLogoImg/index.tsx
  33. 16
      src/features/TournamentLanding/helpers.tsx
  34. 95
      src/features/TournamentLanding/hooks.tsx
  35. 131
      src/features/TournamentLanding/index.tsx
  36. 362
      src/features/TournamentLanding/styled.tsx
  37. 8
      src/features/UserAccount/components/PageSubscriptions/hooks.tsx
  38. 1
      src/helpers/callApi/types.tsx
  39. 1
      src/hooks/useEventListener.tsx
  40. 29
      src/hooks/useLocalStorage.tsx
  41. 2
      src/hooks/useObjectState.tsx
  42. 1
      src/hooks/useRequest.tsx
  43. 1
      src/hooks/useStorage/index.tsx
  44. 4
      src/pages/HighlightsPage/components/FormHighlights/hooks.tsx
  45. 21
      src/requests/getLandingStatus.tsx
  46. 28
      src/requests/getLiveScores.tsx
  47. 14
      src/requests/getMatchInfo.tsx
  48. 33
      src/requests/getMatchScore.tsx
  49. 2
      src/requests/getSound.tsx
  50. 1
      src/requests/getSounds.tsx
  51. 60
      src/requests/getTournamentLanding.tsx
  52. 2
      src/requests/getUserSubscribes.tsx
  53. 3
      src/requests/index.tsx
  54. 2
      src/serviceWorker.ts

@ -11,6 +11,7 @@
"postro4no"
],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/array-type": [
"warn",
{ "default" : "generic" }

38828
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -33,6 +33,7 @@
"react": "^17.0.2",
"react-datepicker": "^3.1.3",
"react-dom": "^17.0.2",
"react-query": "^3.39.3",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^5.0.1",

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
ProfileTypes,
PROFILE_NAMES,

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { css } from 'styled-components/macro'
type ProcedureName = string

@ -10,3 +10,4 @@ export * from './currencies'
export * from './dashes'
export * from './env'
export * from './userAgent'
export * from './queries'

@ -3,6 +3,7 @@ export const PAGES = {
failedPaymee: '/failed-paymee',
highlights: '/highlights',
home: '/',
landing: '/landing',
mailings: '/useraccount/mailings',
match: '/matches',
player: '/players',

@ -0,0 +1,4 @@
export const querieKeys = {
liveMatchScores: 'liveMatchScores',
matchScore: 'matchScore',
}

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect } from 'react'
import includes from 'lodash/includes'

@ -36,6 +36,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 HighlightsPage = lazy(() => import('pages/HighlightsPage'))
const ThanksPage = lazy(() => import('pages/ThanksPage'))
@ -94,6 +95,9 @@ export const AuthenticatedApp = () => {
<Route path={`${PAGES.highlights}`}>
<HighlightsPage />
</Route>
<Route path={`${PAGES.landing}`}>
<TournamentLanding />
</Route>
<Redirect to={PAGES.home} />
</Switch>
{!isProduction && <SystemSettings />}

@ -4,6 +4,8 @@ import {
useState,
} from 'react'
import { Router } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import { history } from 'config/history'
import { client } from 'config/clients'
@ -11,6 +13,8 @@ import { isAvailable } from 'config/env'
import { readToken } from 'helpers'
import { isLocalhost } from 'serviceWorker'
import { setClientTitleAndDescription } from 'helpers/setClientHeads'
import { GlobalStores } from 'features/GlobalStores'
@ -28,13 +32,19 @@ setClientTitleAndDescription(client.title, client.description)
const Main = () => {
const [isToken, setIsToken] = useState(false)
const { userInfo } = useAuthStore()
const queryClient = new QueryClient()
useEffect(() => {
readToken() && setIsToken(true)
}, [userInfo])
// имеется действующий токен
return isToken ? <AuthenticatedApp /> : null
return isToken ? (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={isLocalhost} />
<AuthenticatedApp />
</QueryClientProvider>
) : null
}
const date = new Date()

@ -27,18 +27,23 @@ import {
setCookie,
removeCookie,
} from 'helpers/cookie'
import { isMatchPage } from 'helpers/isMatchPage'
import { useLocalStore, useToggle } from 'hooks'
import {
useLocalStore,
useSessionStore,
useToggle,
} from 'hooks'
import { useLexicsStore } from 'features/LexicsStore'
import { queryParamStorage } from 'features/QueryParamsStorage'
import { getUserInfo, UserInfo } from 'requests/getUserInfo'
import { checkDevice, FailedResponse } from 'requests/checkDevice'
import { getTokenVirtualUser } from 'requests/getTokenVirtualUser'
// eslint-disable-next-line
import { getClientSettings, needCheckNewDeviсe } from '../helpers'
import { getTokenVirtualUser } from '../../../requests'
export const useAuth = () => {
const { changeLang, lang } = useLexicsStore()
@ -116,6 +121,23 @@ export const useAuth = () => {
validator: isString,
})
const [isFromLanding, setIsFromLanding] = useSessionStore({
clearOnUnmount: true,
defaultValue: false,
key: 'isFromLanding',
validator: isBoolean,
})
useEffect(() => {
if (isMatchPage()) setPage(history.location.pathname)
if (history.location.pathname !== page) setIsFromLanding(false)
}, [
history.location.pathname,
page,
setIsFromLanding,
setPage,
])
const getTemporaryToken = async () => {
try {
const { access_token } = await getTokenVirtualUser()
@ -246,10 +268,14 @@ export const useAuth = () => {
const auth = useMemo(() => ({
fetchUserInfo,
isFromLanding,
isNewDeviceLogin,
loadingUser,
login,
logout,
page,
setIsFromLanding,
setPage,
setSearch,
setUserInfo,
user,
@ -263,7 +289,11 @@ export const useAuth = () => {
login,
loadingUser,
setSearch,
setPage,
setUserInfo,
page,
setIsFromLanding,
isFromLanding,
])
return auth

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Fragment,
memo,

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import startsWith from 'lodash/startsWith'
import orderBy from 'lodash/orderBy'
import toLower from 'lodash/toLower'

@ -34,7 +34,7 @@ export const useFilters = () => {
const [selectedFilters, setSelectedFilters] = useState<Array<string>>([])
const [isShowTournament, setIsShowTournament] = useState(true)
const [selectTournament, setSelectTournament] = useState<TournamentType>()
const [sportIds, setSportIds] = useState<any>()
const [sportIds, setSportIds] = useState<Array<number>>([])
const isTodaySelected = isToday(selectedDate)
const compareSport = useCallback((match: Match, sportNames: Array<string>) => {

@ -1,4 +1,5 @@
import React from 'react'
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { CSSProperties } from 'react'
import * as icons from '../../libs/index'
export type IconProps = {
@ -8,7 +9,7 @@ export type IconProps = {
onClick?: () => void,
refIcon: any,
size?: number | string,
styles?: any,
styles?: CSSProperties,
}
export const Icon: React.FC<IconProps> = ({

@ -12,8 +12,11 @@ import { useName } from 'features/Name'
import { T9n } from 'features/T9n'
import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction'
import { useUserFavoritesStore } from 'features/UserFavorites/store'
import { getCardColor } from 'helpers/getCardColor'
import type { LiveScore } from 'requests'
import { NoAccessMessage } from '../../NoAccessMessage'
import { getPrepareTimeFormat } from '../../helpers'
import { getPrepareDateFormat } from '../../helpers/getPrepareDateFormat'
@ -50,6 +53,7 @@ type Props = {
match: Match,
onClick: () => void,
onKeyPress: (e: KeyboardEvent<HTMLLIElement>) => void,
score?: LiveScore,
}
export const CardFrontsideMobile = ({
@ -58,6 +62,7 @@ export const CardFrontsideMobile = ({
match,
onClick,
onKeyPress,
score,
}: Props) => {
const location = useLocation()
const {
@ -150,14 +155,14 @@ export const CardFrontsideMobile = ({
<TeamName nameObj={team1} />
{team1InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{team1.score}</Score>}
{showScore && <Score>{score?.team1.score ?? team1.score}</Score>}
</Team>
<Team>
<NameSignWrapper>
<TeamName nameObj={team2} />
{team2InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{team2.score}</Score>}
{showScore && <Score>{score?.team2.score ?? team2.score}</Score>}
</Team>
</Teams>
<SecondaryInfo>

@ -6,7 +6,10 @@ import getUnixTime from 'date-fns/getUnixTime'
import { ProfileTypes, PAGES } from 'config'
import { isLffClient } from 'config/clients'
import type { LiveScore } from 'requests'
import { getCardColor } from 'helpers/getCardColor'
import type { Match } from 'features/Matches'
import { useMatchSwitchesStore } from 'features/MatchSwitches'
import { useName } from 'features/Name'
@ -47,6 +50,7 @@ type Props = {
match: Match,
onClick: () => void,
onKeyPress: (e: KeyboardEvent<HTMLLIElement>) => void,
score?: LiveScore,
}
export const CardFrontside = ({
@ -54,6 +58,7 @@ export const CardFrontside = ({
match,
onClick,
onKeyPress,
score,
}: Props) => {
const location = useLocation()
const {
@ -165,14 +170,14 @@ export const CardFrontside = ({
<TeamName nameObj={team1} />
{team1InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{team1.score}</Score>}
{showScore && <Score>{score?.team1.score ?? team1.score}</Score>}
</Team>
<Team isMatchPage={isMatchPage}>
<NameSignWrapper>
<TeamName nameObj={team2} />
{team2InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{team2.score}</Score>}
{showScore && <Score>{score?.team2.score ?? team2.score}</Score>}
</Team>
</Teams>
{!isMatchPage && (

@ -2,15 +2,18 @@ import type { Match } from 'features/Matches'
import { isMobileDevice } from 'config/userAgent'
import { LiveScore } from 'requests/getLiveScores'
import { CardFrontside } from './CardFrontside'
import { CardFrontsideMobile } from './CardFrontside/MatchCardMobile'
import { useCard } from './hooks'
type Props = {
match: Match,
score?: LiveScore,
}
export const MatchCard = ({ match }: Props) => {
export const MatchCard = ({ match, score }: Props) => {
const {
isNeedFormatTimeChanged,
isOwnedMatches,
@ -27,6 +30,7 @@ export const MatchCard = ({ match }: Props) => {
onKeyPress={onKeyPress}
isNeedFormatTimeChanged={isNeedFormatTimeChanged}
isOwnedMatches={isOwnedMatches}
score={score}
/>
)
}

@ -13,14 +13,15 @@ export const useFavouriteTeam = () => {
const [group1, setGroup1] = useState<Array<FavouriteTeams>>([])
const [group2, setGroup2] = useState<Array<FavouriteTeams>>([])
const sortTeam = (teams: Array<any>) => teams.sort((n1:FavouriteTeams, n2: FavouriteTeams) => {
if (n1.name_en > n2.name_en) {
return 1
} if (n1.name_en < n2.name_en) {
return -1
}
return 0
})
const sortTeam = (teams: Array<FavouriteTeams>) => (
teams.sort((n1:FavouriteTeams, n2: FavouriteTeams) => {
if (n1.name_en > n2.name_en) {
return 1
} if (n1.name_en < n2.name_en) {
return -1
}
return 0
}))
const changeActive = (team: FavouriteTeams) => {
setActiveId((prev) => (prev === team.id ? null : team.id))
}

@ -1,5 +1,7 @@
import { useCallback } from 'react'
import { useQuery } from 'react-query'
import { format } from 'date-fns'
import includes from 'lodash/includes'
@ -16,11 +18,16 @@ import { useMatchPageStore } from 'features/MatchPage/store'
import { parseDate } from 'helpers/parseDate'
import { ProfileTypes } from 'config'
import { isMobileDevice } from 'config/userAgent'
import {
ProfileTypes,
querieKeys,
isMobileDevice,
} from 'config'
import { usePageParams } from 'hooks/usePageParams'
import { getMatchScore } from 'requests'
import {
Description,
DescriptionInnerBlock,
@ -33,7 +40,10 @@ import {
} from './styled'
export const MatchDescription = () => {
const { sportType } = usePageParams()
const {
profileId,
sportType,
} = usePageParams()
const { user } = useAuthStore()
const { isScoreHidden } = useMatchSwitchesStore()
const { suffix } = useLexicsStore()
@ -45,6 +55,19 @@ export const MatchDescription = () => {
: <Name nameObj={team} />
), [suffix])
const { data: queryScore } = useQuery({
queryFn: async () => {
if (profile?.live && !isScoreHidden) {
const score = await getMatchScore({ profileId, sportType })
return score
}
return null
},
queryKey: querieKeys.matchScore,
refetchInterval: 5000,
})
if (!profile) return <Description />
const {
@ -77,7 +100,8 @@ export const MatchDescription = () => {
{
isScoreHidden || isNil(team1.score) || isNil(team2.score)
? '-'
: `${team1.score} - ${team2.score}`
: `${queryScore?.team1.score ?? team1.score} - ${queryScore?.team2.score ?? team2.score}`
}
</Score>
<StyledLink

@ -8,16 +8,19 @@ import includes from 'lodash/includes'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
import { FULL_GAME_KEY } from 'features/MatchPage/helpers/buildPlaylists'
import { useAuthStore } from 'features/AuthStore'
import { PAGES } from 'config/pages'
import type { MatchInfo } from 'requests/getMatchInfo'
import { getMatchInfo } from 'requests/getMatchInfo'
import { getViewMatchDuration } from 'requests/getViewMatchDuration'
import { getLandingStatus } from 'requests/getLandingStatus'
import { usePageParams } from 'hooks/usePageParams'
import { useToggle } from 'hooks/useToggle'
import { redirectToUrl } from 'helpers/redirectToUrl'
import { parseDate } from 'helpers/parseDate'
import { useTournamentData } from './useTournamentData'
@ -33,7 +36,16 @@ export const useMatchPage = () => {
const [access, setAccess] = useState(true)
const { profileId: matchId, sportType } = usePageParams()
const { user, userInfo } = useAuthStore()
useEffect(() => {
sessionStorage.removeItem('isFromLanding')
}, [])
const {
isFromLanding,
setIsFromLanding,
user,
userInfo,
} = useAuthStore()
const {
close: hideProfileCard,
@ -88,6 +100,21 @@ export const useMatchPage = () => {
}
}))
useEffect(() => {
if (user || isFromLanding) return
getLandingStatus({ matchId, sportType })
.then(({ landing_id }) => {
setIsFromLanding(false)
if (landing_id) redirectToUrl(`${PAGES.landing}/${landing_id}`)
})
}, [
isFromLanding,
matchId,
setIsFromLanding,
sportType,
user,
])
useEffect(() => {
getMatchInfo(sportType, matchId).then(setMatchProfile)
}, [sportType, matchId])
@ -120,17 +147,6 @@ export const useMatchPage = () => {
userInfo,
])
useEffect(() => {
let getIntervalMatch: ReturnType<typeof setInterval>
if (matchProfile?.live && selectedPlaylist?.id === FULL_GAME_KEY
) {
getIntervalMatch = setInterval(
() => getMatchInfo(sportType, matchId).then(setMatchProfile), 5000,
)
}
return () => clearInterval(getIntervalMatch)
})
const isStarted = useMemo(() => (
profile?.date
? parseDate(profile.date) < new Date()

@ -28,7 +28,6 @@ export const Wrapper = styled.div`
export const Container = styled.div`
width: 100%;
max-width: 2090px;
max-height: 896px;
display: flex;
flex-direction: column;

@ -1,14 +1,20 @@
import { memo, useEffect } from 'react'
import { useRouteMatch } from 'react-router-dom'
import { useQuery } from 'react-query'
import { PAGES } from 'config/pages'
import type { LiveScore } from 'requests'
import { getLiveScores } from 'requests'
import { MatchCard } from 'features/MatchCard'
import { TournamentList } from 'features/TournamentList'
import type { Match } from 'features/Matches'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { Wrapper } from './styled'
import { useMatchSwitchesStore } from '../MatchSwitches'
import { querieKeys } from '../../config'
type MatchesGridProps = {
matches: Array<Match>,
@ -16,6 +22,7 @@ type MatchesGridProps = {
export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
const isHomePage = useRouteMatch(PAGES.home)?.isExact
const { isScoreHidden } = useMatchSwitchesStore()
const {
compareSport,
@ -48,19 +55,39 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
return matches
}
const { data: liveMatchScores } = useQuery({
queryFn: async () => {
if (!isScoreHidden && matches.filter(({ live }) => live)?.length > 0) {
const scores = await getLiveScores()
return scores
}
return []
},
queryKey: querieKeys.liveMatchScores,
refetchInterval: 5000,
})
useEffect(() => {
if (!isHomePage) return
updateSportIds(matches)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDate, matches])
return (
<Wrapper>
{isHomePage && isShowTournament ? (
<TournamentList matches={filteredMatches()} />
) : (
filteredMatches().map((match) => <MatchCard key={match.id} match={match} />)
filteredMatches().map((match) => (
<MatchCard
key={match.id}
match={match}
score={liveMatchScores?.find(
({ match_id, sport_id }: LiveScore) => match_id === match.id
&& sport_id === match.sportType,
)}
/>
))
)}
</Wrapper>
)

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMemo } from 'react'
import { secondsToHms } from 'helpers'

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import styled from 'styled-components/macro'
import { useLexicsStore } from 'features/LexicsStore'

@ -65,6 +65,12 @@ export const HeaderStyled = styled.header<HeaderProps>`
` : ''
)}
${({ color }) => (
client.name === 'facr' ? css`
background: ${color};
` : ''
)}
${({ isMatchPage }) => css`
${isMobileDevice
? css`
@ -73,7 +79,6 @@ export const HeaderStyled = styled.header<HeaderProps>`
`
: ''}
`}
`
type Props = {

@ -19,7 +19,7 @@ type Props = {
onModalClose: () => void,
onSportClick: (sport: string) => void,
selectedSport: Array<string>,
sportIds: Array<string>,
sportIds: Array<number>,
sports: SportsType,
}
export const SelectSportPopup = ({

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMemo, RefObject } from 'react'
import { secondsToHms } from 'helpers'

@ -10,6 +10,8 @@ import size from 'lodash/size'
import isNumber from 'lodash/isNumber'
import isEmpty from 'lodash/isEmpty'
import Hls from 'hls.js'
import { isIOS } from 'config/userAgent'
import {
@ -37,6 +39,7 @@ import { useProgressChangeHandler } from './useProgressChangeHandler'
import { usePlayingHandlers } from './usePlayingHandlers'
import { useDuration } from './useDuration'
import { useAudioTrack } from './useAudioTrack'
import { FULL_GAME_KEY } from '../../MatchPage/helpers/buildPlaylists'
export type PlayerState = typeof initialState
@ -125,6 +128,16 @@ export const useVideoPlayer = ({
togglePlaying,
} = usePlayingHandlers(setPlayerState, chapters)
const restartVideo = () => {
onPlaylistSelect({
duration: 0,
episodes: [],
id: FULL_GAME_KEY,
lexic: 13028,
type: 0,
})
}
const getActiveChapter = useCallback(
(index: number = activeChapterIndex) => chapters[index],
[chapters, activeChapterIndex],
@ -231,20 +244,15 @@ export const useVideoPlayer = ({
const backToLive = useCallback(() => {
if (!duration) return
if (selectedPlaylist?.id !== 'full_game') {
onPlaylistSelect({
duration: 0,
episodes: [],
id: 'full_game',
lexic: 13028,
type: 0,
})
if (selectedPlaylist?.id !== FULL_GAME_KEY) {
restartVideo()
setIsLiveTime(true)
}
const liveProgressMs = Math.max(duration - 30000, 0)
setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 })
if (liveProgressMs > 0) setIsLiveTime(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
duration,
onPlaylistSelect,
@ -255,14 +263,8 @@ export const useVideoPlayer = ({
const backToPausedTime = useCallback(() => {
if (!duration) return
if (selectedPlaylist?.id !== 'full_game') {
onPlaylistSelect({
duration: 0,
episodes: [],
id: 'full_game',
lexic: 13028,
type: 0,
})
if (selectedPlaylist?.id !== FULL_GAME_KEY) {
restartVideo()
setIsPausedTime(true)
}
@ -403,6 +405,16 @@ export const useVideoPlayer = ({
stopPlaying,
])
useEffect(() => {
/* воспроизводим плейлист полного матча с начала при завершении трансляции */
if (isLive) hls?.on(Hls.Events.BUFFER_EOS, restartVideo)
return () => {
hls?.off(Hls.Events.BUFFER_EOS, restartVideo)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hls, isLive])
useEffect(() => {
if (!navigator.serviceWorker || !isIOS) return undefined
const listener = (event: MessageEvent) => {

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useRef } from 'react'
import { useEventListener, useToggle } from 'hooks'

@ -0,0 +1,38 @@
import { useState } from 'react'
import styled from 'styled-components/macro'
type LogoImgProps = {
isLogoError: boolean,
}
export const LogoImg = styled.img<LogoImgProps>`
height: 25px;
width: 25px;
margin-right: 15px;
display: ${({ isLogoError }) => (isLogoError ? 'none' : '')};
filter: grayscale(1);
:hover {
filter: none;
}
`
type Props = {
src: string,
}
export const TeamLogoImg = ({
src,
}: Props) => {
const [isLogoError, setIsImgError] = useState(false)
const onError = () => setIsImgError(true)
return (
<LogoImg
src={src}
onError={onError}
isLogoError={isLogoError}
/>
)
}

@ -0,0 +1,16 @@
import { isPast } from 'date-fns'
export const getLandingName = () => {
const splitPath = window.location.pathname.split('/')
return splitPath[2]
}
const convertToDate = (date: string) => {
const d = date.split('.')
return new Date(`${d[2]}/${d[1]}/${d[0]}`)
}
export const isPastLandingDate = (data: string) => {
const landingDate = convertToDate(data)
return isPast(landingDate)
}

@ -0,0 +1,95 @@
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,
}
}

@ -0,0 +1,131 @@
import format from 'date-fns/format'
import map from 'lodash/map'
import { isMobileDevice } from 'config/userAgent'
import { T9n } from 'features/T9n'
import { useTournamentLanding } from './hooks'
import { TeamLogoImg } from './TeamLogoImg'
import {
Wrapper,
InsportsLogo,
HeaderWrapper,
Footer,
BlockWrapper,
TournamentInfo,
DateInfo,
InsportsImg,
TournamentMedia,
TournamentLogo,
TournamentTitle,
TournamentButton,
MainInfoContainer,
TournamentDescription,
TeamsLogo,
SliderContainer,
SliderWrapper,
MainLogoImg,
MainLogoWrapper,
SliderSwitch,
SliderSwitchItem,
SliderImg,
LogoBackground,
TournamentInfoContainer,
} from './styled'
const TournamentLanding = () => {
const {
buttonColor,
buttonLexic,
description,
gallery,
logo,
logoInsports,
onButtonClick,
onSliderSwitchClick,
period,
redirectToHomePage,
sliderItemId,
teams,
title,
} = useTournamentLanding()
const currentYear = format(new Date(), 'Y')
return (
<Wrapper>
<HeaderWrapper>
{isMobileDevice && <TournamentLogo src={logo} />}
<InsportsLogo onClick={redirectToHomePage} />
</HeaderWrapper>
<MainInfoContainer>
<BlockWrapper>
{
gallery
? (
<SliderWrapper>
<SliderContainer>
{map(gallery, (img, itemId) => (
<SliderImg
isAnimatedImg={itemId === sliderItemId}
key={img.id}
src={img.url}
/>
))}
</SliderContainer>
<SliderSwitch>
{map(gallery, (img, itemId) => (
<SliderSwitchItem
onClick={() => onSliderSwitchClick(itemId)}
slideOpacity={itemId === sliderItemId}
key={img.id}
/>
))}
</SliderSwitch>
</SliderWrapper>
)
: (
<MainLogoWrapper>
<LogoBackground />
<MainLogoImg src={logo} />
</MainLogoWrapper>
)
}
<TournamentInfoContainer>
<TournamentInfo>
<DateInfo t={period} />
<TournamentTitle t={title} />
<TournamentDescription t={description} />
<TournamentButton
buttonColor={buttonColor}
onClick={onButtonClick}
>
<T9n t={buttonLexic} />
</TournamentButton>
</TournamentInfo>
<TournamentMedia>
{gallery && <TournamentLogo src={logo} />}
{teams && (
<TeamsLogo>
{map(teams, (item) => (
<TeamLogoImg
key={item.id}
src={item.logo}
/>
))}
</TeamsLogo>
)}
{logoInsports && <InsportsImg src='/images/insports-logo.svg' />}
</TournamentMedia>
</TournamentInfoContainer>
</BlockWrapper>
</MainInfoContainer>
<Footer>©inSports.tv {currentYear}</Footer>
</Wrapper>
)
}
export default TournamentLanding

@ -0,0 +1,362 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { ButtonSolid } from 'features/Common'
import { Logo } from 'features/Logo'
import { T9n } from 'features/T9n'
type ButtonProps = {
buttonColor?: string,
}
type SliderSwitchProps = {
slideOpacity: boolean,
}
type SliderImgProps = {
isAnimatedImg: boolean,
}
export const Wrapper = styled.div`
width: 100vw;
height: 100vh;
color: white;
display: flex;
flex-direction: column;
`
export const HeaderWrapper = styled.div`
background-color: rgba(19, 21, 27, 0.7);
padding: 20px 0;
padding-left: 15%;
${isMobileDevice
? css`
background-color: black;
height: 40px;
padding-left: 25px;
display: flex;
align-items: center;
`
: ''};
`
export const InsportsLogo = styled(Logo)`
height: 26px;
width: 80px;
cursor: pointer;
${isMobileDevice
? css`
width: 57px;
height: 18px;
`
: ''};
`
export const MainInfoContainer = styled.div`
height: 100%;
${isMobileDevice
? css`
overflow: scroll;
position: relative;
`
: ''};
`
export const BlockWrapper = styled.div`
height: 100%;
display: flex;
align-items: center;
${isMobileDevice
? css`
display: block;
@media screen and (orientation: landscape){
height: auto;
}
`
: ''};
`
export const SliderWrapper = styled.div`
position: relative;
width: 50%;
margin-right: 1%;
height: 100%;
${isMobileDevice
? css`
height: 55%;
width: 100%;
`
: ''};
`
export const MainLogoWrapper = styled.div`
display: flex;
justify-content: center;
width: 50%;
position: relative;
align-items: center;
${isMobileDevice
? css`
height: 55%;
width: 100%;
align-items: flex-start;
padding-top: 25px;
`
: ''};
`
export const MainLogoImg = styled.img`
width: 35%;
height: 35%;
position: relative;
${isMobileDevice
? css`
height: 160px;
width: 160px;
`
: ''};
`
export const LogoBackground = styled.div`
background: #294FC4;
width: 65%;
opacity: 0.7;
filter: blur(104.135px);
height: 50%;
position: absolute;
left: 50%;
transform: translateX(-50%);
${isMobileDevice
? css`
opacity: 0.8;
filter: blur(44.1346px);
width: 100%;
height: 35%;
top: 10%;
`
: ''};
`
export const SliderContainer = styled.div`
height: 100%;
position: relative;
overflow: hidden;
${isMobileDevice
? css`
:before {
content: '';
z-index: 10;
width: 100%;
height: 100%;
position: absolute;
background: linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 30%);
}
`
: ''};
`
export const SliderImg = styled.img<SliderImgProps>`
height: 100%;
width: 100%;
opacity: ${({ isAnimatedImg }) => (isAnimatedImg ? '1' : '0')};
position: absolute;
animation-name: ${({ isAnimatedImg }) => (isAnimatedImg ? 'sliderAnimation' : '')};
animation-iteration-count: 1;
animation-timing-function: ease-out;
animation-duration: 5s;
@keyframes sliderAnimation {
0% {
transform: scale(1);
}
100% {
transform: scale(1.1);
}
`
export const SliderSwitch = styled.div`
position: absolute;
display: flex;
justify-content: center;
left: 50%;
top: 90%;
width: 100%;
transform: translateX(-50%);
${isMobileDevice
? css`
display: none;
`
: ''};
`
export const SliderSwitchItem = styled.div<SliderSwitchProps>`
width: 10%;
height: 4px;
border-radius: 2px;
background-color: white;
opacity: ${({ slideOpacity }) => (slideOpacity ? '1' : '.3')};;
margin-right: 10px;
cursor: pointer;
transition: .7s;
`
export const TournamentInfoContainer = styled.div`
width: 50%;
display: flex;
flex-direction: column;
justify-content: space-between;
${isMobileDevice
? css`
position: absolute;
width: 100%;
top: 40%;
padding: 0 25px;
z-index: 100;
@media screen and (orientation: landscape){
padding-top: 0;
}
`
: css`
height: 100%;
`};
`
export const TournamentInfo = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
margin-top: ${(isMobileDevice ? 'none' : '90px')};
`
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: 13px;
font-weight: 600;
${isMobileDevice
? css`
font-size: 10px;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.7);
padding: 0.7em 2.5rem;`
: ''};
`
export const TournamentTitle = styled(T9n)`
font-weight: 700;
font-size: 50px;
margin-top: 50px;
${isMobileDevice
? css`
font-size: 24px;
margin: 15px 0 0;
`
: css`
width: 50%;
`};
`
export const TournamentButton = styled(ButtonSolid)<ButtonProps>`
width: 320px;
height: fit-content;
font-size: 24px;
font-weight: 600;
border-radius: 5px;
margin-bottom: 90px;
padding: 20px 0;
background-color: ${({ buttonColor }) => (buttonColor ? `${buttonColor}` : '#294FC3')};
${isMobileDevice
? css`
width: 100%;
border-radius: 10px;
font-size: 17px;
padding: 20px 50px;
margin-bottom: 0;
`
: ''};
`
export const TournamentDescription = styled(T9n)`
max-width: 400px;
margin: 50px 0;
font-size: 17px;
${isMobileDevice
? css`
font-size: 12px;
margin: 30px 0;
`
: ''};
`
export const TournamentMedia = styled.div`
display: flex;
align-items: center;
height: 130px;
margin-bottom: 25px;
${isMobileDevice
? css`
display: none;
`
: ''};
`
export const TournamentLogo = styled.img`
${isMobileDevice
? css`
margin-right: 10px;
height: 26px;
`
: css`
margin-right: 50px;
height: 100%;
`};
`
export const TeamsLogo = styled.div`
width: 400px;
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
`
export const InsportsImg = styled.img`
height: 200px;
width: 200px;
`
export const Footer = styled.div`
font-size: 14px;
background-color: black;
padding: 16px 0;
padding-left: 15%;
opacity: .5;
${isMobileDevice
? css`
display: none;
`
: ''};
`

@ -1,6 +1,10 @@
import { useEffect, useState } from 'react'
import { getUserSubscribes, Subscribe } from 'requests/getUserSubscribes'
import {
getUserSubscribes,
Subscribe,
Subscribes,
} from 'requests/getUserSubscribes'
import { cancelSubscribe } from 'requests/cancelSubscribe'
import { useLexicsStore } from 'features/LexicsStore'
@ -10,7 +14,7 @@ export const useUserSubscribes = () => {
const { addLexicsConfig } = useLexicsStore()
const [selectedSubscribe, setSelectedSubscribe] = useState<Subscribe>({} as Subscribe)
const [subscribes, setSubscribes] = useState<any>([])
const [subscribes, setSubscribes] = useState<Subscribes>([])
const [isCancelPopupOpen, setIsCancelPopupOpen] = useState(false)
const [changeCardPopupOpen, setChangeCardPopupOpen] = useState(false)
const [isSubCanceled, setSubCanceled] = useState(false)

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type RequestConfig = {
body?: any,
headers?: Headers,

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { RefObject } from 'react'
import { useEffect, useRef } from 'react'

@ -1,29 +0,0 @@
import { useState } from 'react'
export const useLocalStorage = (keyName: string, defaultValue?: any) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const value = localStorage.getItem(keyName)
if (value) {
return JSON.parse(value)
}
localStorage.setItem(keyName, JSON.stringify(defaultValue))
return defaultValue
} catch (err) {
return defaultValue
}
})
const setValue = (newValue: any) => {
try {
localStorage.setItem(keyName, JSON.stringify(newValue))
} catch (err) {
/* eslint-disable-next-line */
console.log(err)
}
setStoredValue(newValue)
}
return [storedValue, setValue]
}

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Dispatch } from 'react'
import { useCallback, useState } from 'react'

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useCallback } from 'react'
/**

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useEffect } from 'react'
import { queryParamStorage } from 'features/QueryParamsStorage'

@ -123,7 +123,7 @@ export const useHighlightsForm = () => {
const { playerHighlight } = useUserFavoritesStore()
const [sports, setSports] = useState<Array<SportTypeName>>([])
const [sounds, setSounds] = useState<any>([])
const [sounds, setSounds] = useState<Array<Sound>>([])
const [isFetchingTeams, setIsFetchingTeams] = useState(false)
const [teams, setTeams] = useState<Array<TeamType>>([])
const [playersData, setPlayersData] = useState<Array<PlayerType>>([])
@ -301,6 +301,8 @@ export const useHighlightsForm = () => {
useEffect(() => {
if (playerHighlight?.sportType && playerHighlight?.profile) {
// TODO: исправить any
// eslint-disable-next-line
setFormState((state: any) => ({
...state,
selectedPlayer: {

@ -0,0 +1,21 @@
import { API_ROOT, SportTypes } from 'config'
import { callApi } from 'helpers'
type Args = {
matchId: number,
sportType: SportTypes,
}
export const getLandingStatus = async ({
matchId,
sportType,
}: Args): Promise<{landing_id: number | null}> => {
const config = {
method: 'GET',
}
return callApi({
config,
url: `${API_ROOT}/v1/landings/${sportType}/${matchId}/status`,
})
}

@ -0,0 +1,28 @@
import { callApi } from 'helpers'
import { API_ROOT } from 'config'
type ScoreTeam = {
id: number,
score: number | null,
}
export type LiveScore = {
match_id: number,
sport_id: number,
team1:ScoreTeam,
team2: ScoreTeam,
}
export const getLiveScores = (): Promise<Array<LiveScore>> => {
const url = `${API_ROOT}/v1/matches/live/scores`
const config = {
method: 'GET',
}
return callApi({
config,
url,
})
}

@ -19,6 +19,13 @@ export type Team = {
score: number,
}
export type MatchTournament = {
id: number,
name_eng: string,
name_rus: string,
sportType: SportTypes,
}
export type VideoBound = {
e: string,
h: string,
@ -42,12 +49,7 @@ export type MatchInfo = {
sub: boolean,
team1: Team,
team2: Team,
tournament: {
id: number,
name_eng: string,
name_rus: string,
sportType: SportTypes,
},
tournament: MatchTournament,
video_bounds?: VideoBounds,
youtube_link?: string,
} | null

@ -0,0 +1,33 @@
import { callApi } from 'helpers'
import { API_ROOT } from 'config'
import type { Team, MatchTournament } from 'requests/getMatchInfo'
type Params = {
profileId: number,
sportType: number,
}
type Response = {
match_date: string,
match_date_utc: string,
match_id: number,
sport_id: number,
team1: Team,
team2: Team,
tournament: MatchTournament,
}
export const getMatchScore = ({ profileId, sportType }: Params): Promise<Response> => {
const url = `${API_ROOT}/v1/matches/${sportType}/${profileId}/scores`
const config = {
method: 'GET',
}
return callApi({
config,
url,
})
}

@ -3,6 +3,8 @@ import { API_ROOT } from 'config'
type ResponseSound = {
asset: string,
id: number,
name: string,
}
export const getSound = async (id: number | string): Promise<ResponseSound> => {

@ -3,6 +3,7 @@ import { API_ROOT } from 'config'
type ResponseSound = {
asset: string,
id: number,
name: string,
}

@ -0,0 +1,60 @@
import { API_ROOT } from 'config'
import { callApi } from 'helpers'
type Tournaments = {
season: string,
season_id: number,
sport_eng: string,
sport_id: number,
sport_rus: string,
tournament_eng: string,
tournament_id: number,
tournament_rus: string,
}
type Teams = {
id: number,
logo: string,
name_eng: string,
name_rus: string,
sport_id: number,
}
type Gallery = {
id: string,
url: string,
}
export type TournamentLanding = {
button_color?: string,
date_from: string,
date_to: string,
id: number,
lexic_button?: number,
lexic_description?: number,
lexic_period?: number,
lexic_title?: number,
logo_insports: boolean,
logo_team: boolean,
media: {
gallery: Array<Gallery>,
logo: string,
},
name: string,
teams: Array<Teams>,
tournaments: Array<Tournaments>,
url_button?: string,
}
export const getTournamentLanding = async (
landingName: number | string,
): Promise<TournamentLanding> => {
const config = {
method: 'GET',
}
return callApi({
config,
url: `${API_ROOT}/v1/landings/${landingName}`,
})
}

@ -35,7 +35,7 @@ type Team = {
name_ru: string,
}
type Subscribes = Array<Subscribe>
export type Subscribes = Array<Subscribe>
export const getUserSubscribes = (
_p_email: string,

@ -8,6 +8,7 @@ export * from './getUserSportFavs'
export * from './modifyUserSportFavs'
export * from './getSportTournaments'
export * from './getTournamentInfo'
export * from './getTournamentLanding'
export * from './getTeamInfo'
export * from './getUserInfo'
export * from './getMatchInfo'
@ -27,3 +28,5 @@ export * from './buySubscription'
export * from './saveMatchStats'
export * from './getGeoInfo'
export * from './getTokenVirtualUser'
export * from './getMatchScore'
export * from './getLiveScores'

@ -10,7 +10,7 @@
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
/* eslint-disable no-console */
const isLocalhost = Boolean(
export const isLocalhost = Boolean(
window.location.hostname === 'localhost'
// [::1] is the IPv6 localhost address.
|| window.location.hostname === '[::1]'

Loading…
Cancel
Save