add ads to main page

features(#564): add ads to main page
Andrei Dekterev 3 years ago committed by andreidekterev
parent 2b3427800c
commit 5172fdf560
  1. 1
      Makefile
  2. 35261
      package-lock.json
  3. 1
      package.json
  4. 84
      src/components/Ads/components/AdComponent/hooks.tsx
  5. 78
      src/components/Ads/components/AdComponent/index.tsx
  6. 63
      src/components/Ads/components/AdComponent/styled.tsx
  7. 69
      src/components/Ads/components/MobileAd/index.tsx
  8. 35
      src/components/Ads/components/MobileAd/styled.tsx
  9. 16
      src/components/Ads/helpers/calcMaxDurationAds.tsx
  10. 1
      src/components/Ads/helpers/index.tsx
  11. 2
      src/components/Ads/helpers/isVideo.tsx
  12. 44
      src/components/Ads/hooks.tsx
  13. 30
      src/components/Ads/index.tsx
  14. 15
      src/components/Ads/styled.tsx
  15. 37
      src/components/Ads/types.tsx
  16. 31
      src/components/PictureInPicture/PiP.tsx
  17. 2
      src/config/clients/lff.tsx
  18. 1
      src/config/index.tsx
  19. 1
      src/config/localStorageKeys.tsx
  20. 1
      src/config/queries.tsx
  21. 7
      src/config/routes.tsx
  22. 2
      src/config/userAgent.tsx
  23. 3
      src/features/AuthServiceApp/components/ConfirmPopup/index.tsx
  24. 2
      src/features/AuthStore/hooks/useAuth.tsx
  25. 12
      src/features/GlobalStyles/index.tsx
  26. 17
      src/features/HeaderFilters/components/DateFilter/hooks/index.tsx
  27. 29
      src/features/HomePage/hooks.tsx
  28. 16
      src/features/HomePage/index.tsx
  29. 14
      src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx
  30. 14
      src/features/MatchCard/CardFrontside/MatchCardMobile/styled.tsx
  31. 14
      src/features/MatchCard/CardFrontside/index.tsx
  32. 30
      src/features/MatchCard/Score/index.tsx
  33. 31
      src/features/MatchCard/Score/styled.tsx
  34. 2
      src/features/MatchCard/styled.tsx
  35. 10
      src/features/MatchPage/components/FavouriteTeam/hooks.tsx
  36. 26
      src/features/MatchPage/components/MatchDescription/index.tsx
  37. 5
      src/features/MatchPage/components/MatchDescription/styled.tsx
  38. 2
      src/features/MatchPage/store/hooks/index.tsx
  39. 8
      src/features/MatchPage/store/hooks/usePlayersStats.tsx
  40. 24
      src/features/MatchPage/store/hooks/useStatsTab.tsx
  41. 6
      src/features/MatchPage/store/hooks/useTeamsStats.tsx
  42. 68
      src/features/MatchSidePlaylists/components/TeamsStatsTable/Cell.tsx
  43. 26
      src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx
  44. 6
      src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx
  45. 6
      src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx
  46. 1
      src/features/Matches/components/MatchesList/index.tsx
  47. 19
      src/features/MatchesGrid/index.tsx
  48. 8
      src/features/MatchesGrid/styled.tsx
  49. 2
      src/features/StreamPlayer/components/Controls/Components/ControlsWeb/index.tsx
  50. 16
      src/features/StreamPlayer/hooks/index.tsx
  51. 39
      src/features/TournamentList/components/TournamentMobile/index.tsx
  52. 4
      src/features/TournamentList/components/TournamentMobile/styled.tsx
  53. 1
      src/features/TournamentList/index.tsx
  54. 4
      src/features/UserAccount/components/PagePersonalInfo/hooks/index.tsx
  55. 28
      src/features/UserAccount/components/PersonalInfoForm/index.tsx
  56. 4
      src/hooks/usePageLogger.tsx
  57. 9
      src/index.tsx
  58. 7
      src/pages/HighlightsPage/storeHighlightsAtoms.tsx
  59. 52
      src/requests/getAds/getAds.tsx
  60. 2
      src/requests/getAds/index.tsx
  61. 29
      src/requests/getAds/updateAdsView.tsx
  62. 2
      src/requests/getFavouriteTeam.tsx
  63. 1
      src/requests/getLiveScores.tsx
  64. 1
      src/requests/getMatchInfo.tsx
  65. 2
      src/requests/getMatches/getHomeMatches.tsx
  66. 2
      src/requests/getMatches/getPlayerMatches.tsx
  67. 82
      src/requests/getMatches/getPreviews.tsx
  68. 2
      src/requests/getMatches/getTeamMatches.tsx
  69. 2
      src/requests/getMatches/getTournamentMatches.tsx
  70. 1
      src/requests/getMatches/types.tsx
  71. 4
      src/requests/index.tsx
  72. 31
      src/utilits/mirage/Mirage.tsx
  73. 300
      src/utilits/mirage/fixtures/getAds.tsx
  74. 2
      src/utilits/mirage/fixtures/index.tsx
  75. 8
      src/utilits/mirage/models/index.tsx
  76. 44
      src/utilits/mirage/server.ts

@ -204,4 +204,3 @@ test:
generate-ssl-keys:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365

35261
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -38,6 +38,7 @@
"react": "^17.0.2",
"react-datepicker": "^3.1.3",
"react-dom": "^17.0.2",
"react-ga": "^3.3.1",
"react-query": "^3.39.3",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",

@ -0,0 +1,84 @@
import { useEffect, useState } from 'react'
import ReactGA from 'react-ga'
import { updateAdsView } from 'requests'
import { useToggle } from 'hooks'
import { getLocalStorageItem } from 'helpers'
import {
device,
COUNTRY,
} from 'config'
import type { AdComponentType } from './index'
import { checkVideo } from '../../helpers'
import {
adsViews,
EventGA,
ViewsType,
} from '../../types'
const countryCode = getLocalStorageItem(COUNTRY)
export const useAd = ({ ad, close: hideElement }: AdComponentType) => {
const [isOpenAd, setIsOpenAd] = useState(true)
const views = getLocalStorageItem(adsViews) as ViewsType
const {
duration,
frequency,
id,
media,
name,
time_close,
} = ad
const isVideo = checkVideo(media.url)
const {
isOpen: isOpenCloseBtn,
open: showCloseBtn,
} = useToggle()
const handleClose = async () => {
setIsOpenAd(false)
await updateAdsView({ adv_id: id })
sendBannerClickEvent(EventGA.CLOSE)
}
const sendBannerClickEvent = (event: EventGA) => {
ReactGA.event({
action: event,
category: 'Advertisement',
label: `${name}_${countryCode ?? ''}_${device}`,
value: id,
})
}
const isNeedToShow = Number(views?.HOME) % frequency === 0
useEffect(() => {
!isNeedToShow && setIsOpenAd(false)
const timeoutCloseAd = setTimeout(handleClose, duration * 1000)
const timeoutCloseBtn = time_close && setTimeout(showCloseBtn, time_close * 1000)
return () => {
time_close && clearTimeout(timeoutCloseBtn)
clearTimeout(timeoutCloseAd)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
}
}

@ -0,0 +1,78 @@
import { memo, MouseEvent } from 'react'
import type { AdType } from 'requests'
import {
AdImg,
AdVideo,
AdWrapper,
} from './styled'
import { useAd } from './hooks'
import { EventGA } from '../../types'
import { CloseButton } from '../../../../features/PopupComponents'
export type AdComponentType = {
ad: AdType,
close?: () => void,
}
export const AdComponent = memo(({ ad, close }: AdComponentType) => {
const {
link,
media,
position,
} = ad
const {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
} = useAd({ ad, close })
return (
position && isOpenAd && isNeedToShow
? (
<AdWrapper
position={position.id}
isOpenAd={isOpenAd}
>
{isOpenCloseBtn && (
<CloseButton
onClick={(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
handleClose()
}}
size={12}
className='closeBtn'
/>
)}
<a
href={link}
target='_blank'
rel='noreferrer'
onClick={() => link && sendBannerClickEvent(EventGA.CLICK)}
>
{isVideo
? (
<AdVideo
muted={isVideo}
autoPlay={isVideo}
loop={isVideo}
src={media.url}
position={position.id}
/>
)
: (
<AdImg
src={media.url}
position={position.id}
/>
)}
</a>
</AdWrapper>
) : null
)
})

@ -0,0 +1,63 @@
import styled from 'styled-components'
import { VIEW_ADS } from '../../types'
type Props = {
position: number,
}
const chooseStyle = (type: number) => {
switch (true) {
case VIEW_ADS[type] === 'COLUMN':
return 'grid-row: 1 / 3; img {max-height: none;}'
case VIEW_ADS[type] === 'ROW':
return 'grid-column: 1 / 3'
case VIEW_ADS[type] === 'SQUARE':
return 'grid-row: 1 / 3; grid-column: 1 / 3; img {max-height: none;}'
case VIEW_ADS[type] === 'SECOND_COLUMN':
return 'grid-column: 2 / 3; grid-row: 1 / 1'
case VIEW_ADS[type] === 'SECOND_ROW':
return 'grid-column: 1 / 2; grid-row: 2 / 3;'
default:
return ''
}
}
const header = [7, 8, 9]
export const AdImg = styled.img<Props>`
object-fit: cover;
width: 100%;
min-height: ${({ position }) => (!header.includes(position) && '100%')};
max-height: ${({ position }) => (header.includes(position) ? '13rem' : '100%')};
cursor: pointer;
border-radius: 3px;
`
export const AdVideo = styled.video<Props>`
object-fit: contain;
width: 100%;
cursor: pointer;
max-height: ${({ position }) => (header.includes(position) ? '283px' : '100%')};
background-color: black;
border-radius: 3px;
`
export const AdWrapper = styled.div<Props & {isOpenAd: boolean}>`
position: relative;
width: 100%;
height: 100%;
${({ position }) => chooseStyle(position)};
display: ${({ isOpenAd }) => (isOpenAd ? '' : 'none')};
.closeBtn {
position: absolute;
right: 0;
top: 0;
background: none;
border-radius: 0;
z-index: 2;
cursor: pointer;
}
`

@ -0,0 +1,69 @@
import { MouseEvent } from 'react'
import type { AdType } from 'requests'
import { CloseButton } from 'features/PopupComponents'
import {
Img,
MobileAdWrapper,
Video,
} from './styled'
import { useAd } from '../AdComponent/hooks'
import { EventGA } from '../../types'
type MobileAdTypes = {
ad: AdType,
className?: string,
close?: () => void,
}
export const MobileAd = ({
ad,
className,
close,
}: MobileAdTypes) => {
const {
link,
media,
position,
} = ad
const {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
} = useAd({ ad, close })
return (
position && isOpenAd && isNeedToShow ? (
<MobileAdWrapper
className={className}
onClick={() => {
if (link) {
sendBannerClickEvent(EventGA.CLICK)
window.open(link, '_blank')
}
}}
>
{isOpenCloseBtn
&& (
<CloseButton
onClick={(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
handleClose()
}}
size={8}
className='mobileCloseBtn'
/>
)}
{isVideo
? <Video position={position.id} src={media.url} />
: (
<Img position={position.id} src={media.url} />
)}
</MobileAdWrapper>
) : null
)
}

@ -0,0 +1,35 @@
import styled from 'styled-components'
type Props = {
position: number,
}
export const MobileAdWrapper = styled.div`
position: relative;
width: 100%;
.mobileCloseBtn{
position: absolute;
right: -10px;
background: none;
border-radius: 0;
color: rgba(0, 0, 0, 0.6);
top: 10px;
transform: translate(-50%, -50%);
z-index: 2;
}
`
export const Img = styled.img<Props>`
object-fit: cover;
height: ${({ position }) => (position === 10 ? '50px' : '75px')};
border-radius: 2px;
width: 100%;
`
export const Video = styled.video<Props>`
max-height: 100%;
object-fit: cover;
min-width: 100%;
height: ${({ position }) => (position === 10 ? '50px' : '75px')};
border-radius: 2px;
`

@ -0,0 +1,16 @@
import { AdResponse, AdsListType } from 'requests'
export const calcMaxAdDurationAds = (advertisements: AdResponse) => {
const allAds = Object.values(advertisements)
const combineAds = allAds.reduce((result, currentAd) => {
result.push(...currentAd)
return result
}, [] as AdsListType)
const maxDuration = combineAds
.reduce((result, { duration }) => Math.max(result, duration), 0)
return maxDuration
}

@ -0,0 +1 @@
export * from './isVideo'

@ -0,0 +1,2 @@
const regexp = /^https?:\/\/\S+(?:mp4)$/
export const checkVideo = (url: string) => regexp.test(url)

@ -0,0 +1,44 @@
import { useQuery } from 'react-query'
import { useRecoilState } from 'recoil'
import { isMobileDevice, querieKeys } from 'config'
import { getAds } from 'requests'
import { useLang } from 'features/LexicsStore/hooks/useLang'
import { useMemo } from 'react'
import {
DeviceType,
PageType,
} from './types'
import { calcMaxAdDurationAds } from './helpers/calcMaxDurationAds'
import { useAuthStore } from '../../features/AuthStore'
import { adsStore } from '../../pages/HighlightsPage/storeHighlightsAtoms'
export const useAds = () => {
const [ads, setAds] = useRecoilState(adsStore)
const { lang } = useLang()
const { user } = useAuthStore()
useQuery({
queryFn: async () => {
if (user) {
const adsList = await getAds({
client_type: isMobileDevice ? DeviceType.MOBILE : DeviceType.WEB,
language: lang,
type_id: PageType.HOME,
})
adsList && setAds(adsList)
return adsList
}
return {}
},
queryKey: querieKeys.ads,
staleTime: useMemo(() => Math.max(calcMaxAdDurationAds(ads), 60 * 1000), [ads]),
})
return {
ads,
}
}

@ -0,0 +1,30 @@
import type { AdType } from 'requests'
import {
HeaderWrapAd,
} from './styled'
import { AdComponent } from './components/AdComponent'
import { AdsPropsType } from './types'
import { MobileAd } from './components/MobileAd'
import { isMobileDevice } from '../../config'
export const HeaderAds = ({ ads }: AdsPropsType) => (
ads?.length ? (
<HeaderWrapAd column={ads?.length}>
{ads.map((ad: AdType) => (
!isMobileDevice ? (
<AdComponent
ad={ad}
key={ad.id}
/>
) : (
<MobileAd
ad={ad}
key={ad.id}
/>
)
))}
</HeaderWrapAd>
) : null
)

@ -0,0 +1,15 @@
import styled, { css } from 'styled-components'
import { isMobileDevice } from '../../config'
export const HeaderWrapAd = styled.div<{column: number}>`
width: 100%;
margin-bottom: 0.7rem;
display: grid;
grid-column-gap: 0.9rem;
grid-template-columns: ${({ column }) => (column > 1 ? `repeat(${column},${16.3 * 6 / column}%)` : 'repeat(1, 98.7%)')};
${isMobileDevice && css`
padding: 0px 0.71rem;
grid-template-columns: none;
`}}
`

@ -0,0 +1,37 @@
import { AdsListType } from '../../requests'
export enum PageType {
HOME = 1,
MATCH = 2,
}
export enum DeviceType {
MOBILE = 'mobile',
WEB = 'web'
}
export type ViewsType = Partial<Record<keyof typeof PageType, number>>
export enum EventGA {
CLICK = 'banner_click',
CLOSE = 'banner_close'
}
export enum VIEW_ADS {
ROW = 4,
COLUMN = 5,
SQUARE = 6,
SECOND_COLUMN = 2,
SECOND_ROW= 3,
HEADER_1 = 7,
HEADER_2 = 8,
HEADER_LONG = 9,
MOBILE_IN_COLLAPSE_HEADER = 12,
MOBILE_IN_COLLAPSE_FOOTER = 25
}
export const HEADER_MOBILE_ADS = [10, 11]
export type AdsPropsType = Record<'ads', AdsListType | undefined>
export const adsViews = 'adsViews'

@ -1,5 +1,4 @@
import {
memo,
RefObject,
useEffect,
} from 'react'
@ -17,11 +16,10 @@ const PipWrapper = styled.div`
`
type PipProps = {
isPlaying: boolean,
videoRef: RefObject<HTMLVideoElement>,
}
export const PiP = memo(({ isPlaying, videoRef }: PipProps) => {
export const PiP = ({ videoRef }: PipProps) => {
const { user } = useAuthStore()
const togglePip = async () => {
try {
@ -38,23 +36,38 @@ export const PiP = memo(({ isPlaying, videoRef }: PipProps) => {
}
useEffect(() => {
window.addEventListener('visibilitychange', async () => {
const onVisibilityChange = async () => {
if (
document.hidden === true
&& document.pictureInPictureEnabled
&& videoRef.current !== document.pictureInPictureElement
&& videoRef.current?.hidden === false
&& isPlaying
&& !videoRef.current?.paused
&& document.visibilityState !== 'visible'
&& user
) {
await videoRef.current?.requestPictureInPicture()
try {
await videoRef.current?.requestPictureInPicture()
} catch (error) { /* empty */ }
}
if (
document.visibilityState === 'visible'
&& document.pictureInPictureEnabled
&& videoRef.current === document.pictureInPictureElement
) {
try {
await document.exitPictureInPicture()
} catch (error) { /* empty */ }
}
})
}, [videoRef, isPlaying, user])
}
window.addEventListener('visibilitychange', onVisibilityChange)
return () => window.removeEventListener('visibilitychange', onVisibilityChange)
}, [videoRef, user])
return (
<PipWrapper id='match_video_pic_in_pic'>
<Icon refIcon='PiP' onClick={togglePip} />
</PipWrapper>
)
})
}

@ -19,7 +19,7 @@ export const lff: ClientConfig = {
disabledHighlights: true,
disabledPreferences: true,
name: ClientNames.Lff,
privacyLink: '/clients/instat/terms-and-conditions.html',
privacyLink: '/privacy-policy-and-statement',
showSearch: true,
styles: {
background: 'background-image: url(/images/Checker.png);',

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

@ -0,0 +1 @@
export const COUNTRY = 'COUNTRY'

@ -1,4 +1,5 @@
export const querieKeys = {
ads: 'ads',
liveMatchScores: 'liveMatchScores',
matchScore: 'matchScore',
sportsList: 'sportsList',

@ -29,6 +29,12 @@ const STATS_APIS = {
staging: 'https://statistic-stage.insports.tv',
}
const ADS_APIS = {
preproduction: 'https://ads.insports.tv/v1/ads',
production: 'https://ads.insports.tv/v1/ads',
staging: 'https://ads-test.insports.tv/v1/ads',
}
const env = isProduction ? ENV : readSelectedApi() ?? ENV
export const VIEWS_API = VIEWS_APIS[env]
@ -37,3 +43,4 @@ export const API_ROOT = APIS[env].api
export const DATA_URL = `${API_ROOT}/data`
export const URL_AWS = 'https://cf-aws.insports.tv'
export const STATS_API_URL = STATS_APIS[env]
export const ADS_API_URL = ADS_APIS[env]

@ -3,3 +3,5 @@ export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
export const isAndroid = /Android/.test(navigator.userAgent)
export const isMobileDevice = /iPhone|Android/.test(navigator.userAgent)
export const device = navigator.userAgent

@ -1,7 +1,6 @@
import { T9n } from 'features/T9n'
import { client } from 'features/AuthServiceApp/config/clients'
import { client } from 'config/clients'
import { AUTH_SERVICE } from 'config/routes'
import {

@ -105,7 +105,7 @@ export const useAuth = () => {
token && await fetchUserInfo()
return Promise.resolve()
}
return Promise.reject()
return Promise.resolve()
}
storeUser(loadedUser)

@ -1,6 +1,4 @@
import { createGlobalStyle, css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { createGlobalStyle } from 'styled-components/macro'
export const GlobalStyles = createGlobalStyle`
*, *:before, *:after {
@ -9,13 +7,7 @@ export const GlobalStyles = createGlobalStyle`
html {
font-size: calc(2px + 1vw);
overflow-y: hidden;
${isMobileDevice
? css`
overflow-y: auto;
`
: ''};
overflow-y: auto;
}
body {

@ -6,16 +6,18 @@ import {
import addDays from 'date-fns/addDays'
import { useToggle } from 'hooks'
import { useLocalStore, useToggle } from 'hooks'
import { useLexicsStore } from 'features/LexicsStore'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import isObject from 'lodash/isObject'
import {
getDisplayDate,
getWeekName,
getWeeks,
} from '../helpers'
import { ViewsType } from '../../../../../components/Ads/types'
export const useDateFilter = () => {
const {
@ -46,6 +48,15 @@ export const useDateFilter = () => {
const parseFilters = filters && JSON.parse(filters)
const lastDate = parseFilters?.selectedDate
const weekName = getWeekName(selectedDate, 'en')
const validator = (value: unknown) => Boolean(value) && isObject(value)
const [adsViews, setAdsViews] = useLocalStore<ViewsType>({
clearOnUnmount: true,
defaultValue: { HOME: 0 },
key: 'adsViews',
validator,
})
useEffect(() => {
if (lastDate === selectedDate.getDate()
&& parseFilters
@ -57,6 +68,10 @@ export const useDateFilter = () => {
setIsShowTournament(true)
setSelectedFilters([])
setSelectedLeague(['all_competitions'])
setAdsViews({
...adsViews,
HOME: (adsViews.HOME ?? 0) + 1,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDate])

@ -14,10 +14,19 @@ import { ClientNames } from 'config/clients/types'
import { useAuthStore } from 'features/AuthStore'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { getHomeMatches } from 'requests/getMatches'
import { getAgreements, setAgreements } from 'requests/getAgreements'
import {
getHomeMatches,
getAgreements,
setAgreements,
getCountryCode,
} from 'requests'
import { setLocalStorageItem } from 'helpers'
import { COUNTRY } from 'config'
import { isSportFilterShownAtom } from './Atoms/HomePageAtoms'
import { useAds } from '../../components/Ads/hooks'
/**
* возвращает смещение в минутах относительно UTC
@ -34,18 +43,27 @@ const getTimezoneOffset = (date: Date) => {
const getDate = (date: Date) => format(date, 'yyyy-MM-dd')
export const useHomePage = () => {
const { user, userInfo } = useAuthStore()
const { userInfo } = useAuthStore()
const { selectedDate } = useHeaderFiltersStore()
const [isOpenDownload, setIsOpenDownload] = useState(false)
const [isShowConfirmPopup, setIsShowConfirmPopup] = useState(false)
const setIsSportFilterShown = useSetRecoilState(isSportFilterShownAtom)
const { ads } = useAds()
const handleCloseConfirmPopup = useCallback(async () => {
await setAgreements(`${userInfo?.email}` || '')
setIsShowConfirmPopup(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsShowConfirmPopup, userInfo])
const countryCode = async () => {
const { country_code } = await getCountryCode()
country_code && setLocalStorageItem(COUNTRY, country_code)
return country_code
}
useEffect(() => {
if (userInfo?.email) {
(async () => {
@ -65,6 +83,8 @@ export const useHomePage = () => {
) {
setIsOpenDownload(true)
}
countryCode()
}, [])
const fetchMatches = useCallback(
@ -75,7 +95,7 @@ export const useHomePage = () => {
timezoneOffset: getTimezoneOffset(selectedDate),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[selectedDate, user],
[selectedDate, userInfo?.email],
)
useEffect(() => {
@ -85,6 +105,7 @@ export const useHomePage = () => {
}, [setIsSportFilterShown])
return {
ads,
fetchMatches,
handleCloseConfirmPopup,
isOpenDownload,

@ -20,10 +20,13 @@ import { useHomePage } from './hooks'
import { Header } from './components/Header'
import { HeaderMobile } from '../HeaderMobile'
import { HeaderFilters } from './components/HeaderFilters'
import { HeaderAds } from '../../components/Ads'
import { HEADER_MOBILE_ADS } from '../../components/Ads/types'
const Home = () => {
usePageLogger(PAGES.home)
const {
ads,
fetchMatches,
handleCloseConfirmPopup,
isOpenDownload,
@ -46,8 +49,17 @@ const Home = () => {
<Main>
<UserFavorites />
<Content>
{isMobileDevice ? null : <HeaderFilters />}
{/* {userInfo?.email && <Matches fetch={fetchMatches} />} */}
{!isMobileDevice && <HeaderFilters />}
{userInfo?.email
&& ads
&& (
<HeaderAds ads={
isMobileDevice
? ads?.mobile?.filter(({ position }) => HEADER_MOBILE_ADS.includes(position.id))
: ads?.header
}
/>
)}
<Matches fetch={fetchMatches} />
<ConfirmPopup
isModalOpen={isShowConfirmPopup}

@ -35,7 +35,6 @@ import {
MatchTimeInfo,
Preview,
PreviewWrapper,
Score,
Team,
TeamName,
Teams,
@ -50,6 +49,7 @@ import {
MobTime,
} from './styled'
import { useCardPreview } from '../hooks'
import { Score } from '../../Score'
type Props = {
isNeedFormatTimeChanged: boolean,
@ -159,14 +159,22 @@ export const CardFrontsideMobile = ({
<TeamName nameObj={team1} />
{team1InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{score?.team1.score ?? team1.score}</Score>}
<Score
showScore={showScore}
score={score?.team1.score ?? team1.score}
penaltyScore={score?.team1.penalty_score ?? team1.penalty_score}
/>
</Team>
<Team>
<NameSignWrapper>
<TeamName nameObj={team2} />
{team2InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{score?.team2.score ?? team2.score}</Score>}
<Score
showScore={showScore}
score={score?.team2.score ?? team2.score}
penaltyScore={score?.team2.penalty_score ?? team2.penalty_score}
/>
</Team>
</Teams>
<SecondaryInfo>

@ -199,7 +199,7 @@ export const Info = styled.div`
left: 45%; */
width: 60%;
height: 100%;
padding: 9px 9px 9px 19px;
padding: 9px 11px 9px 19px;
`
: ''};
`
@ -304,18 +304,6 @@ export const TeamName = styled(Name)`
${nameStyles}
`
export const Score = styled.div`
${isMobileDevice
? css`
display: flex;
justify-content: center;
width: 15px;
`
: ''};
`
export const TeamLogos = styled.div`
display: flex;
align-items: center;

@ -19,6 +19,7 @@ import { useUserFavoritesStore } from 'features/UserFavorites/store'
import { TournamentSubtitle } from 'features/TournamentSubtitle'
import { NoAccessMessage } from '../NoAccessMessage'
import { Score } from '../Score'
import {
CardWrapperOuter,
CardWrapper,
@ -28,7 +29,6 @@ import {
MatchTimeInfo,
Preview,
PreviewWrapper,
Score,
Team,
TeamName,
Teams,
@ -206,7 +206,11 @@ export const CardFrontside = ({
<TeamName nameObj={team1} />
{team1InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{score?.team1.score ?? team1.score}</Score>}
<Score
showScore={showScore}
score={score?.team1.score ?? team1.score}
penaltyScore={score?.team1.penalty_score ?? team1.penalty_score}
/>
</Team>
<Team isMatchPage={isMatchPage}>
<NameSignWrapper>
@ -224,7 +228,11 @@ export const CardFrontside = ({
<TeamName nameObj={team2} />
{team2InFavorites && <FavoriteSign />}
</NameSignWrapper>
{showScore && <Score>{score?.team2.score ?? team2.score}</Score>}
<Score
showScore={showScore}
score={score?.team2.score ?? team2.score}
penaltyScore={score?.team2.penalty_score ?? team2.penalty_score}
/>
</Team>
</Teams>
{!isMatchPage && (

@ -0,0 +1,30 @@
import {
MainScore,
ScoreWrapper,
PenaltyScore,
} from './styled'
type Props = {
penaltyScore?: number | null,
score?: number | null,
showScore: boolean,
}
export const Score = ({
penaltyScore,
score,
showScore,
}: Props) => {
if (!showScore) return null
return (
<ScoreWrapper>
<MainScore>
{score}
</MainScore>
<PenaltyScore>
{penaltyScore}
</PenaltyScore>
</ScoreWrapper>
)
}

@ -0,0 +1,31 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
export const MainScore = styled.div`
${isMobileDevice
? css`
display: flex;
justify-content: center;
`
: ''};
`
export const ScoreWrapper = styled.div`
display: flex;
line-height: 1;
`
export const PenaltyScore = styled.div`
font-weight: 400;
font-size: .55rem;
margin-left: 1px;
${isMobileDevice
? css`
font-size: 10px;
`
: ''};
`

@ -274,8 +274,6 @@ export const TeamName = styled(Name)`
${nameStyles}
`
export const Score = styled.div``
export const TeamLogos = styled.div<CardProps>`
display: ${({ isMatchPage }) => (isMatchPage ? 'none' : 'flex')};
align-items: center;

@ -38,21 +38,21 @@ export const useFavouriteTeam = () => {
useEffect(() => {
(async () => {
const { data: { data: teams1 }, status }: ResponseType = await getFavouriteTeam({
const { data: teams1, status }: ResponseType = await getFavouriteTeam({
country_id: 77,
season: 30,
sport_id: 1,
tournament_id: 131,
})
const { data: { data: teams2 } }: ResponseType = await getFavouriteTeam({
const { data: teams2 }: ResponseType = await getFavouriteTeam({
country_id: 77,
season: 30,
sport_id: 1,
tournament_id: 2032,
})
if (!status) {
setGroup1(sortTeam(teams1))
setGroup2(sortTeam(teams2))
if (!status && teams1?.data && teams2?.data) {
setGroup1(sortTeam(teams1?.data))
setGroup2(sortTeam(teams2?.data))
setIsOpen(true)
}
})()

@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { Fragment, useCallback } from 'react'
import { useQuery } from 'react-query'
@ -41,6 +41,7 @@ import {
Time,
Title,
Views,
PenaltyScore,
} from './styled'
export const MatchDescription = () => {
@ -93,6 +94,7 @@ export const MatchDescription = () => {
parseDate(date),
isChangedTimeFormat ? 'h:mm a' : 'HH:mm',
)
const isPenaltyScoreShow = queryScore?.team1.penalty_score || team1.penalty_score
return (
<Description isHidden={!profileCardShown}>
@ -111,8 +113,26 @@ export const MatchDescription = () => {
{
isScoreHidden || isNil(team1.score) || isNil(team2.score)
? '-'
: `${queryScore?.team1.score ?? team1.score} - ${queryScore?.team2.score ?? team2.score}`
: (
<Fragment>
{queryScore?.team1.score ?? team1.score}
{isPenaltyScoreShow && (
<PenaltyScore>
({queryScore?.team1.penalty_score ?? team1.penalty_score})
</PenaltyScore>
)}
{` - ${queryScore?.team2.score ?? team2.score}`}
{isPenaltyScoreShow && (
<PenaltyScore>
({queryScore?.team2.penalty_score ?? team2.penalty_score})
</PenaltyScore>
)}
</Fragment>
)
}
</Score>
<StyledLink

@ -40,6 +40,11 @@ export const Score = styled.span`
: ''};
`
export const PenaltyScore = styled.span`
font-weight: 400;
opacity: .8;
`
export const StyledLink = styled(ProfileLink)`
display: flex;
align-items: center;

@ -296,7 +296,6 @@ export const useMatchPage = () => {
} = useTeamsStats({
matchProfile,
playingProgress,
selectedPlaylist,
selectedStatsTable,
selectedTab,
setIsTeamsStatsFetching,
@ -311,7 +310,6 @@ export const useMatchPage = () => {
} = usePlayersStats({
matchProfile,
playingProgress,
selectedPlaylist,
selectedStatsTable,
selectedTab,
setIsPlayersStatsFetching,

@ -36,9 +36,7 @@ import { getLocalStorageItem } from 'helpers/getLocalStorage'
import { useObjectState, usePageParams } from 'hooks'
import type{ PlaylistOption } from 'features/MatchPage/types'
import { StatsType, Tabs as StatsTabs } 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 { Tabs } from 'features/MatchSidePlaylists/config'
@ -52,7 +50,6 @@ const STATS_POLL_INTERVAL = 30000
type UsePlayersStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
selectedPlaylist?: PlaylistOption,
selectedStatsTable: StatsTabs,
selectedTab: Tabs,
setIsPlayersStatsFetching: Dispatch<SetStateAction<boolean>>,
@ -67,7 +64,6 @@ type PlayersData = {
export const usePlayersStats = ({
matchProfile,
playingProgress,
selectedPlaylist,
selectedStatsTable,
selectedTab,
setIsPlayersStatsFetching,
@ -147,8 +143,7 @@ export const usePlayersStats = ({
const isTeam1Selected = selectedStatsTable === StatsTabs.TEAM1
if (
selectedPlaylist?.id !== FULL_GAME_KEY
|| selectedTab !== Tabs.STATS
selectedTab !== Tabs.STATS
|| !includes([StatsTabs.TEAM1, StatsTabs.TEAM2], selectedStatsTable)
|| !matchProfile?.team1.id
|| !matchProfile?.team2.id
@ -195,7 +190,6 @@ export const usePlayersStats = ({
setIsPlayersStatsFetching(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, REQUEST_DELAY), [
selectedPlaylist?.id,
fetchPlayers,
fetchPlayersStats,
setPlayersStats,

@ -1,10 +1,8 @@
import { useState, useEffect } from 'react'
import map from 'lodash/map'
import isEqual from 'lodash/isEqual'
import type {
Episode,
Episodes,
Events,
MatchInfo,
@ -36,18 +34,6 @@ type PlayNextEpisodeArgs = {
order?: number,
}
const EPISODE_TIMESTAMP_OFFSET = 0.001
const addOffset = ({
e,
h,
s,
}: Episode) => ({
e: e + EPISODE_TIMESTAMP_OFFSET,
h,
s: s + EPISODE_TIMESTAMP_OFFSET,
})
export const useStatsTab = ({
disablePlayingEpisodes,
handlePlaylistClick,
@ -82,15 +68,7 @@ export const useStatsTab = ({
}
const getEpisodesToPlay = (episodes: Episodes) => map(episodes, (episode, i) => ({
episodes: [
/** При проигрывании нового эпизода с такими же e и s, как у текущего
воспроизведение начинается не с начала, чтобы пофиксить это добавляем
небольшой оффсет
*/
isEqual(episode, selectedPlaylist?.episodes[0])
? addOffset(episode)
: episode,
],
episodes: [episode],
id: i,
type: PlaylistTypes.EVENT,
})) as Array<EventPlaylistOption>

@ -25,9 +25,7 @@ import { usePageParams } from 'hooks'
import { getLocalStorageItem } from 'helpers/getLocalStorage'
import type { PlaylistOption } from 'features/MatchPage/types'
import { StatsType, Tabs as StatsTab } 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 { Tabs } from 'features/MatchSidePlaylists/config'
@ -40,7 +38,6 @@ const STATS_POLL_INTERVAL = 30000
type UseTeamsStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
selectedPlaylist?: PlaylistOption,
selectedStatsTable: StatsTab,
selectedTab: Tabs,
setIsTeamsStatsFetching: Dispatch<SetStateAction<boolean>>,
@ -54,7 +51,6 @@ type TeamsStats = {
export const useTeamsStats = ({
matchProfile,
playingProgress,
selectedPlaylist,
selectedStatsTable,
selectedTab,
setIsTeamsStatsFetching,
@ -101,7 +97,6 @@ export const useTeamsStats = ({
if (
!sportName
|| selectedPlaylist?.id !== FULL_GAME_KEY
|| !videoBounds
|| selectedTab !== Tabs.STATS
|| selectedStatsTable !== StatsTab.TEAMS
@ -129,7 +124,6 @@ export const useTeamsStats = ({
matchProfile?.video_bounds,
matchProfile?.live,
matchScore?.video_bounds,
selectedPlaylist?.id,
matchId,
setIsTeamsStatsFetching,
sportName,

@ -1,11 +1,16 @@
import { Fragment, useRef } from 'react'
import { createPortal } from 'react-dom'
import { useQueryClient } from 'react-query'
import { useTour } from '@reactour/tour'
import isNumber from 'lodash/isNumber'
import { KEYBOARD_KEYS, querieKeys } from 'config'
import {
isMobileDevice,
KEYBOARD_KEYS,
querieKeys,
} from 'config'
import type {
Param,
@ -14,7 +19,12 @@ import type {
} from 'requests'
import { getStatsEvents } from 'requests'
import { usePageParams, useEventListener } from 'hooks'
import {
usePageParams,
useEventListener,
useTooltip,
useModalRoot,
} from 'hooks'
import { getHalfTime } from 'features/MatchPage/helpers/getHalfTime'
import { useMatchPageStore } from 'features/MatchPage/store'
@ -24,6 +34,7 @@ import { Spotlight, Steps } from 'features/MatchTour'
import { StatsType } from '../TabStats/config'
import { CircleAnimationBar } from '../CircleAnimationBar'
import { Tooltip } from '../TabStats/styled'
import {
CellContainer,
ParamValueContainer,
@ -59,14 +70,28 @@ export const Cell = ({
watchAllEpisodesTimer,
} = useMatchPageStore()
const { shortSuffix, suffix } = useLexicsStore()
const { suffix } = useLexicsStore()
const { currentStep, isOpen } = useTour()
const client = useQueryClient()
const {
isTooltipShown,
onMouseLeave,
onMouseOver,
tooltipStyle,
tooltipText,
} = useTooltip()
const modalRoot = useModalRoot()
const { translate } = useLexicsStore()
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore)
const isTeam1 = teamId === profile?.team1.id
const getDisplayedValue = (val: number | null) => (
isNumber(val) ? String(val) : '-'
)
@ -104,7 +129,7 @@ export const Cell = ({
setEpisodeInfo({
episodesCount: param.val!,
paramName,
playerOrTeamName: teamId === profile?.team1.id
playerOrTeamName: isTeam1
? profile.team1[`name_${suffix}`]
: profile?.team2[`name_${suffix}`] || '',
})
@ -122,7 +147,7 @@ export const Cell = ({
? teamStatItem.param1
: teamStatItem.param2)
param && onParamClick(param, teamStatItem[`name_${shortSuffix}`])
param && onParamClick(param, translate(teamStatItem.lexic))
},
event: 'keydown',
target: paramValueContainerRef,
@ -137,7 +162,7 @@ export const Cell = ({
&& playingData.team.paramId === teamStatItem.param1.id
&& playingData.team.id === teamId
? (
<ParamValue>
<ParamValue onMouseLeave={onMouseLeave}>
<CircleAnimationBar
text={getDisplayedValue(teamStatItem.param1.val)}
size={22}
@ -147,13 +172,21 @@ export const Cell = ({
: (
<ParamValue
clickable={isClickable(teamStatItem.param1)}
onClick={() => onParamClick(teamStatItem.param1, teamStatItem[`name_${shortSuffix}`])}
onClick={() => onParamClick(teamStatItem.param1, translate(teamStatItem.lexic))}
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,
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...!isMobileDevice && isNumber(teamStatItem.param1.val) && teamStatItem.param2 && {
onMouseLeave,
onMouseOver: onMouseOver({
horizontalPosition: isTeam1 ? 'left' : 'right',
tooltipText: translate(teamStatItem.param1.lexic),
}),
}}
>
{getDisplayedValue(teamStatItem.param1.val)}
{firstClickableParam === teamStatItem.param1
@ -168,7 +201,7 @@ export const Cell = ({
&& playingData.team.paramId === teamStatItem.param2.id
&& playingData.team.id === teamId
? (
<ParamValue>
<ParamValue onMouseLeave={onMouseLeave}>
<CircleAnimationBar
text={getDisplayedValue(teamStatItem.param2.val)}
size={22}
@ -180,13 +213,24 @@ export const Cell = ({
<Divider>/</Divider>
<ParamValue
clickable={isClickable(teamStatItem.param2)}
onClick={() => onParamClick(teamStatItem.param2!, teamStatItem[`name_${shortSuffix}`])}
onClick={() => onParamClick(
teamStatItem.param2!,
translate(teamStatItem.lexic),
)}
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,
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...!isMobileDevice && isNumber(teamStatItem.param2.val) && {
onMouseLeave,
onMouseOver: onMouseOver({
horizontalPosition: isTeam1 ? 'left' : 'right',
tooltipText: translate(teamStatItem.param2.lexic),
}),
}}
>
{getDisplayedValue(teamStatItem.param2.val)}
{firstClickableParam === teamStatItem.param2
@ -198,6 +242,12 @@ export const Cell = ({
</Fragment>
)}
</ParamValueContainer>
{isTooltipShown && modalRoot.current && createPortal(
<Tooltip style={tooltipStyle}>
{tooltipText}
</Tooltip>,
modalRoot.current,
)}
</CellContainer>
)
}

@ -1,8 +1,12 @@
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import find from 'lodash/find'
import reduce from 'lodash/reduce'
import type { TeamStatItem } from 'requests'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsConfig } from 'features/LexicsStore'
export const useTeamsStatsTable = () => {
const {
@ -13,6 +17,26 @@ export const useTeamsStatsTable = () => {
teamsStats,
} = useMatchPageStore()
const lexicsIds = useMemo(
() => (
profile
? reduce<TeamStatItem, Array<number>>(
teamsStats[profile.team1.id],
(acc, curr) => {
!acc.includes(curr.lexic) && acc.push(curr.lexic)
!acc.includes(curr.param1.lexic) && acc.push(curr.param1.lexic)
curr.param2 && !acc.includes(curr.param2.lexic) && acc.push(curr.param2.lexic)
return acc
},
[],
)
: []),
[profile, teamsStats],
)
useLexicsConfig(lexicsIds)
const getStatItemById = (paramId: number) => {
if (!profile) return null

@ -3,7 +3,6 @@ import { useTour } from '@reactour/tour'
import map from 'lodash/map'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import { Loader } from 'features/Loader'
import { defaultTheme } from 'features/Theme/config'
@ -32,8 +31,6 @@ export const TeamsStatsTable = () => {
getStatItemById,
} = useTeamsStatsTable()
const { shortSuffix } = useLexicsStore()
const { isOpen } = useTour()
if (!profile) return null
@ -69,7 +66,6 @@ export const TeamsStatsTable = () => {
<tbody>
{map(teamsStats[profile.team1.id], (team1StatItem) => {
const team2StatItem = getStatItemById(team1StatItem.param1.id)
const statItemTitle = team1StatItem[`name_${shortSuffix}`]
return (
<Row key={team1StatItem.param1.id}>
@ -80,7 +76,7 @@ export const TeamsStatsTable = () => {
/>
<CellContainer>
<StatItemTitle>{statItemTitle}</StatItemTitle>
<StatItemTitle t={team1StatItem.lexic} />
</CellContainer>
<Cell

@ -4,6 +4,7 @@ import { isMobileDevice } from 'config'
import { Name } from 'features/Name'
import { customScrollbar } from 'features/Common'
import { T9n } from 'features/T9n'
export const Container = styled.div``
@ -59,11 +60,12 @@ export const CellContainer = styled.td`
}
:first-child, :last-child {
width: 32px;
width: 50px;
}
:first-child {
padding-left: 12px;
text-align: left;
}
:last-child {
@ -122,7 +124,7 @@ export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({
: '')}
`
export const StatItemTitle = styled.span`
export const StatItemTitle = styled(T9n)`
color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px;
text-transform: uppercase;

@ -28,6 +28,7 @@ export const MatchesList = ({
title,
}: Props) => {
const Component = matchesComponents[as]
return (
<Fragment>
{!isEmpty(matches) && (

@ -2,9 +2,9 @@ import { memo, useEffect } from 'react'
import { useRouteMatch } from 'react-router-dom'
import { useQuery } from 'react-query'
import { PAGES } from 'config/pages'
import { isMobileDevice, PAGES } from 'config'
import type { LiveScore } from 'requests'
import type { AdType, LiveScore } from 'requests'
import { getLiveScores } from 'requests'
import { MatchCard } from 'features/MatchCard'
@ -12,9 +12,14 @@ import { TournamentList } from 'features/TournamentList'
import type { Match } from 'features/Matches'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { readToken } from 'helpers'
import { useRecoilValue } from 'recoil'
import { Wrapper } from './styled'
import { useMatchSwitchesStore } from '../MatchSwitches'
import { querieKeys } from '../../config'
import { AdComponent } from '../../components/Ads/components/AdComponent'
import { adsStore } from '../../pages/HighlightsPage/storeHighlightsAtoms'
type MatchesGridProps = {
matches: Array<Match>,
@ -24,6 +29,10 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
const isHomePage = useRouteMatch(PAGES.home)?.isExact
const { isScoreHidden } = useMatchSwitchesStore()
const ads = useRecoilValue(adsStore)
const currentAds = ads?.match_cell?.length ? ads?.match_cell : ads?.block
const {
compareSport,
isShowTournament,
@ -76,6 +85,12 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
return (
<Wrapper>
{!isMobileDevice
&& readToken()
&& currentAds
&& (
currentAds?.map((ad: AdType) => <AdComponent ad={ad} />)
)}
{isHomePage && isShowTournament ? (
<TournamentList matches={filteredMatches()} />
) : (

@ -4,8 +4,14 @@ import { isMobileDevice } from 'config/userAgent'
export const Wrapper = styled.ul`
display: grid;
grid-template-columns: repeat(6, 15.6%);
grid-gap: 0.9rem;
grid-template-columns: repeat(6, 15.7%);
.secondRow {
grid-column: 1 / 2;
grid-row: 2 / 3;
}
${isMobileDevice
? css`
display: flex;

@ -120,7 +120,7 @@ export const ControlsWeb = (controlsProps: { props: ControlsPropsExtended }) =>
)}
<ChromeCast src={src} />
{document.pictureInPictureEnabled && (
<PiP videoRef={videoRef} isPlaying={playing} />
<PiP videoRef={videoRef} />
)}
<Settings
onSelect={onQualitySelect}

@ -339,7 +339,7 @@ export const useVideoPlayer = ({
setPlayerState({ playedProgress: value })
timeForStatistics.current = (value + chapter.startMs) / 1000
setPlayingProgress(Math.floor(value / 1000))
chapter.isFullMatchChapter && setPlayingProgress(Math.floor(value / 1000))
progressChangeCallback(value / 1000)
}
@ -530,6 +530,20 @@ export const useVideoPlayer = ({
selectedPlaylist,
])
/**
* Для воcпроизведения нового эпизода, аналогичного текущему, с начала
*/
useEffect(() => {
if (chaptersProps[0].isFullMatchChapter) return
setPlayerState({
...initialState,
chapters: chaptersProps,
playing: true,
seek: chaptersProps[0].startOffsetMs / 1000,
})
}, [chaptersProps, setPlayerState])
useEffect(() => {
if ((
chapters[0]?.isFullMatchChapter)

@ -1,7 +1,10 @@
import { Fragment, useState } from 'react'
import { isLffClient } from 'config/clients'
import { URL_AWS } from 'config'
import {
isMobileDevice,
URL_AWS,
} from 'config'
import { SportIcon } from 'components/SportIcon/SportIcon'
import { T9n } from 'features/T9n'
@ -14,6 +17,8 @@ import {
LiveSign,
} from 'features/MatchCard/CardFrontside/MatchCardMobile/styled'
import { AdType } from 'requests'
import { useRecoilValue } from 'recoil'
import {
CardWrapperOuter,
CardWrapper,
@ -25,6 +30,9 @@ import {
} from './styled'
import { TournamentProps } from '../..'
import { VIEW_ADS } from '../../../../components/Ads/types'
import { MobileAd } from '../../../../components/Ads/components/MobileAd'
import { adsStore } from '../../../../pages/HighlightsPage/storeHighlightsAtoms'
export const TournamentMobile = ({
tournament,
@ -40,6 +48,8 @@ export const TournamentMobile = ({
const currentColor = open ? '#ffffff' : 'rgba(255, 255, 255, 0.5)'
const ads = useRecoilValue(adsStore)
return (
<CardWrapperOuter>
<CardWrapper
@ -75,10 +85,29 @@ export const TournamentMobile = ({
</ScSecondInfo>
</CardWrapper>
<ScMatchesWrapper>
{open
&& tournamentMatches?.map((match: Match) => (
<MatchCard key={match.id} match={match} />
))}
{open && (
<>
{isMobileDevice && ads?.mobile?.map((ad: AdType) => (
ad.position.id === VIEW_ADS.MOBILE_IN_COLLAPSE_HEADER && (
<MobileAd
ad={ad}
key={ad.id}
className='mobileAdInCell'
/>
)))}
{tournamentMatches?.map((match: Match) => (
<MatchCard key={match.id} match={match} />
))}
{isMobileDevice && ads?.mobile?.map((ad: AdType) => (
ad.position.id === VIEW_ADS.MOBILE_IN_COLLAPSE_FOOTER && (
<MobileAd
ad={ad}
key={ad.id}
className='mobileAdInCell'
/>
)))}
</>
)}
</ScMatchesWrapper>
</CardWrapperOuter>
)

@ -80,6 +80,10 @@ export const ScSecondInfo = styled.div`
export const ScMatchesWrapper = styled.ul`
display: flex;
flex-direction: column;
.mobileAdInCell {
margin: 6px 0 0;
}
`
export const ScStar = styled(Icon)`

@ -32,6 +32,7 @@ export type TournamentListProps = {
export const TournamentList = ({ matches }: TournamentTypeProps) => {
const { tournaments, tournamentSort } = useTournaments(matches)
const isHomePage = useRouteMatch(PAGES.home)?.isExact
switch (true) {

@ -23,6 +23,8 @@ import {
import type { FormState } from 'features/FormStore/hooks/useFormState'
import { useLexicsStore } from 'features/LexicsStore'
import { readToken } from 'helpers'
const transformToFormState = async (userInfo: UserInfo, lang: string) => {
const cities = userInfo.country?.id
? await getCountryCities('', userInfo.country.id)
@ -87,7 +89,7 @@ export const useUserInfo = () => {
}, [fetchUserInfo])
useEffect(() => {
fetchUserInfo()
readToken() && fetchUserInfo()
}, [fetchUserInfo])
return {

@ -1,9 +1,6 @@
import { useMemo } from 'react'
import { client } from 'config/clients'
import { formIds } from 'config/form'
import { AUTH_SERVICE } from 'config/routes'
import { ClientNames } from 'config/clients/types'
import { Combobox } from 'features/Combobox'
import { Input } from 'features/Common'
@ -47,15 +44,6 @@ export const PersonalInfoForm = (props: Props) => {
updateFormValue,
} = useUserInfo(props)
const isPrivacyPolicyShown = useMemo(() => {
switch (client.name) {
case ClientNames.Facr:
return false
default:
return true
}
}, [])
return (
<Form>
<Input
@ -136,15 +124,13 @@ export const PersonalInfoForm = (props: Props) => {
>
<T9n t='terms_and_conditions' />
</PrivacyPolicyLink>
{isPrivacyPolicyShown && (
<PrivacyPolicyLink
target='_blank'
href={`${AUTH_SERVICE}${client.privacyLink}`}
id='personal_policy'
>
<T9n t='privacy_policy_and_statement' />
</PrivacyPolicyLink>
)}
<PrivacyPolicyLink
target='_blank'
href={`${AUTH_SERVICE}${client.privacyLink}`}
id='personal_policy'
>
<T9n t='privacy_policy_and_statement' />
</PrivacyPolicyLink>
</PrivacyWrapper>
</Form>
)

@ -9,6 +9,8 @@ import round from 'lodash/round'
import { LogActions, logUserAction } from 'requests/logUserAction'
import { readToken } from 'helpers'
export const usePageLogger = (page?: string) => {
const location = useLocation()
const startTimeRef = useRef(new Date())
@ -24,7 +26,7 @@ export const usePageLogger = (page?: string) => {
const url = page || location.pathname
const log = useCallback(() => {
logUserAction({
readToken() && logUserAction({
actionType: LogActions.PageChange,
dateVisit: startTimeRef.current.toISOString(),
duration: getSpentTime(),

@ -5,14 +5,13 @@ import {
} from 'react'
import ReactDOM from 'react-dom'
import { isIOS } from 'config/userAgent'
// import { makeServer } from 'utilits/mirage/server'
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/react'
import { isIOS, ENV } from 'config'
// import { makeServer } from 'utilits/mirage/Mirage'
import * as serviceWorker from './serviceWorker'
import { ENV } from './config'
if (process.env.NODE_ENV !== 'development') {
Sentry.init({

@ -1,6 +1,6 @@
import { atom, selector } from 'recoil'
import type { Match } from 'requests'
import type { AdResponse, Match } from 'requests'
export type MatchType = Match & {
isChecked: boolean,
@ -43,6 +43,11 @@ export const fetchingMatches = atom({
key: 'fetchingMatches',
})
export const adsStore = atom({
default: {} as AdResponse,
key: 'adsStore',
})
export const checkedMatches = selector({
get: ({ get }) => {
const matches = get(playerMatchesState)

@ -0,0 +1,52 @@
import { callApi } from 'helpers'
import { ADS_API_URL } from 'config'
import { DeviceType } from '../../components/Ads/types'
export type AdsParams = {
client_type: DeviceType,
language: string,
type_id: number,
}
export type AdType = {
'duration': number,
'frequency': number,
'id': number,
'impressions': number,
'link': string,
'media': {
url: string,
},
'name': string,
'position': {
'id': number,
'name_eng': string,
'name_rus': string,
'source_type': string,
},
remaining_views: number,
'time_close': number,
'type': {
'id': number,
'name_eng': string,
'name_rus': string,
},
}
export type PositionName = 'header' | 'block' | 'match_cell' | 'mobile'
export type AdResponse = Record<PositionName, AdsListType>
export type AdsListType = Array<AdType>
export const getAds = (params: AdsParams): Promise<AdResponse> => {
const config = {
body: params,
}
return callApi({
config,
url: ADS_API_URL,
})
}

@ -0,0 +1,2 @@
export * from './getAds'
export * from './updateAdsView'

@ -0,0 +1,29 @@
import { callApi } from 'helpers'
import { ADS_API_URL } from 'config'
export type AdsViewParams = {
adv_id: number,
}
type AdsViewResponse = {
'data': string,
'message': string,
'reason': string,
'status': 'failed' | 'failed' | 'success',
}
export const updateAdsView = (
{ adv_id }: AdsViewParams,
): Promise<AdsViewResponse> => {
const config = {
body: {
adv_id,
},
}
return callApi({
config,
url: `${ADS_API_URL}/${adv_id}/view`,
})
}

@ -20,7 +20,7 @@ export type FavouriteTeams = {
export type ResponseType = {
data: {
data: Array<FavouriteTeams>,
},
} | null,
msg: string,
status: string,
}

@ -4,6 +4,7 @@ import { API_ROOT } from 'config'
type ScoreTeam = {
id: number,
penalty_score: number | null,
score: number | null,
}

@ -16,6 +16,7 @@ export type Team = {
id: number,
name_eng: string,
name_rus: string,
penalty_score: number | null,
score: number,
shirt_color: string | null,
}

@ -4,7 +4,6 @@ import { client } from 'config/clients'
import type { MatchesBySection } from './types'
import { requestMatches } from './request'
import { getMatchesPreviews } from './getPreviews'
const proc = PROCEDURES.get_matches
@ -34,5 +33,4 @@ export const getHomeMatches = async ({
}
return requestMatches(config, url)
.then(getMatchesPreviews)
}

@ -10,7 +10,6 @@ import { addSportType } from 'features/Matches/helpers/addSportType'
import type { MatchesBySection } from './types'
import { requestMatches } from './request'
import { getMatchesPreviews } from './getPreviews'
const proc = PROCEDURES.get_player_matches
@ -47,5 +46,4 @@ export const getPlayerMatches = async ({
return requestMatches(config, url)
.then((matches) => addSportType(matches, sportType))
.then(getMatchesPreviews)
}

@ -1,82 +0,0 @@
import isEmpty from 'lodash/isEmpty'
import reduce from 'lodash/reduce'
import find from 'lodash/find'
import map from 'lodash/map'
import type { PreviewsData, Previews } from 'requests'
import { getMatchesPreviewImages } from 'requests'
import type { MatchesBySection, Matches } from './types'
const combinePreviews = (matches: Matches, previews: Previews) => (
map(matches, (match) => {
const preview = find(
previews,
{ match_id: match.id, sport_id: match.sport },
)?.preview
return preview ? { ...match, preview } : match
})
)
/**
* Запрашивает превью картинок матчей с видео и статус которых Завершенный
*/
const getPreviews = async (matches: Matches) => {
const previewsData = reduce(
matches,
(acc: PreviewsData, {
has_video,
id,
sport,
}) => {
if (has_video) {
acc.push({
match_id: id,
sport_id: sport,
})
}
return acc
},
[],
)
if (isEmpty(previewsData)) return matches
const previews = await getMatchesPreviewImages(previewsData)
return combinePreviews(matches, previews)
}
export const getMatchesPreviews = async (matches: MatchesBySection) => {
try {
if (matches.isVideoSections) {
const [
broadcast,
features,
highlights,
] = await Promise.all(
[
getPreviews(matches.broadcast),
getPreviews(matches.features),
getPreviews(matches.highlights),
],
)
return {
...matches,
broadcast,
features,
highlights,
}
}
const broadcast = await getPreviews(matches.broadcast)
return {
...matches,
broadcast,
features: [],
highlights: [],
}
} catch (error) {
return matches
}
}

@ -9,7 +9,6 @@ import { client } from 'config/clients'
import { addSportType } from 'features/Matches/helpers/addSportType'
import type { MatchesBySection } from './types'
import { getMatchesPreviews } from './getPreviews'
import { requestMatches } from './request'
const proc = PROCEDURES.get_team_matches
@ -41,5 +40,4 @@ export const getTeamMatches = async ({
return requestMatches(config, url)
.then((matches) => addSportType(matches, sportType))
.then(getMatchesPreviews)
}

@ -9,7 +9,6 @@ import { client } from 'config/clients'
import { addSportType } from 'features/Matches/helpers/addSportType'
import type { MatchesBySection } from './types'
import { getMatchesPreviews } from './getPreviews'
import { requestMatches } from './request'
const proc = PROCEDURES.get_tournament_matches
@ -41,5 +40,4 @@ export const getTournamentMatches = async ({
return requestMatches(config, url)
.then((matches) => addSportType(matches, sportType))
.then(getMatchesPreviews)
}

@ -22,6 +22,7 @@ type Team = {
media?: AwsTeamMedia,
name_eng: string,
name_rus: string,
penalty_score?: number | null,
score?: number | null,
}

@ -34,3 +34,7 @@ export * from './getMatchParticipants'
export * from './getStatsEvents'
export * from './getTokenVirtualUser'
export * from './checkDevice'
export * from './getAds'
export * from './getFavouriteTeam'
export * from './getCountryCode'
export * from './getAgreements'

@ -1,31 +0,0 @@
/* eslint-disable */
import {
createServer,
Model,
} from 'miragejs'
import { ResponseType } from 'requests/getFavouriteTeam'
import { surveys } from './fixtures/surveys'
export function makeServer({ environment = 'test' } = {}) {
const server = createServer({
environment,
factories: {},
fixtures: {
surveys,
},
models: {
surveys: Model.extend<Partial<ResponseType>>({}),
},
routes() {
this.passthrough('https://api.insports.tv/***')
this.passthrough('https://insports.tv/***')
this.passthrough('https://images.insports.tv/***')
this.passthrough('https://auth.insports.tv/***')
this.passthrough('${URL_AWS}/***')
this.get('https://api.insports.tv/v1/survey/teams/1/131/30', (schema: any) => schema.all('surveys').models[0].attrs)
},
})
return server
}

@ -0,0 +1,300 @@
/* eslint-disable */
export const getAds = () => {
return {
"mobile": [
{
"id": 71,
"name": "Test 2",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 12,
"source_type": "Web",
"name_eng": "Web main ad 2 (1x1)",
"name_rus": "Веб: главная 2 (1x1)"
},
"link": "https://www.google.com/",
"impressions": 10,
"frequency": 2,
"duration": 120,
"time_close": 150,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png"
},
"remaining_views": 9
},{
"id": 71,
"name": "Test 2",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 25,
"source_type": "Web",
"name_eng": "Web main ad 2 (1x1)",
"name_rus": "Веб: главная 2 (1x1)"
},
"link": "https://www.google.com/",
"impressions": 10,
"frequency": 2,
"duration": 120,
"time_close": 150,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png"
},
"remaining_views": 9
},
{
"id": 72,
"name": "Best mens boots for sport advertise here",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 11,
"source_type": "Web",
"name_eng": "Web main ad 7 (1x1)",
"name_rus": "Веб: главная 7 (1x1)"
},
"link": "https://www.google.com/",
"impressions": 2,
"frequency": 1,
"duration": 300,
"time_close": 500,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/72/en/web.png"
},
"remaining_views": 9
}
],
"match_cell": [
{
"id": 71,
"name": "Test 2",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 1,
"source_type": "Web",
"name_eng": "Web main ad 2 (1x1)",
"name_rus": "Веб: главная 2 (1x1)"
},
"link": "https://www.google.com/",
"impressions": 10,
"frequency": 2,
"duration": 15,
"time_close": 15,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png"
},
"remaining_views": 9
},
{
"id": 71,
"name": "Test 2",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 2,
"source_type": "Web",
"name_eng": "Web main ad 2 (1x1)",
"name_rus": "Веб: главная 2 (1x1)"
},
"link": "https://www.google.com/",
"impressions": 10,
"frequency": 2,
"duration": 12,
"time_close": 15,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png"
},
"remaining_views": 9
},
{
"id": 72,
"name": "Test 3",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 3,
"source_type": "Web",
"name_eng": "Web main ad 7 (1x1)",
"name_rus": "Веб: главная 7 (1x1)"
},
"link": "https://www.google.com/",
"impressions": 10,
"frequency": 3,
"duration": 15,
"time_close": 15,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/72/en/web.png"
},
"remaining_views": 9
}
],
"header": [
// {
// "id": 77,
// "name": "Test 8",
// "type": {
// "id": 1,
// "name_eng": "Main",
// "name_rus": "Главная"
// },
// "position": {
// "id": 8,
// "source_type": "Web",
// "name_eng": "Web main ad 2 (3x1)",
// "name_rus": "Веб: главная 2(3x1)"
// },
// "link": null,
// "impressions": 10,
// "frequency": 2,
// "duration": 120,
// "time_close": 15,
// "media": {
// "url": "https://cf-aws-staging.insports.tv/media/folder/77/en/web.png"
// },
// "remaining_views": 10
// },
{
"id": 78,
"name": "Test 9",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 9,
"source_type": "Web",
"name_eng": "Web main ad (6x1)",
"name_rus": "Веб: главная (6x1)"
},
"link": null,
"impressions": 10,
"frequency": 2,
"duration": 10,
"time_close": 15,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/78/en/web.png"
},
"remaining_views": 15
},
{
"id": 76,
"name": "Test 7",
"type": {
"id": 1,
"name_eng": "Main",
"name_rus": "Главная"
},
"position": {
"id": 7,
"source_type": "Web",
"name_eng": "Web main ad (3x1)",
"name_rus": "Веб: главная (3x1)"
},
"link": null,
"impressions": 10,
"frequency": 4,
"duration": 10,
"time_close": 15,
"media": {
"url": "https://cf-aws-staging.insports.tv/media/folder/76/en/web.png"
},
"remaining_views": 15
}
],
"block": [
// {
// "id": 75,
// "name": "Test 6",
// "type": {
// "id": 1,
// "name_eng": "Main",
// "name_rus": "Главная"
// },
// "position": {
// "id": 6,
// "source_type": "Web",
// "name_eng": "Web main ad (2x2)",
// "name_rus": "Веб: главная (2x2)"
// },
// "link": null,
// "impressions": 10,
// "frequency": 3,
// "duration": 120,
// "time_close": null,
// "media": {
// "url": "https://cf-aws-staging.insports.tv/media/folder/75/en/web.png"
// },
// "remaining_views": 10
// },
// {
// "id": 73,
// "name": "Test 4",
// "type": {
// "id": 1,
// "name_eng": "Main",
// "name_rus": "Главная"
// },
// "position": {
// "id": 4,
// "source_type": "Web",
// "name_eng": "Web main ad (2x1)",
// "name_rus": "Веб: главная (2x1)"
// },
// "link": null,
// "impressions": 10,
// "frequency": 1,
// "duration": 120,
// "time_close": null,
// "media": {
// "url": "https://cf-aws-staging.insports.tv/media/folder/73/en/web.png"
// },
// "remaining_views": 9
// },
// {
// "id": 74,
// "name": "Test 5",
// "type": {
// "id": 1,
// "name_eng": "Main",
// "name_rus": "Главная"
// },
// "position": {
// "id": 5,
// "source_type": "Web",
// "name_eng": "Web main ad (1x2)",
// "name_rus": "Веб: главная (1x2)"
// },
// "link": null,
// "impressions": 10,
// "frequency": 2,
// "duration": 120,
// "time_close": null,
// "media": {
// "url": "https://cf-aws-staging.insports.tv/media/folder/74/en/web.png"
// },
// "remaining_views": 9
// }
]
}
}

@ -0,0 +1,2 @@
export * from './getAds'
export * from './surveys'

@ -0,0 +1,8 @@
/* eslint-disable */
import { Model } from 'miragejs'
import type { ResponseType, AdResponse } from 'requests'
export const models = {
ads: Model.extend<Partial<AdResponse>>({}),
surveys: Model.extend<Partial<ResponseType>>({}),
}

@ -0,0 +1,44 @@
/* eslint-disable */
import {
createServer,
} from 'miragejs'
import {
ADS_API_URL,
APIS,
AUTH_SERVICE,
STATS_API_URL,
URL_AWS,
VIEWS_API,
} from 'config'
import { API_ROOT } from 'features/AuthServiceApp/config/routes'
import { surveys, getAds } from './fixtures'
import { models } from './models'
const mainDomain = 'insports.tv'
export function makeServer({ environment = 'test' } = {}) {
const server = createServer({
environment,
fixtures: {
surveys,
ads: getAds(),
},
models: models,
routes() {
this.passthrough(`${API_ROOT}/***`)
this.passthrough(`${VIEWS_API}/***`)
this.passthrough(`${STATS_API_URL}/***`)
this.passthrough(`${APIS.production.api}/***`)
this.passthrough(`${APIS.staging.api}/***`)
this.passthrough(`${AUTH_SERVICE}/***`)
this.passthrough(`https://${mainDomain}/***`)
this.passthrough(`https://images.${mainDomain}/***`)
this.passthrough(`${URL_AWS}/***`)
this.post(`${ADS_API_URL}`, getAds)
this.logging = true;
},
})
return server
}
Loading…
Cancel
Save