Merge branch 'develop' into master_

pull/261/head
Ruslan Khayrullin 2 years ago
commit 01ce675fed
  1. 53
      .drone.yml
  2. 19
      Makefile
  3. 5
      package-lock.json
  4. 1
      package.json
  5. 156
      src/components/Ads/components/AdComponent/hooks.tsx
  6. 91
      src/components/Ads/components/AdComponent/index.tsx
  7. 109
      src/components/Ads/components/AdComponent/styled.tsx
  8. 79
      src/components/Ads/components/MobileAd/index.tsx
  9. 111
      src/components/Ads/components/MobileAd/styled.tsx
  10. 16
      src/components/Ads/helpers/calcMaxDurationAds.tsx
  11. 1
      src/components/Ads/helpers/index.tsx
  12. 2
      src/components/Ads/helpers/isVideo.tsx
  13. 65
      src/components/Ads/hooks.tsx
  14. 31
      src/components/Ads/index.tsx
  15. 16
      src/components/Ads/styled.tsx
  16. 59
      src/components/Ads/types.tsx
  17. 72
      src/components/Carousel/helpers/common.tsx
  18. 274
      src/components/Carousel/helpers/elements.tsx
  19. 4
      src/components/Carousel/helpers/index.tsx
  20. 12
      src/components/Carousel/helpers/mappers.tsx
  21. 44
      src/components/Carousel/helpers/math.tsx
  22. 160
      src/components/Carousel/hooks.tsx
  23. 86
      src/components/Carousel/index.tsx
  24. 25
      src/components/Carousel/styled.tsx
  25. 73
      src/components/Carousel/types.tsx
  26. 1
      src/config/clients/facr.tsx
  27. 1
      src/config/clients/fqtv.tsx
  28. 2
      src/config/clients/india.tsx
  29. 2
      src/config/clients/insports.tsx
  30. 1
      src/config/clients/instat.tsx
  31. 1
      src/config/clients/lff.tsx
  32. 2
      src/config/clients/tunisia.tsx
  33. 2
      src/config/clients/types.tsx
  34. 2
      src/config/index.tsx
  35. 15
      src/config/lexics/indexLexics.tsx
  36. 1
      src/config/lexics/landingLexics.tsx
  37. 1
      src/config/localStorageKeys.tsx
  38. 1
      src/config/pages.tsx
  39. 4
      src/config/payments.tsx
  40. 1
      src/config/queries.tsx
  41. 6
      src/config/routes.tsx
  42. 8
      src/config/userAgent.tsx
  43. 18
      src/features/App/AuthenticatedApp.tsx
  44. 9
      src/features/AuthServiceApp/components/ChangePassword/hooks.tsx
  45. 2
      src/features/AuthServiceApp/config/clients/index.tsx
  46. 1
      src/features/AuthServiceApp/config/clients/types.tsx
  47. 2
      src/features/BuyMatchPopup/components/CardStep/index.tsx
  48. 13
      src/features/BuyMatchPopup/components/ErrorStep/index.tsx
  49. 39
      src/features/BuyMatchPopup/components/IframePayment/hooks.tsx
  50. 94
      src/features/BuyMatchPopup/components/PackageMobile/index.tsx
  51. 125
      src/features/BuyMatchPopup/components/PackageMobile/styled.tsx
  52. 154
      src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx
  53. 22
      src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx
  54. 206
      src/features/BuyMatchPopup/components/Packages/index.tsx
  55. 65
      src/features/BuyMatchPopup/components/Packages/styled.tsx
  56. 84
      src/features/BuyMatchPopup/components/PackagesList/index.tsx
  57. 230
      src/features/BuyMatchPopup/components/PackagesList/styled.tsx
  58. 74
      src/features/BuyMatchPopup/components/RegularPackage/index.tsx
  59. 140
      src/features/BuyMatchPopup/components/RegularPackage/styled.tsx
  60. 70
      src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx
  61. 115
      src/features/BuyMatchPopup/components/SelectSubscription/index.tsx
  62. 33
      src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx
  63. 58
      src/features/BuyMatchPopup/components/SelectedCard/index.tsx
  64. 52
      src/features/BuyMatchPopup/components/SinglePackage/index.tsx
  65. 100
      src/features/BuyMatchPopup/components/SinglePackage/styled.tsx
  66. 22
      src/features/BuyMatchPopup/index.tsx
  67. 47
      src/features/BuyMatchPopup/store/helpers.tsx
  68. 61
      src/features/BuyMatchPopup/store/hooks/index.tsx
  69. 9
      src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx
  70. 18
      src/features/BuyMatchPopup/styled.tsx
  71. 5
      src/features/BuyMatchPopup/types.tsx
  72. 19
      src/features/HeaderFilters/components/DateFilter/hooks/index.tsx
  73. 31
      src/features/HomePage/hooks.tsx
  74. 25
      src/features/HomePage/index.tsx
  75. 49
      src/features/Landings/hooks.tsx
  76. 13
      src/features/Landings/index.tsx
  77. 11
      src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx
  78. 25
      src/features/MatchCard/CardFrontside/MatchCardMobile/styled.tsx
  79. 19
      src/features/MatchCard/CardFrontside/index.tsx
  80. 10
      src/features/MatchCard/hooks.tsx
  81. 13
      src/features/MatchCard/styled.tsx
  82. 4
      src/features/MatchPage/components/SubscriptionGuard/index.tsx
  83. 35
      src/features/MatchPage/index.tsx
  84. 14
      src/features/MatchPage/store/hooks/index.tsx
  85. 24
      src/features/MatchSidePlaylists/components/EventsList/index.tsx
  86. 18
      src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx
  87. 1
      src/features/MatchSidePlaylists/components/TabEvents/index.tsx
  88. 5
      src/features/MatchSidePlaylists/components/TabEvents/styled.tsx
  89. 18
      src/features/MatchSidePlaylists/components/TabWatch/index.tsx
  90. 18
      src/features/MatchSidePlaylists/index.tsx
  91. 15
      src/features/MatchSidePlaylists/styled.tsx
  92. 23
      src/features/MatchesGrid/index.tsx
  93. 1
      src/features/MatchesGrid/styled.tsx
  94. 31
      src/features/PaymentPeriodTabs/helpers.tsx
  95. 115
      src/features/PaymentPeriodTabs/index.tsx
  96. 1
      src/features/PlayerPage/hooks.tsx
  97. 12
      src/features/PlayerPage/index.tsx
  98. 51
      src/features/Price/components/BasePrice/index.tsx
  99. 51
      src/features/Price/components/BrazilianPrice/index.tsx
  100. 6
      src/features/Price/components/BrazilianPrice/styled.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -954,3 +954,56 @@ steps:
- aws cloudfront create-invalidation --distribution-id E5DKN8IPOMASO --paths "/*"
depends_on:
- make-india
---
kind: pipeline
type: docker
name: deploy tv.rustatsport.ru
concurrency:
limit: 1
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/tv.rustatsport.ru
steps:
- name: npm-install
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- npm install --legacy-peer-deps
- name: make-rustat
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make rustat-prod
depends_on:
- npm-install
- name: deploy-rustat
image: amazon/aws-cli:latest
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION:
from_secret: AWS_DEFAULT_REGION
AWS_MAX_ATTEMPTS: 10
commands:
- aws s3 sync build_rustat s3://insports-tv-rustatsport --delete
- aws cloudfront create-invalidation --distribution-id E15IFY23VM147K --paths "/*"
depends_on:
- make-rustat

@ -46,7 +46,7 @@ build-c: clean
build-d: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=facr \
REACT_APP_CLIENT=insports \
REACT_APP_STAGE=test-d \
npm run build
@ -148,6 +148,12 @@ lff-build: clean
REACT_APP_CLIENT=lff \
npm run build
rustat-build: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=rustat \
npm run build
.PHONY: build
prod: clean
@ -210,7 +216,16 @@ fqtv-prod:
BUILD_PATH=build_fqtv \
npm run build && cp -r .well-known build_fqtv
deploy-all: prod preprod facr-prod lff-prod diwansport-prod india-prod fqtv-prod
rustat-prod:
rm -rf build_rustat && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=production \
REACT_APP_CLIENT=rustat \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
BUILD_PATH=build_rustat \
npm run build && cp -r .well-known build_rustat
deploy-all: prod preprod facr-prod lff-prod diwansport-prod india-prod fqtv-prod rustat-prod
test:
npm test

5
package-lock.json generated

@ -21000,6 +21000,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
"react-ga": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz",
"integrity": "sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ=="
},
"react-inspector": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz",

@ -39,6 +39,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,156 @@
import { useEffect, useState } from 'react'
import ReactGA from 'react-ga'
import { updateAdsView } from 'requests'
import { useToggle } from 'hooks'
import { getLocalStorageItem, isMatchPage } from 'helpers'
import {
device,
COUNTRY,
} from 'config'
import { useMatchPageStore } from 'features/MatchPage/store'
import type { AdComponentType } from './index'
import { checkVideo } from '../../helpers'
import {
adsViews,
EventGA,
ViewsType,
} from '../../types'
const countryCode = getLocalStorageItem(COUNTRY)
export const useAd = ({ ad }: AdComponentType) => {
const [isOpenAd, setIsOpenAd] = useState(true)
const [isNeedToShow, setIsNeedToShow] = useState(true)
const [shownTime, setShownTime] = useState(0)
const { isFullscreen } = useMatchPageStore()
const views = getLocalStorageItem(adsViews) as ViewsType
const {
duration,
frequency,
id,
media,
name,
time_close,
} = ad
const {
close,
isOpen: isOpenCloseBtn,
open: showCloseBtn,
} = useToggle()
const isNeedBanner = Number(views?.HOME) % frequency === 0
const isVideo = checkVideo(media.url)
const currentAdsTime = duration - shownTime
useEffect(() => {
if (!isFullscreen) {
if (currentAdsTime === 0) {
setShownTime(0)
} else {
const stopWatch = setInterval(() => {
setShownTime((prev) => prev + 1)
}, 1000)
return () => clearInterval(stopWatch)
}
}
return undefined
}, [
isFullscreen,
currentAdsTime,
])
const handleClose = () => {
setIsOpenAd(false)
isMatchPage() && setIsNeedToShow(false)
sendBannerClickEvent(EventGA.CLOSE)
}
const sendBannerClickEvent = (event: EventGA) => {
ReactGA.initialize('Advertisement')
ReactGA.event({
action: event,
category: 'Advertisement',
label: `${name}_${countryCode ?? ''}_${device}`,
value: id,
})
}
useEffect(() => {
setShownTime(0)
if (isMatchPage()) {
const interval = setInterval(() => {
setIsNeedToShow(true)
setIsOpenAd(true)
}, frequency * 1000)
return () => clearInterval(interval)
}
setIsNeedToShow(isNeedBanner)
return setIsOpenAd(isNeedBanner)
}, [
frequency,
isNeedToShow,
views?.HOME,
isNeedBanner,
])
useEffect(() => {
if (isFullscreen || !isOpenAd) return undefined
const timeoutCloseAd = setTimeout(handleClose, currentAdsTime * 1000)
return () => {
clearTimeout(timeoutCloseAd)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isNeedToShow,
isOpenAd,
isFullscreen,
currentAdsTime,
])
useEffect(() => {
close()
const timeoutCloseBtn = time_close && setTimeout(showCloseBtn, time_close * 1000)
return () => {
time_close && clearTimeout(timeoutCloseBtn)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isNeedToShow,
isOpenAd,
views?.HOME,
])
useEffect(() => {
if (!isNeedToShow || (!isMatchPage() && !isNeedBanner)) return
(async () => {
await updateAdsView({ adv_id: id })
})()
sendBannerClickEvent(EventGA.DISPLAY)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, isNeedToShow])
return {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
}
}

@ -0,0 +1,91 @@
import type { MouseEvent } from 'react'
import { memo } from 'react'
import type { AdType } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
import { useAd } from './hooks'
import { EventGA } from '../../types'
import {
AdImg,
AdVideo,
AdWrapper,
LinkWrapper,
AdsCloseButton,
} from './styled'
export type AdComponentType = {
ad: AdType,
}
export const AdComponent = memo(({ ad }: AdComponentType) => {
const {
link,
media,
position,
} = ad
const {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
} = useAd({ ad })
const { suffix } = useLexicsStore()
const close = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
handleClose()
}
const onLinkClick = () => {
link && sendBannerClickEvent(EventGA.CLICK)
}
return (
position && isOpenAd && isNeedToShow
? (
<AdWrapper
position={position.id}
isOpenAd={isOpenAd}
>
{isOpenCloseBtn && (
<AdsCloseButton
onClick={close}
size={12}
position={position.id}
/>
)}
<LinkWrapper
href={link}
target='_blank'
rel='noreferrer'
onClick={onLinkClick}
>
{isVideo
? (
<AdVideo
muted={isVideo}
autoPlay={isVideo}
loop={isVideo}
src={media.url}
position={position.id}
/>
)
: (
<AdImg
src={media.url}
position={position.id}
alt={`name_${suffix}`}
/>
)}
</LinkWrapper>
</AdWrapper>
) : null
)
})

@ -0,0 +1,109 @@
import styled, { css } from 'styled-components/macro'
import includes from 'lodash/includes'
import { CloseButton } from 'features/PopupComponents'
import {
MATCH_ADS,
PLAYER_ADS,
VIEW_ADS,
} from '../../types'
type Props = {
position: number,
}
const header = [7, 8, 9]
const chooseStyle = (type: number) => {
switch (true) {
case VIEW_ADS.COLUMN === type:
return 'grid-row: 1 / 3; img {max-height: none;}'
case VIEW_ADS.ROW === type:
return 'grid-column: 1 / 3'
case VIEW_ADS.SQUARE === type:
return 'grid-row: 1 / 3; grid-column: 1 / 3; img {max-height: none;}'
case VIEW_ADS.SECOND_COLUMN === type:
return 'grid-column: 2 / 3; grid-row: 1 / 1'
case VIEW_ADS.SECOND_ROW === type:
return 'grid-column: 1 / 2; grid-row: 2 / 3;'
case MATCH_ADS.PLAYS_TOP === type:
return 'margin-left: 14px; height: 48px'
case MATCH_ADS.PLAYS_BOTTOM === type:
return 'grid-row: 4; margin-bottom: 12px; height: 48px;'
case PLAYER_ADS.LEFT_BOTTOM === type:
return css`
height: auto;
width: 42.4%;
bottom: 100px;
left: 20px;
`
case PLAYER_ADS.CENTER_BOTTOM === type:
return css`
height: 18.3%;
width: 81.3%;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
`
case PLAYER_ADS.RIGHT === type:
return css`
height: 87.2%;
width: 18.3%;
bottom: 90px;
right: 18px;
`
case PLAYER_ADS.FULL_SCREEN === type:
return 'bottom: 0; left: 0;'
default:
return ''
}
}
export const AdImg = styled.img<Props>`
width: 100%;
min-height: ${({ position }) => (!includes(header, position) && '100%')};
max-height: ${({ position }) => (includes(header, position) ? '13rem' : '100%')};
cursor: pointer;
border-radius: 3px;
`
export const AdVideo = styled.video<Props>`
object-fit: contain;
width: 100%;
cursor: pointer;
max-height: ${({ position }) => (includes(header, position) ? '283px' : '100%')};
background-color: black;
border-radius: 3px;
`
export const AdWrapper = styled.div<Props & {isOpenAd: boolean}>`
position: ${({ position }) => (includes(PLAYER_ADS, position) ? 'absolute' : 'relative')};
width: 100%;
height: 100%;
z-index: 1;
${({ position }) => chooseStyle(position)};
display: ${({ isOpenAd }) => (isOpenAd ? '' : 'none')};
`
export const AdsCloseButton = styled(CloseButton)<Props>`
position: absolute;
right: ${({ position }) => (position === PLAYER_ADS.FULL_SCREEN ? '10px' : '0')};
top: ${({ position }) => (position === PLAYER_ADS.FULL_SCREEN ? '10px' : '0')};
background: none;
border-radius: 0;
z-index: 2;
cursor: pointer;
color: #9B9B9B;
width: 28px;
height: 28px;
:hover {
background: none;
}
`
export const LinkWrapper = styled.a``

@ -0,0 +1,79 @@
import type { MouseEvent } from 'react'
import includes from 'lodash/includes'
import type { AdType } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
import { useAd } from '../AdComponent/hooks'
import { EventGA, PLAYER_MOBILE_FULL_SCREEN } from '../../types'
import {
AdsCloseButton,
Img,
MobileAdWrapper,
Video,
} from './styled'
type MobileAdTypes = {
ad: AdType,
}
export const MobileAd = ({ ad }: MobileAdTypes) => {
const {
link,
media,
position,
} = ad
const { suffix } = useLexicsStore()
const {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
} = useAd({ ad })
const close = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
handleClose()
}
const onLinkClick = () => {
if (link) {
sendBannerClickEvent(EventGA.CLICK)
window.open(link, '_blank')
}
}
return (
position && isOpenAd && isNeedToShow ? (
<MobileAdWrapper
position={position.id}
onClick={onLinkClick}
>
{isOpenCloseBtn
&& (
<AdsCloseButton
position={position.id}
onClick={close}
size={includes(PLAYER_MOBILE_FULL_SCREEN, position.id) ? 12 : 8}
/>
)}
{isVideo
? <Video position={position.id} src={media.url} />
: (
<Img
position={position.id}
src={media.url}
alt={`name_${suffix}`}
/>
)}
</MobileAdWrapper>
) : null
)
}

@ -0,0 +1,111 @@
import styled, { css } from 'styled-components/macro'
import includes from 'lodash/includes'
import { CloseButton } from 'features/PopupComponents'
import {
MATCH_ADS,
PLAYER_MOBILE_FULL_SCREEN,
PLAYER_MOBILE_ADS,
MATCH_PAGE_MOBILE_ADS,
} from '../../types'
type Props = {
position: number,
}
const chooseStyle = (type: number) => {
switch (type) {
case MATCH_ADS.PLAYS_BOTTOM_MOBILE:
return css`
grid-row: 4;
margin-bottom: 12px;
height: 48px;
`
case PLAYER_MOBILE_ADS:
return css`
position: absolute;
width: 92%;
bottom: 50px;
left: 15px;
`
case PLAYER_MOBILE_FULL_SCREEN.VERTICAL_FULL_SCREEN:
case PLAYER_MOBILE_FULL_SCREEN.HORIZONTAL_FULL_SCREEN:
return css`
position: absolute;
top: 0;
left: 0;
height: 100%;
padding: 5px;
background-color: rgba(0, 0, 0, 0.7);
`
default:
return ''
}
}
export const MobileAdWrapper = styled.div<Props>`
position: relative;
width: 100%;
z-index: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '101' : '100')};
${({ position }) => chooseStyle(position)};
`
export const AdsCloseButton = styled(CloseButton)<Props>`
position: absolute;
right: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '-5px' : '-10px')};
top: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '15px' : '10px')};
background: none;
border-radius: 0;
transform: translate(-50%, -50%);
z-index: 2;
color: #9B9B9B;
`
export const Img = styled.img<Props>`
border-radius: 2px;
width: 100%;
object-fit: ${({ position }) => {
switch (true) {
case position === PLAYER_MOBILE_FULL_SCREEN.VERTICAL_FULL_SCREEN:
return 'fill'
case position === MATCH_ADS.PLAYS_TOP_MOBILE:
return 'contain'
default:
return 'cover'
}
}};
height: ${({ position }) => {
switch (true) {
case position === 10:
return '50px'
case includes(MATCH_PAGE_MOBILE_ADS, position):
return '100%'
default:
return '75px'
}
}}
`
export const Video = styled.video<Props>`
max-height: 100%;
object-fit: cover;
min-width: 100%;
height: ${({ position }) => (position === 10 ? '50px' : '75px')};
border-radius: 2px;
height: ${({ position }) => {
switch (true) {
case position === 10:
return '50px'
case position === MATCH_ADS.PLAYS_TOP_MOBILE:
return '48px'
default:
return '75px'
}
}}
`

@ -0,0 +1,16 @@
import type { 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,65 @@
import { useMemo } from 'react'
import { useQuery } from 'react-query'
import { useRecoilState } from 'recoil'
import { isMobileDevice, querieKeys } from 'config'
import { getAds } from 'requests'
import { isMatchPage } from 'helpers/isMatchPage'
import { useLang } from 'features/LexicsStore/hooks/useLang'
import { useAuthStore } from 'features/AuthStore'
import {
DeviceType,
PageType,
} from './types'
import { calcMaxAdDurationAds } from './helpers/calcMaxDurationAds'
import { adsStore } from '../../pages/HighlightsPage/storeHighlightsAtoms'
type Props = {
matchId?: number,
sportType?: number,
tournamentId?: number,
}
export const useAds = ({
matchId,
sportType,
tournamentId,
}: Props) => {
const [ads, setAds] = useRecoilState(adsStore)
const { lang } = useLang()
const { user } = useAuthStore()
useQuery({
enabled: isMatchPage() ? (!!user && !!tournamentId) : !!user,
queryFn: async () => {
const adsList = await getAds({
client_type: isMobileDevice ? DeviceType.MOBILE : DeviceType.WEB,
language: lang,
type_id: isMatchPage() ? PageType.MATCH : PageType.HOME,
...isMatchPage() && {
matches: [{
match_id: matchId,
sport_id: sportType,
}],
tournaments: [{
sport_id: sportType,
tournament_id: tournamentId,
}],
},
})
adsList && setAds(adsList)
return adsList
},
queryKey: [querieKeys.ads, matchId],
staleTime: useMemo(() => Math.max(calcMaxAdDurationAds(ads), 60 * 1000), [ads]),
})
return {
ads,
}
}

@ -0,0 +1,31 @@
import type { AdType } from 'requests'
import { isMobileDevice } from 'config'
import type { AdsPropsType } from './types'
import { AdComponent } from './components/AdComponent'
import { MobileAd } from './components/MobileAd'
import {
HeaderWrapAd,
} from './styled'
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,16 @@
import styled, { css } from 'styled-components/macro'
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: 0 0.71rem;
grid-template-columns: none;
`}}
`

@ -0,0 +1,59 @@
import type { 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',
DISPLAY = 'banner_display'
}
export enum VIEW_ADS {
ROW = 4,
COLUMN = 5,
SQUARE = 6,
SECOND_COLUMN = 2,
SECOND_ROW = 3,
MOBILE_IN_COLLAPSE_HEADER = 12,
MOBILE_IN_COLLAPSE_FOOTER = 25
}
export const HEADER_MOBILE_ADS = [10, 11]
export enum MATCH_ADS {
WATCH_TOP = 13,
PLAYS_TOP = 14,
PLAYS_BOTTOM = 15,
PLAYS_TOP_MOBILE = 16,
PLAYS_BOTTOM_MOBILE = 17,
}
export enum PLAYER_ADS {
LEFT_BOTTOM = 18,
CENTER_BOTTOM = 19,
RIGHT = 20,
FULL_SCREEN = 21,
}
export const PLAYER_MOBILE_ADS = 22
export enum PLAYER_MOBILE_FULL_SCREEN {
VERTICAL_FULL_SCREEN = 23,
HORIZONTAL_FULL_SCREEN = 24,
}
export const MATCH_PAGE_MOBILE_ADS = [16, 17, 22, 23, 24]
export type AdsPropsType = Record<'ads', AdsListType | undefined>
export const adsViews = 'adsViews'

@ -0,0 +1,72 @@
import type { Props, State } from '../types'
import {
createVariableSizingTransformationSet,
createClones,
createDefaultTransformationSet,
getElementDimensions,
getItemsCount,
getItemsOffset,
getTransitionProperty,
getTranslate3dProperty,
getItemsInSlide,
} from './elements'
import { getActiveIndex, getStartIndex } from './math'
export const calculateInitialState = (props: Props, el: HTMLElement | null): State => {
const {
activeIndex: propsActiveIndex = 0,
animationDuration = 0,
infinite = false,
variableSizing = false,
} = props
const clones = createClones(props)
const transition = getTransitionProperty()
const itemsCount = getItemsCount(props)
const itemsOffset = getItemsOffset(props)
const itemsInSlide = getItemsInSlide(itemsCount, props)
const startIndex = getStartIndex(propsActiveIndex, itemsCount)
const activeIndex = getActiveIndex({
infinite,
itemsCount,
startIndex,
})
const { width: listWidth } = getElementDimensions(el)
const transformationSet = variableSizing
? createVariableSizingTransformationSet({
el,
infinite,
listWidth,
props,
}).coords
: createDefaultTransformationSet({
children: clones,
infinite,
itemsInSlide,
listWidth,
props,
}).coords
const translate3d = getTranslate3dProperty(activeIndex, {
infinite,
itemsInSlide,
itemsOffset,
transformationSet,
variableSizing,
})
return {
activeIndex,
animationDuration,
clones,
infinite,
itemsCount,
itemsInSlide,
itemsOffset,
listWidth,
transformationSet,
transition,
translate3d,
variableSizing,
}
}

@ -0,0 +1,274 @@
import type { CSSProperties, ReactNode } from 'react'
import { Children } from 'react'
import type {
Transformations,
ItemCoords,
Props,
State,
Transition,
} from '../types'
import { mapPartialCoords, mapPositionCoords } from './mappers'
import { getShiftIndex } from './math'
export const getSlides = ({ children }: Props) => Children.toArray(children)
export const getItemsCount = (props: Props) => getSlides(props).length
export const getItemsOffset = ({ infinite }: Props) => (infinite ? 1 : 0)
export const createClones = (props: Props) => {
const slides = getSlides(props)
if (!props.infinite) return slides
const itemsCount = getItemsCount(props)
const itemsOffset = getItemsOffset(props)
const itemsInSlide = getItemsInSlide(itemsCount, props)
const cursor = Math.min(itemsInSlide, itemsCount) + itemsOffset
const clonesAfter = slides.slice(0, cursor)
const clonesBefore = slides.slice(-cursor)
if (itemsOffset && itemsInSlide === itemsCount) {
const afterOffsetClone = slides[0]
const [beforeOffsetClone] = slides.slice(-1)
clonesBefore.unshift(beforeOffsetClone)
clonesAfter.push(afterOffsetClone)
}
return clonesBefore.concat(slides, clonesAfter)
}
type CreateVariableSizingTransformationSetArgs = {
el: HTMLElement | null,
infinite: boolean,
listWidth: number,
props: Props,
}
export const createVariableSizingTransformationSet = ({
el,
infinite,
listWidth,
props,
}: CreateVariableSizingTransformationSetArgs) => {
let content = 0
let partial = true
let coords: Array<ItemCoords> = []
const { spaceBetween = 0 } = props
const children: Array<HTMLElement | Element> = Array.from(el?.children || [])
coords = children.reduce<Array<ItemCoords>>((
acc,
child,
i,
) => {
let position = 0
const previewsChildCursor = i - 1
const previewsChild = acc[previewsChildCursor]
const { width = 0 } = getElementDimensions(child?.firstChild as HTMLElement)
content += width + spaceBetween
partial = listWidth >= content
if (previewsChild) {
position = previewsChildCursor === 0
? previewsChild.width + spaceBetween
: previewsChild.width + previewsChild.position + spaceBetween
}
acc.push({ position, width })
return acc
}, [])
if (!infinite) {
if (partial) {
coords = mapPartialCoords(coords)
} else {
const position = content - listWidth
coords = mapPositionCoords(coords, position)
}
}
return {
content,
coords,
partial,
}
}
type CreateDefaultTransformationSetArgs = {
children: Array<ReactNode>,
infinite: boolean,
itemsInSlide: number,
listWidth: number,
props: Props,
}
export const createDefaultTransformationSet = ({
children,
infinite,
itemsInSlide,
listWidth,
props,
}: CreateDefaultTransformationSetArgs): Transformations => {
let content = 0
let partial = true
let coords: Array<ItemCoords> = []
const { spaceBetween = 0 } = props
const width = getItemWidth({
galleryWidth: listWidth,
itemsInSlide,
props,
})
coords = children.reduce<Array<ItemCoords>>((
acc,
_,
i,
) => {
let position = 0
const previewsChild = acc[i - 1]
content += width + spaceBetween
partial = listWidth >= content
if (previewsChild) {
position = width + spaceBetween + previewsChild.position || 0
}
acc.push({ position, width })
return acc
}, [])
if (!infinite) {
if (partial) {
coords = mapPartialCoords(coords)
} else {
const position = content - listWidth
coords = mapPositionCoords(coords, position)
}
}
return {
content,
coords,
partial,
}
}
type GetItemWidthArgs = {
galleryWidth: number,
itemsInSlide: number,
props: Props,
}
export const getItemWidth = ({
galleryWidth,
itemsInSlide,
props: { spaceBetween = 0 },
}: GetItemWidthArgs) => (itemsInSlide > 0
? (galleryWidth - spaceBetween * (itemsInSlide - 1)) / itemsInSlide
: galleryWidth)
export const getElementDimensions = (element: HTMLElement | null) => {
const { height, width } = element?.getBoundingClientRect() || { height: 0, width: 0 }
return { height, width }
}
export const getTransitionProperty = (options?: Transition): string => {
const { animationDuration = 0, animationTimingFunction = 'ease' } = options || {}
return `transform ${animationDuration}ms ${animationTimingFunction} 0ms`
}
export const getListElementStyles = (
{ translate3d }: Partial<State>,
currentStyles: CSSProperties,
): CSSProperties => {
const transform = `translate3d(${-(translate3d || 0)}px, 0, 0)`
return { ...currentStyles, transform }
}
export const getItemStyles = (index: number, state: State): CSSProperties => {
const { transformationSet } = state
const { width } = transformationSet[index] || {}
return {
width,
}
}
export const getTranslate3dProperty = (nextIndex: number, state: Partial<State>) => {
let cursor = nextIndex
const {
infinite,
itemsInSlide = 0,
itemsOffset = 0,
transformationSet = [],
} = state
if (infinite) {
cursor = nextIndex + getShiftIndex(itemsInSlide, itemsOffset)
}
return (transformationSet[cursor] || {}).position || 0
}
export const isDisplayedItem = (i = 0, state: State) => {
const {
activeIndex,
infinite,
itemsInSlide,
itemsOffset,
variableSizing,
} = state
const shiftIndex = getShiftIndex(itemsInSlide, itemsOffset)
if (variableSizing && infinite) {
return i - shiftIndex === activeIndex + itemsOffset
}
const index = activeIndex + shiftIndex
if (!infinite) {
return i >= activeIndex && i < index
}
return i >= index && i < index + itemsInSlide
}
export const getItemsInSlide = (itemsCount: number, props: Props) => {
let itemsInSlide = 1
const {
breakpoints = {},
infinite,
variableSizing,
} = props
if (variableSizing) {
return infinite ? itemsCount : itemsInSlide
}
const configKeys = Object.keys(breakpoints)
configKeys.forEach((key) => {
if (Number(key) <= window.innerWidth) {
const { items, itemsFit = 'fill' } = breakpoints[key]
itemsInSlide = itemsFit === 'contain' ? items : Math.min(items, itemsCount)
}
})
return itemsInSlide || 1
}

@ -0,0 +1,4 @@
export * from './common'
export * from './elements'
export * from './math'
export * from './mappers'

@ -0,0 +1,12 @@
import type { ItemCoords } from '../types'
export const mapPartialCoords = (coords: Array<ItemCoords>) => (
coords.map(({ width }) => ({ position: 0, width }))
)
export const mapPositionCoords = (coords: Array<ItemCoords>, position = 0) => coords.map((item) => {
if (item.position > position) {
return { ...item, position }
}
return item
})

@ -0,0 +1,44 @@
import type { ItemCoords } from '../types'
export const getShiftIndex = (itemsInSlide = 0, itemsOffset = 0) => itemsInSlide + itemsOffset
export const getStartIndex = (index = 0, itemsCount = 0) => {
if (itemsCount) {
if (index >= itemsCount) {
return itemsCount - 1
}
if (index > 0) {
return index
}
}
return 0
}
export const getActiveIndex = ({
infinite = false,
itemsCount = 0,
startIndex = 0,
}) => (infinite ? startIndex : getStartIndex(startIndex, itemsCount))
export const getUpdateSlidePositionIndex = (activeIndex: number, itemsCount: number) => {
if (activeIndex < 0) return itemsCount - 1
if (activeIndex >= itemsCount) return 0
return activeIndex
}
export const shouldRecalculateSlideIndex = (activeIndex: number, itemsCount: number) => (
activeIndex < 0 || activeIndex >= itemsCount
)
export const shouldCancelSlideAnimation = (activeIndex: number, itemsCount: number) => (
activeIndex < 0 || activeIndex >= itemsCount
)
export const getTransformationItemIndex = (
transformationSet: Array<ItemCoords> = [],
position = 0,
) => transformationSet.findIndex((item) => item.position >= Math.abs(position))

@ -0,0 +1,160 @@
import {
useRef,
useEffect,
useLayoutEffect,
type ReactNode,
} from 'react'
import { KEYBOARD_KEYS } from 'config'
import { useEventListener, useObjectState } from 'hooks'
import type { State, Props } from './types'
import * as Utils from './helpers'
import { ListItem } from './styled'
export const useCarousel = (props: Props) => {
const [state, setState] = useObjectState<State>(Utils.calculateInitialState(props, null))
const isAnimationDisabledRef = useRef(false)
const slideEndTimeoutIdRef = useRef<number | null>(null)
const listElementRef = useRef<HTMLUListElement>(null)
const rootElementRef = useRef<HTMLDivElement>(null)
const {
activeIndex,
animationDuration,
clones,
itemsCount,
itemsInSlide,
transition,
translate3d,
} = state
const {
animationTimingFunction,
infinite,
onSlideChange,
useKeyboardNavigation,
} = props
const listElementStyles = Utils.getListElementStyles({ translate3d }, { transition })
const clearSlideEndTimeout = () => {
slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current)
slideEndTimeoutIdRef.current = null
}
const slidePrev = () => {
const newActiveIndex = activeIndex - 1
handleSlideTo(newActiveIndex)
}
const slideNext = () => {
const newActiveIndex = activeIndex + 1
handleSlideTo(newActiveIndex)
}
const handleUpdateSlidePosition = (index: number) => {
const newTranslate3d = Utils.getTranslate3dProperty(index, state)
const newTransition = Utils.getTransitionProperty({ animationDuration: 0 })
setState({
activeIndex: index,
transition: newTransition,
translate3d: newTranslate3d,
})
}
const handleBeforeSlideEnd = (index: number) => {
if (Utils.shouldRecalculateSlideIndex(index, itemsCount)) {
const nextIndex = Utils.getUpdateSlidePositionIndex(index, itemsCount)
handleUpdateSlidePosition(nextIndex)
}
isAnimationDisabledRef.current = false
}
const handleSlideTo = (newActiveIndex = 0) => {
if (
isAnimationDisabledRef.current
|| newActiveIndex === activeIndex
|| (!infinite && Utils.shouldCancelSlideAnimation(newActiveIndex, itemsCount))
) return
isAnimationDisabledRef.current = true
clearSlideEndTimeout()
const newTranslate3d = Utils.getTranslate3dProperty(newActiveIndex, state)
const newTransition = Utils.getTransitionProperty({
animationDuration,
animationTimingFunction,
})
onSlideChange?.(newActiveIndex)
setState({
activeIndex: newActiveIndex,
transition: newTransition,
translate3d: newTranslate3d,
})
slideEndTimeoutIdRef.current = setTimeout(
() => handleBeforeSlideEnd(newActiveIndex),
animationDuration,
)
}
const renderItem = (item: ReactNode, index: number) => {
const styles = Utils.getItemStyles(index, state)
return (
<ListItem
key={index}
style={styles}
aria-hidden={!Utils.isDisplayedItem(index, state)}
>
{item}
</ListItem>
)
}
useEventListener({
callback: (e) => {
if (!useKeyboardNavigation) return
if (e.key === KEYBOARD_KEYS.ArrowLeft) slidePrev()
if (e.key === KEYBOARD_KEYS.ArrowRight) slideNext()
},
event: 'keydown',
})
useLayoutEffect(() => {
const setInitialState = () => {
const initialState = Utils.calculateInitialState(props, listElementRef.current)
setState(initialState)
}
setInitialState()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.activeIndex])
useEffect(() => () => {
slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current)
}, [])
return {
activeIndex,
clones,
itemsCount,
itemsInSlide,
listElementRef,
listElementStyles,
renderItem,
rootElementRef,
slideNext,
slidePrev,
}
}

@ -0,0 +1,86 @@
import { ArrowButton, Arrow } from 'features/HeaderFilters/components/DateFilter/styled'
import type { Props } from './types'
import { useCarousel } from './hooks'
import {
Wrapper,
List,
ButtonsWrapper,
} from './styled'
export * from './types'
type NavButtonProps = {
direction: 'left' | 'right',
disabled?: boolean,
onClick: () => void,
}
const NavButton = ({
direction,
disabled,
onClick,
}: NavButtonProps) => (
<ArrowButton
aria-label={direction === 'left' ? 'Previous' : 'Next'}
disabled={disabled}
onClick={onClick}
>
<Arrow direction={direction} />
</ArrowButton>
)
export const Carousel = (props: Props) => {
const {
infinite,
renderNextButton,
renderPrevButton,
} = props
const {
activeIndex,
clones,
itemsCount,
itemsInSlide,
listElementRef,
listElementStyles,
renderItem,
rootElementRef,
slideNext,
slidePrev,
} = useCarousel(props)
return (
<Wrapper ref={rootElementRef}>
<List ref={listElementRef} style={listElementStyles}>
{clones.map(renderItem)}
</List>
<ButtonsWrapper>
{renderPrevButton
? renderPrevButton({
disabled: !infinite && activeIndex === 0,
onClick: slidePrev,
})
: (
<NavButton
disabled={!infinite && activeIndex === 0}
onClick={slidePrev}
direction='left'
/>
)}
{renderNextButton
? renderNextButton({
disabled: !infinite && itemsInSlide + activeIndex === itemsCount,
onClick: slideNext,
})
: (
<NavButton
disabled={!infinite && itemsInSlide + activeIndex === itemsCount}
onClick={slideNext}
direction='right'
/>
)}
</ButtonsWrapper>
</Wrapper>
)
}

@ -0,0 +1,25 @@
import styled from 'styled-components/macro'
export const Wrapper = styled.div`
width: 100%;
margin: auto;
overflow: hidden;
`
export const List = styled.ul`
display: flex;
gap: 20px;
width: 100%;
height: 100%;
`
export const ListItem = styled.li`
flex-shrink: 0;
height: 100%;
`
export const ButtonsWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
`

@ -0,0 +1,73 @@
import type { ReactNode } from 'react'
type RenderButtonArgs = {
disabled?: boolean,
onClick: () => void,
}
export type Transition = {
animationDuration?: number,
animationTimingFunction?: string,
}
export type Breakpoints = {
[key: string]: {
// Число элементов в слайде
items: number,
// Определяет, как элемент должен заполнять контейнер в соответствии с шириной слайда
itemsFit?: 'contain' | 'fill',
},
}
export type Transformations = {
content: number,
coords: Array<ItemCoords>,
partial: boolean,
}
export type ItemCoords = {
position: number,
width: number,
}
export type Props = {
/** Текущая позиция */
activeIndex?: number,
/** Длительность анимации */
animationDuration?: number,
/** animation-timing-function */
animationTimingFunction?: string,
/** Объект с брэкпойнтами */
breakpoints?: Breakpoints,
/** Элементы карусели */
children: ReactNode,
/** Бесконечный режим прокрутки */
infinite?: boolean,
/** Колбэк при прокрутке */
onSlideChange?: (activeIndex: number) => void,
/** Рендер-функция кнопки прокрутки вперед */
renderNextButton?: (args: RenderButtonArgs) => ReactNode,
/** Рендер-функция кнопки прокрутки назад */
renderPrevButton?: (args: RenderButtonArgs) => ReactNode,
/** Расстояние между элементами карусели */
spaceBetween?: number,
/** Использование клавиатуры для навигации */
useKeyboardNavigation?: boolean,
/** Использование произвольной ширины элементов карусели */
variableSizing?: boolean,
}
export type State = {
activeIndex: number,
animationDuration?: number,
clones: Array<ReactNode>,
infinite?: boolean,
itemsCount: number,
itemsInSlide: number,
itemsOffset: number,
listWidth: number,
transformationSet: Array<ItemCoords>,
transition: string,
translate3d: number,
variableSizing: boolean,
}

@ -31,6 +31,7 @@ export const facr: ClientConfig = {
defaultLanguage: 'cs',
description: 'Live sports streaming platform. All matches playing under the auspices of Czech Republic FA. Access to full matches, various player playlists, and highlights. Free access in the Czech Republic. Available across all devices',
disabledPreferences: false,
host: 'facr.tv',
name: ClientNames.Facr,
privacyLink: '/privacy-policy-and-statement',
requests: {

@ -17,6 +17,7 @@ export const fqtv: ClientConfig = {
defaultLanguage: 'en',
description: 'Queensland’s streamed competitions, including NPL Men, NPL Women and McDonald\'s FQPL Leagues.',
disabledPreferences: true,
host: 'fqtv.com.au',
name: ClientNames.Fqtv,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,

@ -17,5 +17,7 @@ export const india: ClientConfig = {
sign: 'Rupee',
},
disabledHighlights: true,
host: 'india.insports.tv',
name: ClientNames.India,
userAccountCardsHidden: true,
}

@ -1,4 +1,5 @@
import { css } from 'styled-components/macro'
import {
ClientConfig,
ClientIds,
@ -17,6 +18,7 @@ export const insports: ClientConfig = {
defaultLanguage: 'en',
description: 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.',
disabledPreferences: true,
host: 'insports.tv',
name: ClientNames.Insports,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,

@ -17,6 +17,7 @@ export const instat: ClientConfig = {
defaultLanguage: 'en',
description: 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.',
disabledPreferences: true,
host: 'instat.tv',
name: ClientNames.Instat,
privacyLink: '/privacy-policy-and-statement',
showSearch: true,

@ -18,6 +18,7 @@ export const lff: ClientConfig = {
description: 'Latvijas Futbola federācija (LFF) pašmāju futbola spēļu tiešraižu, apskatu un ierakstu platforma.',
disabledHighlights: true,
disabledPreferences: true,
host: 'tv.lff.lv',
name: ClientNames.Lff,
privacyLink: '/privacy-policy-and-statement',
showSearch: true,

@ -25,6 +25,7 @@ export const tunisia: ClientConfig = {
disabledFilters: true,
disabledHighlights: true,
disabledPreferences: true,
host: 'diwansport.net',
name: ClientNames.Tunisia,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,
@ -60,4 +61,5 @@ export const tunisia: ClientConfig = {
},
termsLink: '/terms-and-conditions?client_id=insports-ott-web',
title: 'Diwan Sport - The home of Tunisian Ligue Professionnelle 1',
userAccountCardsHidden: true,
}

@ -42,6 +42,7 @@ export type ClientConfig = {
disabledFilters?: boolean,
disabledHighlights?: boolean,
disabledPreferences?: boolean,
host: string,
name: ClientNames,
privacyLink: string,
requests?: Record<ProcedureName, RequestParameters>,
@ -64,5 +65,6 @@ export type ClientConfig = {
},
termsLink: string,
title: string,
userAccountCardsHidden?: boolean,
userAccountLinksDisabled?: boolean,
}

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

@ -90,11 +90,16 @@ const confirmPopup = {
const buyMatchPopupLexics = {
add: 15075,
adding_card: 15074,
all_away_games_of: 20186,
all_games_of: 20182,
all_home_games_of: 20185,
auto_renewal: 16624,
best_choice: 20175,
buy_for: 14095,
buy_subscription: 13565,
cancel_anytime: 20171,
change_card: 13564,
choose_subscription: 13563,
choose_subscription: 20174,
completed: 14072,
description_all_season_matches: 15069,
description_all_team_matches: 15070,
@ -105,7 +110,11 @@ const buyMatchPopupLexics = {
for_month: 13561,
for_view: 15064,
for_year: 13562,
how_to_watch: 20199,
in: 20183,
in_season: 20184,
next_choose: 15156,
one_off_payment: 20172,
pass_league: 15065,
pass_match_access: 15067,
pass_team: 15066,
@ -114,8 +123,10 @@ const buyMatchPopupLexics = {
pay: 15073,
payment: 14096,
payment_confirmation: 14094,
per_month: 13573,
per_month: 20173,
per_year: 13574,
purchase: 12623,
subscription: 15604,
subscription_done: 2668,
success_subscription: 14097,
}

@ -3,6 +3,7 @@ export const landingLexics = {
inactive_button: 20083,
inactive_description_1: 20084,
inactive_description_2: 20086,
inactive_period: 801,
inactive_title_1: 20087,
inactive_title_2: 20088,
}

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

@ -7,6 +7,7 @@ export const PAGES = {
mailings: '/useraccount/mailings',
match: '/matches',
player: '/players',
subscriptions: '/subscriptions',
team: '/teams',
thanksForSubscribe: '/thanks-for-subscription',
tournament: '/tournaments',

@ -3,7 +3,7 @@ import { ClientNames } from './clients/types'
export enum PaymentSystem {
PagBrazil = 'pag_brasil',
Paymee = 'paymee',
Paytm = 'paytm',
PhonePe = 'phonePe',
Stripe = 'stripe'
}
@ -20,7 +20,7 @@ type PaymentsType = {
export const payments: PaymentsType = {
[ClientNames.Tunisia]: PaymentSystem.Paymee,
[ClientNames.Brasil]: PaymentSystem.PagBrazil,
[ClientNames.India]: PaymentSystem.Paytm,
[ClientNames.India]: PaymentSystem.PhonePe,
[ClientNames.Insports]: PaymentSystem.Stripe,
[ClientNames.Instat]: PaymentSystem.Stripe,
[ClientNames.Facr]: PaymentSystem.Stripe,

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

@ -5,15 +5,15 @@ import { ENV, isProduction } from './env'
export const APIS = {
preproduction: {
api: 'https://api.insports.tv',
auth: 'https://auth.insports.tv',
auth: 'https://api.auth.insports.tv',
},
production: {
api: 'https://api.insports.tv',
auth: 'https://auth.insports.tv',
auth: 'https://api.auth.insports.tv',
},
staging: {
api: 'https://api.test.insports.tv',
auth: 'https://auth.test.insports.tv',
auth: 'https://api.auth.test.insports.tv',
},
}

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

@ -9,9 +9,12 @@ import {
import { RecoilRoot } from 'recoil'
import { indexLexics } from 'config/lexics/indexLexics'
import { isProduction } from 'config/env'
import { PAGES } from 'config/pages'
import { client } from 'config/clients'
import {
client,
PAGES,
isProduction,
isMobileDevice,
} from 'config'
import { StripeElements } from 'features/StripeElements'
import { useLexicsConfig } from 'features/LexicsStore'
@ -19,7 +22,7 @@ import { ExtendedSearchStore } from 'features/ExtendedSearchPage'
import { MatchSwitchesStore } from 'features/MatchSwitches'
import { UserFavoritesStore } from 'features/UserFavorites/store'
import { MatchPopup, MatchPopupStore } from 'features/MatchPopup'
import { BuyMatchPopup, BuyMatchPopupStore } from 'features/BuyMatchPopup'
import { BuyMatchPopupStore } from 'features/BuyMatchPopup'
import { PreferencesPopup, PreferencesPopupStore } from 'features/PreferencesPopup'
import { TournamentsPopup } from 'features/TournamentsPopup'
import { TournamentPopupStore } from 'features/TournamentsPopup/store'
@ -41,6 +44,7 @@ const HighlightsPage = lazy(() => import('pages/HighlightsPage'))
const ThanksPage = lazy(() => import('pages/ThanksPage'))
const Mailings = lazy(() => import('pages/Mailings'))
const FailedPaymeePage = lazy(() => import('pages/FailedPaymeePage'))
const SubscriptionsPage = lazy(() => import('pages/SubscriptionsPage'))
export const AuthenticatedApp = () => {
useSportList()
@ -59,7 +63,6 @@ export const AuthenticatedApp = () => {
<BuyMatchPopupStore>
<NoNetworkPopupStore>
<MatchPopup />
<BuyMatchPopup />
{ client.name === 'facr' ? <TournamentsPopup /> : <PreferencesPopup /> }
<NoNetworkPopup />
{/* в Switch как прямой children
@ -98,6 +101,11 @@ export const AuthenticatedApp = () => {
<Route path={`${PAGES.landing}`}>
<Landings />
</Route>
{isMobileDevice && (
<Route path={PAGES.subscriptions}>
<SubscriptionsPage />
</Route>
)}
<Redirect to={PAGES.home} />
</Switch>
{!isProduction && <SystemSettings />}

@ -8,12 +8,14 @@ import { useHistory } from 'react-router'
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields'
import { changePassword } from 'features/AuthServiceApp/requests/changePassword'
import { clients } from 'features/AuthServiceApp/config/clients'
import { useParamsUrl } from '../../hooks/useParamsUrl'
export const useChangePasswordForm = () => {
const { client_id } = useParamsUrl()
const { client_id, redirect_uri } = useParamsUrl()
const history = useHistory()
const [error, setError] = useState('')
const [modalOpen, setModalOpen] = useState<boolean>(false)
const [isFetching, setIsFetching] = useState(false)
@ -36,9 +38,12 @@ export const useChangePasswordForm = () => {
setIsFetching(false)
}
}
const host = String(redirect_uri || clients[client_id].host)
const redirectUrl = `/authorize?client_id=${client_id}&redirect_uri=${encodeURIComponent(host)}`
const modalButtonClick = () => {
history.push(`/authorize?client_id=${client_id}`)
history.push(redirectUrl)
}
const onPasswordChange = ({

@ -8,7 +8,7 @@ import { india } from './india'
import { tunisia } from './tunisia'
import { fqtv } from './fqtv'
const clients = {
export const clients = {
[ClientIds.Facr]: facr,
[ClientIds.Fqtv]: fqtv,
[ClientIds.Instat]: instat,

@ -12,6 +12,7 @@ export type ClientConfig = {
background: FC<{ children: ReactNode }>,
defaultLanguage: string,
description: string,
host?: string,
isHideSelectLanguages?: boolean,
name: ClientNames,
privacyLink: string,

@ -51,7 +51,7 @@ export const CardStep = ({
return (
<Wrapper width={642}>
<Header>
<ButtonPrevious onClick={goBack}>
<ButtonPrevious aria-label='Back' onClick={goBack}>
{isHighlightsPage ? '' : <Arrow direction='left' />}
</ButtonPrevious>
<HeaderTitle>

@ -1,3 +1,7 @@
import { useHistory } from 'react-router-dom'
import { isMobileDevice } from 'config'
import { T9n } from 'features/T9n'
import { Header, HeaderTitle } from 'features/PopupComponents'
@ -16,6 +20,13 @@ export const ErrorStep = () => {
error: paymentError,
} = useBuyMatchPopupStore()
const history = useHistory()
const handleButtonclick = () => {
close()
isMobileDevice && history.goBack()
}
return (
<Wrapper width={369}>
<Header height={24}>
@ -29,7 +40,7 @@ export const ErrorStep = () => {
</ResultText>
</Body>
<Footer>
<SmallButton onClick={close}>
<SmallButton onClick={handleButtonclick}>
Ок
</SmallButton>
</Footer>

@ -6,19 +6,23 @@ import {
useState,
} from 'react'
import isNumber from 'lodash/isNumber'
import { PAGES, ProfileTypes } from 'config'
import { ClientNames } from 'config/clients/types'
import { payments, PaymentSystem } from 'config/payments'
import isNumber from 'lodash/isNumber'
import { useLexicsStore } from 'features/LexicsStore'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
import { getProfileUrl } from 'features/ProfileLink/helpers'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { getMatchInfo } from 'requests/getMatchInfo'
import { SubscriptionAction, getPaymentUrl } from 'requests/getPaymentUrl'
import {
getPaymentOTTUrl,
getPaymentPayUrl,
getMatchInfo,
SubscriptionAction,
} from 'requests'
import { redirectToUrl } from 'helpers'
@ -88,7 +92,7 @@ export const useIframePayment = ({
}
}, [close, error, id, matchLink, setIsOpenIframe, sportType])
const paymentRequest = async () => {
const paymentRequestOTT = async () => {
let url_cancel
let url_return
let action: SubscriptionAction
@ -106,7 +110,7 @@ export const useIframePayment = ({
break
}
const payment: ResponsePaymentArray = await getPaymentUrl({
const payment: ResponsePaymentArray = await getPaymentOTTUrl({
action,
item: originalObject,
product_name: `${pack} ${teams}`,
@ -117,6 +121,19 @@ export const useIframePayment = ({
setSrc(payment?.url || '')
}
// новое апи для оплаты, в будущем все платежки переедут на него
// делаем оплату на новой вкладке, а не через iframe
const paymentRequestPay = async () => {
const payment = await getPaymentPayUrl({
item: {
...originalObject,
},
url_return: `${window.location.origin}${matchLink}`,
})
redirectToUrl(payment.url)
}
if (paymentSystem === payments[ClientNames.Brasil]) {
// eslint-disable-next-line
window.onmessage = function (event) {
@ -126,6 +143,7 @@ export const useIframePayment = ({
}
}
// отслеживание оплаты для Paymee
useEffect(() => {
let interval: ReturnType<typeof setInterval>
let timeout: ReturnType<typeof setTimeout>
@ -164,7 +182,14 @@ export const useIframePayment = ({
if (open) {
(async () => {
try {
await paymentRequest()
switch (paymentSystem) {
case PaymentSystem.PhonePe:
await paymentRequestPay()
break
default:
await paymentRequestOTT()
break
}
} catch (err) {
setError('error_payment_unsuccessful')
}

@ -0,0 +1,94 @@
import { useState, type MouseEvent } from 'react'
import { useToggle } from 'hooks'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { Price } from 'features/Price'
import { T9n } from 'features/T9n'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
import { ArrowLoader } from 'features/ArrowLoader'
import { usePackage } from '../RegularPackage/usePackage'
import {
Wrapper,
Header,
Title,
Button,
Content,
Description,
Body,
Border,
SubscriptionTypeText,
BestChoice,
} from './styled'
type Props = {
buttonId?: string,
buttonLexic?: string,
matchPackage: MatchPackage,
onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void,
}
export const PackageMobile = ({
buttonId,
buttonLexic,
matchPackage,
onButtonClick,
}: Props) => {
const {
matchPackages,
setSelectedPackage,
} = useBuyMatchPopupStore()
const isSinglePackage = matchPackages.length === 1
const { isOpen, toggle } = useToggle(isSinglePackage)
const [loader, setLoader] = useState(false)
const { firstDescription, priceTextTopLexic } = usePackage(matchPackage)
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
setSelectedPackage(matchPackage)
onButtonClick?.(e, matchPackage)
setLoader(true)
}
return (
<Wrapper>
<Header
isOpen={isOpen}
onClick={isSinglePackage ? undefined : toggle}
>
<Title t={matchPackage.originalObject.sub.lexic1} />
<Price
amount={matchPackage.originalObject.price}
currency={matchPackage.currency}
perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined}
/>
</Header>
{isOpen && <Border />}
<Content isOpen={isOpen}>
<Body>
<Description>
{firstDescription}
</Description>
<Description>
<T9n t={matchPackage.originalObject.sub.lexic3} />
</Description>
{matchPackage.isMainPackage && (
<BestChoice>
<T9n t='best_choice' />
</BestChoice>
)}
<SubscriptionTypeText t={priceTextTopLexic} />
</Body>
<Button id={buttonId} onClick={handleButtonClick}>
{loader
? <ArrowLoader disabled />
: <T9n t={buttonLexic || ''} />}
</Button>
</Content>
</Wrapper>
)
}

@ -0,0 +1,125 @@
import styled, { css } from 'styled-components/macro'
import { T9n } from 'features/T9n'
import { ButtonSolid } from 'features/Common'
import { PriceDetails, PriceAmount } from 'features/Price/styled'
export const Wrapper = styled.div`
width: 100%;
border-radius: 2px 2px 0px 0px;
background-color: #414141;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
`
type HeaderProps = {
isOpen?: boolean,
}
export const Header = styled.div<HeaderProps>`
position: relative;
display: flex;
justify-content: space-between;
height: 52px;
align-items: center;
padding: 0 15px 0 20px;
${PriceAmount} {
font-size: 24px;
position: absolute;
right: 46px;
top: 50%;
translate: 0 -50%;
}
${PriceDetails} {
position: absolute;
top: 12px;
left: calc(100% - 48px);
}
${({ isOpen }) => (isOpen
? ''
: css`
${PriceDetails}, ${PriceAmount}, ${Title} {
color: #D9D9D9;
}
`)}
`
export const Border = styled.div`
height: 0.66px;
background: linear-gradient(90deg, transparent 0%, #656565 50%, transparent 100%);
`
export const Title = styled(T9n)`
max-width: calc(100% - 112px);
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.colors.white};
overflow: hidden;
`
type ContentProps = {
isOpen?: boolean,
}
export const Content = styled.div<ContentProps>`
height: ${({ isOpen }) => (isOpen ? 'auto' : 0)};
`
export const Body = styled.div`
position: relative;
padding: 8px 24px 30px;
background-color: #414141;
`
export const Description = styled.p`
width: calc(100% - 90px);
margin-bottom: 15px;
font-weight: 500;
font-size: 12px;
line-height: 16px;
color: #C7C7C7;
:empty, :has(span:empty) {
display: none;
}
:last-of-type {
margin-bottom: 0;
}
`
export const Button = styled(ButtonSolid)`
width: 100%;
height: 48px;
`
export const SubscriptionTypeText = styled(T9n)`
position: absolute;
right: 16px;
bottom: 12px;
font-size: 12px;
font-weight: 600;
color: ${({ theme }) => theme.colors.white};
`
export const BestChoice = styled.div`
position: absolute;
bottom: 40px;
right: 0;
display: flex;
align-items: center;
justify-content: flex-end;
width: 108px;
height: 20px;
padding-right: 16px;
border-radius: 1px 0px 0px 1px;
font-weight: 600;
font-size: 12px;
font-variant: all-small-caps;
letter-spacing: 0.03em;
color: #464646;
background: linear-gradient(270deg, rgba(253, 253, 254, 0.8) 0%, rgba(253, 253, 254, 0) 97.42%);
backdrop-filter: blur(3px);
`

@ -1,26 +1,34 @@
import {
Fragment,
useEffect,
useState,
MouseEvent,
useMemo,
} from 'react'
import { useHistory } from 'react-router-dom'
import isNull from 'lodash/isNull'
import { MDASH } from 'config'
import { payments, CountryCode } from 'config/payments'
import { client } from 'config/clients'
import {
isMobileDevice,
client,
payments,
CountryCode,
} from 'config'
import { CountryCodeType, getCountryCode } from 'requests/getCountryCode'
import type { CountryCodeType } from 'requests/getCountryCode'
import { getCountryCode } from 'requests/getCountryCode'
import { CloseButton, HeaderActions } from 'features/PopupComponents'
import { T9n } from 'features/T9n'
import { Name } from 'features/Name'
import { useCardsStore } from 'features/CardsStore'
import { ArrowLoader } from 'features/ArrowLoader'
import { Arrow } from 'features/HeaderFilters/components/DateFilter/styled'
import { useAuthStore } from 'features/AuthStore'
import { Arrow } from 'features/HeaderFilters/components/DateFilter/styled'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { ClientNames } from 'config/clients/types'
import { ChooseSub, Footer } from './styled'
import { IframePayment } from '../IframePayment'
@ -30,11 +38,9 @@ import { Packages } from '../Packages'
import {
Wrapper,
Body,
Footer,
Button,
ButtonPrevious,
Header,
HeaderTitle,
ButtonPrevious,
} from '../../styled'
export const PackageSelectionStep = () => {
@ -53,18 +59,29 @@ export const PackageSelectionStep = () => {
const {
close,
disabledBuyBtn,
goBack,
hasPreviousStep,
lastSelectedPackage,
loader,
match,
matchPackages,
onBuyClick,
selectedPackage,
selectedSubscription,
setDisabledBuyBtn,
setLastSelectedPackage,
} = useBuyMatchPopupStore()
const { defaultCard } = useCardsStore()
const history = useHistory()
const hasCard = Boolean(defaultCard)
const buttonId = hasCard ? 'purchase_buy' : 'purchase_next'
const buttonLexic = hasCard ? 'buy_subscription' : 'next_choose'
const hasOnlyOneSubscription = matchPackages.length === 1
const titleLexic = hasOnlyOneSubscription ? 'how_to_watch' : 'choose_subscription'
useEffect(() => {
getUserCountry()
if (isNull(cards)) {
@ -73,20 +90,25 @@ export const PackageSelectionStep = () => {
}, [cards, fetchCards])
const paymentSystem = useMemo(() => {
switch (countryCode?.country_code) {
case CountryCode.BR:
switch (true) {
case countryCode?.country_code === CountryCode.BR:
return payments.brasil
case CountryCode.TN:
case countryCode?.country_code === CountryCode.TN:
return payments.tunisia
case countryCode?.country_code === CountryCode.IN:
case client.name === ClientNames.India:
return payments.india
default:
return payments[client.name]
}
}, [countryCode])
const isIframePayment = useMemo(() => {
switch (countryCode?.country_code) {
case CountryCode.BR:
case CountryCode.TN:
switch (true) {
case countryCode?.country_code === CountryCode.BR:
case countryCode?.country_code === CountryCode.TN:
case countryCode?.country_code === CountryCode.IN:
case client.name === ClientNames.India:
return true
default:
return false
@ -99,65 +121,77 @@ export const PackageSelectionStep = () => {
getCountryCode().then(setCountryCode)
}
const onHandleClick = (e?: MouseEvent<HTMLButtonElement>) => {
cards?.length
&& lastSelectedPackage === selectedPackage?.id
&& setDisabledBuyBtn(true)
if (isIframePayment) {
setIsOpenIframe(true)
const onHandleClick = (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => {
if (user) {
cards?.length
&& lastSelectedPackage === selectedPackage?.id
&& setDisabledBuyBtn(true)
if (isIframePayment) {
setIsOpenIframe(true)
} else {
onBuyClick(e, matchPackage)
}
setLastSelectedPackage(selectedPackage?.id || '')
} else {
onBuyClick(e)
setSearch(window.location.search)
logout('saveToken')
}
setLastSelectedPackage(selectedPackage?.id || '')
}
return (
<Wrapper>
if (isMobileDevice) {
const handleBackClick = () => {
close()
history.goBack()
}
return (
<Wrapper padding='0 16px'>
<Header>
<ButtonPrevious aria-label='Back' onClick={handleBackClick}>
<Arrow direction='left' />
</ButtonPrevious>
<ChooseSub>
<T9n t={titleLexic} />
</ChooseSub>
</Header>
<Body>
<Packages
onButtonClick={onHandleClick}
buttonId={buttonId}
buttonLexic={buttonLexic}
/>
</Body>
</Wrapper>
)
}
return (
<Wrapper
width={hasOnlyOneSubscription ? 624 : undefined}
padding={hasOnlyOneSubscription ? '60px' : '60px 80px'}
>
<Header>
{hasPreviousStep && (
<HeaderActions position='left'>
<ButtonPrevious onClick={goBack}>
<Arrow direction='left' />
</ButtonPrevious>
</HeaderActions>
)}
<HeaderTitle>
{hasPreviousStep && selectedSubscription ? (
<T9n t={selectedSubscription?.lexic} />
) : (
<Fragment>
<Name nameObj={match.team1} />
{` ${MDASH} `}
<Name nameObj={match.team2} />
</Fragment>
)}
</HeaderTitle>
<ChooseSub>
<T9n t={titleLexic} />
</ChooseSub>
<HeaderActions position='right'>
<CloseButton onClick={close} />
</HeaderActions>
</Header>
<Body marginTop={20}>
<Body marginTop={40}>
<Packages />
{!isIframePayment && <SelectedCard />}
</Body>
<Footer>
<Footer hasCard={hasCard}>
{!isIframePayment && <SelectedCard />}
<Button
disabled={!selectedPackage || disabledBuyBtn}
onClick={(e) => {
if (user) {
onHandleClick(e)
} else {
setSearch(window.location.search)
logout('saveToken')
}
}}
id='purchase_buy'
onClick={onHandleClick}
id={buttonId}
>
{loader ? (
<ArrowLoader disabled />
) : (
<T9n t='buy_subscription' />
<T9n t={buttonLexic} />
)}
</Button>
</Footer>

@ -0,0 +1,22 @@
import styled from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { Footer as FooterBase } from 'features/BuyMatchPopup/styled'
export const ChooseSub = styled.div`
font-weight: 700;
font-size: ${isMobileDevice ? 16 : 24}px;
margin: auto;
color: ${({ theme }) => theme.colors.white};
`
type FooterProps = {
hasCard?: boolean,
}
export const Footer = styled(FooterBase)<FooterProps>`
flex-direction: column;
align-items: center;
margin-top: ${({ hasCard }) => (hasCard ? 20 : 40)}px;
`

@ -1,41 +1,181 @@
import styled from 'styled-components/macro'
import type { MouseEvent } from 'react'
import { useMemo } from 'react'
import { PaymentPeriodTabs } from 'features/PaymentPeriodTabs'
import reduce from 'lodash/reduce'
import sortBy from 'lodash/sortBy'
import without from 'lodash/without'
import { isMobileDevice } from 'config'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { useLexicsConfig } from 'features/LexicsStore'
import { Carousel, type Breakpoints } from 'components/Carousel'
import { useBuyMatchPopupStore } from '../../store'
import { PackagesList } from '../PackagesList'
const Wrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
`
export const Packages = () => {
const {
onPackageSelect,
onPeriodSelect,
selectedPackage,
selectedPeriod,
selectedSubscription,
subscriptions,
} = useBuyMatchPopupStore()
if (!selectedSubscription) return null
import { RegularPackage } from '../RegularPackage'
import { PackageMobile } from '../PackageMobile'
import {
Wrapper,
List,
PrevButton,
NextButton,
Arrow,
ListItem,
} from './styled'
import { SinglePackage } from '../SinglePackage'
const breakpoints: Breakpoints = {
1000: { items: 3 },
1280: { items: 4 },
1400: { items: 5 },
}
type ButtonProps = {
disabled?: boolean,
onClick: () => void,
}
const renderPrevButton = ({ disabled, onClick }: ButtonProps) => (
<PrevButton
aria-label='Previous'
disabled={disabled}
onClick={onClick}
>
<Arrow direction='left' />
</PrevButton>
)
const renderNextButton = ({ disabled, onClick }: ButtonProps) => (
<NextButton
aria-label='Next'
disabled={disabled}
onClick={onClick}
>
<Arrow direction='right' />
</NextButton>
)
type Props = {
buttonId?: string,
buttonLexic?: string,
onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void,
}
export const Packages = ({
buttonId,
buttonLexic,
onButtonClick,
}: Props) => {
const { matchPackages } = useBuyMatchPopupStore()
const hasOnlyOneSubscription = matchPackages.length === 1
const hasMoreThanFiveSubscriptions = matchPackages.length > 5
const getSortedPackages = () => {
let temp = sortBy(matchPackages, 'order')
const mainPackage = matchPackages.find(({ isMainPackage }) => isMainPackage)
if (mainPackage && matchPackages.length > 1) {
const index = matchPackages.length >= 2 && matchPackages.length < 4 ? 1 : 2
temp = without(temp, mainPackage)
temp.splice(
index,
0,
mainPackage,
)
}
return temp
}
const sortedPackages = getSortedPackages()
const lexicsIds = useMemo(
() => [...reduce<MatchPackage, Set<number>>(
matchPackages,
(acc, { originalObject: { sub } }) => {
acc.add(sub.lexic1)
sub.lexic2 && acc.add(sub.lexic2)
acc.add(sub.lexic3)
return acc
},
new Set(),
)],
[matchPackages],
)
useLexicsConfig(lexicsIds)
if (isMobileDevice) {
return (
<Wrapper>
<List>
{sortedPackages.map((matchPackage) => (
<ListItem key={matchPackage.id}>
<PackageMobile
matchPackage={matchPackage}
onButtonClick={onButtonClick}
buttonId={buttonId}
buttonLexic={buttonLexic}
/>
</ListItem>
))}
</List>
</Wrapper>
)
}
if (hasOnlyOneSubscription) {
return (
<SinglePackage matchPackage={matchPackages[0]} />
)
}
const getWrapperWidth = () => {
switch (true) {
case !hasMoreThanFiveSubscriptions:
return undefined
case window.innerWidth < 1500:
return window.innerWidth - 160
default:
return 1380
}
}
const wrapperWidth = getWrapperWidth()
return (
<Wrapper>
<PaymentPeriodTabs
onPeriodSelect={onPeriodSelect}
selectedPeriod={selectedPeriod}
selectedSubscription={selectedSubscription}
/>
<PackagesList
packages={subscriptions}
selectedPackage={selectedPackage}
onSelect={onPackageSelect}
/>
<Wrapper width={wrapperWidth}>
{hasMoreThanFiveSubscriptions
? (
<Carousel
animationDuration={400}
infinite
useKeyboardNavigation
breakpoints={breakpoints}
spaceBetween={20}
renderPrevButton={renderPrevButton}
renderNextButton={renderNextButton}
>
{sortedPackages.map((matchPackage) => (
<RegularPackage matchPackage={matchPackage} />
))}
</Carousel>
)
: (
<List>
{sortedPackages.map((matchPackage) => (
<ListItem key={matchPackage.id}>
<RegularPackage matchPackage={matchPackage} />
</ListItem>
))}
</List>
)}
</Wrapper>
)
}

@ -0,0 +1,65 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { ArrowButton, Arrow as ArrowBase } from 'features/HeaderFilters/components/DateFilter/styled'
type WrapperProps = {
width?: number,
}
export const Wrapper = styled.div<WrapperProps>`
width: ${({ width }) => (width ? `${width}px` : 'auto')};
margin: auto;
`
export const List = styled.ul`
display: flex;
gap: 20px;
${isMobileDevice
? css`
display: initial;
max-height: calc(100vh - 140px);
overflow-y: auto;
`
: ''}
`
export const ListItem = styled.li`
width: 260px;
${isMobileDevice
? css`
width: 100%;
margin-bottom: 12px;
border-radius: 2px;
overflow: hidden;
`
: ''}
`
const NavButton = styled(ArrowButton)`
position: absolute;
top: 50%;
translate: 0 -50%;
${({ disabled }) => (disabled
? css`
opacity: 0.5;
`
: '')}
`
export const PrevButton = styled(NavButton)`
left: 30px;
`
export const NextButton = styled(NavButton)`
right: 30px;
`
export const Arrow = styled(ArrowBase)`
width: 20px;
height: 20px;
`

@ -1,84 +0,0 @@
import isNumber from 'lodash/isNumber'
import map from 'lodash/map'
import { T9n } from 'features/T9n'
import { MatchPackage, SubscriptionType } from 'features/BuyMatchPopup/types'
import {
Pass,
Name,
Description,
InfoWrapper,
Item,
List,
Price,
ScAutoRenewal,
ScPriceContainer,
} from './styled'
type Props = {
onSelect: (subscription: MatchPackage) => void,
packages: Array<MatchPackage>,
selectedPackage: MatchPackage | null,
}
export const PackagesList = ({
onSelect,
packages,
selectedPackage,
}: Props) => (
<List>
{
map(
packages,
(subPackage) => (
<Item
key={subPackage.id}
onClick={() => onSelect(subPackage)}
active={subPackage === selectedPackage}
>
<InfoWrapper>
<Pass>
<T9n t={subPackage.pass} />
</Pass>
<Name>
{
isNumber(subPackage.nameLexic)
? <T9n t={subPackage.nameLexic} />
: subPackage.name
}
</Name>
<Description>
<T9n
t={subPackage.description.lexic}
values={subPackage.description.values}
/>
</Description>
</InfoWrapper>
<ScPriceContainer>
<Price
amount={subPackage.price}
currency={subPackage.currency}
perPeriod={
subPackage.type !== SubscriptionType.Month
? null
: `per_${subPackage.type}`
}
/>
{
subPackage.type === SubscriptionType.Month
&& (
<ScAutoRenewal>
<T9n
t='auto_renewal'
/>
</ScAutoRenewal>
)
}
</ScPriceContainer>
</Item>
),
)
}
</List>
)

@ -1,230 +0,0 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { popupScrollbarStyles } from 'features/PopupComponents'
import { Price as BasePrice } from 'features/Price'
import {
PriceAmount,
PriceDetails,
Period,
} from 'features/Price/styled'
export const List = styled.ul`
width: 100%;
height: 460px;
overflow-y: auto;
margin-top: 25px;
padding: 0 40px;
${popupScrollbarStyles}
@media (max-width: 1370px) {
max-height: 415px;
height: auto;
}
@media (max-height: 768px) {
max-height: 280px;
}
@media (max-height: 500px) {
max-height: 140px;
}
${isMobileDevice
? css`
padding: 0;
margin-top: 19px;
@media screen and (orientation: landscape){
margin-top: 10px;
}
`
: ''};
`
type ItemProps = {
active?: boolean,
}
export const Item = styled.li.attrs(() => ({
tabIndex: 0,
}))<ItemProps>`
width: 100%;
min-height: 140px;
padding: 20px 30px 20px 20px;
background: ${({ theme }) => theme.colors.packageBackground};
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
border-radius: 2px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
:not(:last-child) {
margin-bottom: 20px;
}
${({ active, theme: { colors } }) => (
active ? `background-color: ${colors.button}` : ''
)};
@media (max-width: 1370px) {
min-height: 125px;
}
@media (max-width: 750px) {
height: 100%;
}
${isMobileDevice
? css`
padding: 5px 10px;
@media (max-width: 750px) {
min-height: 52.29px;
height: auto;
}
:not(:last-child) {
margin-bottom: 10px;
@media screen and (orientation: landscape) {
margin-bottom: 5.24px;
}
}
` : ''};
@media (max-width: 850px) and (orientation: landscape){
min-height: 52.29px;
height: auto;
}
`
export const InfoWrapper = styled.div`
width: 75%;
display: flex;
flex-direction: column;
align-self: flex-start;
${isMobileDevice
? css`
align-self: center;
@media (max-width: 850px) and (orientation: landscape){
height: 100%;
}
`
: ''};
`
export const Name = styled.span`
font-weight: 500;
font-size: 20px;
line-height: 23px;
letter-spacing: 0.03em;
@media (max-width: 1370px) {
line-height: 20px;
}
${isMobileDevice
? css`
@media (max-width: 750px){
font-size: 12px;
line-height: 10.04px;
}
@media screen and (orientation: landscape){
font-size: 14px;
}
`
: ''};
`
export const Pass = styled(Name)`
font-weight: 600;
text-transform: uppercase;
${isMobileDevice
? css`
line-height: 12px;
@media screen and (orientation: landscape){
line-height: 14px;
}
`
: ''};
`
export const Description = styled.span`
width: 68%;
margin-top: 13px;
font-weight: 500;
font-size: 15px;
line-height: 20px;
letter-spacing: 0.03em;
@media (max-width: 1370px) {
line-height: 18px;
}
${isMobileDevice
? css`
@media (max-width: 750px){
font-size: 8px;
line-height: 8px;
margin-top: 5px;
width: 100%;
}
@media (max-width: 850px) and (orientation: landscape){
margin-top: 0;
line-height: 8px;
font-size: 10px;
}
`
: ''};
`
export const Price = styled(BasePrice)`
${PriceAmount} {
font-size: 24px;
line-height: 24px;
font-weight: normal;
${isMobileDevice
? css`
font-size: 14px;
`
: ''};
}
${PriceDetails} {
font-weight: 500;
font-size: 12px;
line-height: 18px;
${isMobileDevice
? css`
font-size: 8px;
`
: ''};
}
${Period} {
text-transform: capitalize;
}
`
export const ScAutoRenewal = styled(PriceDetails)`
line-height: 21px;
font-size: 12px;
text-transform: none;
margin: 0;
color: ${({ theme: { colors } }) => colors.white70};
${isMobileDevice
? css`
line-height: normal;
font-size: 10px;
text-align: center;
`
: ''};
`
export const ScPriceContainer = styled.div`
display: flex;
flex-direction: column;
`

@ -0,0 +1,74 @@
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { T9n } from 'features/T9n'
import { Price } from 'features/Price'
import { usePackage } from './usePackage'
import {
Wrapper,
InfoWrapper,
Description,
Header,
HeaderTitle,
BestChoice,
PriceBlock,
PriceTextWrapper,
PriceTextTop,
PriceTextBottom,
} from './styled'
type Props = {
matchPackage: MatchPackage,
}
export const RegularPackage = ({ matchPackage }: Props) => {
const {
firstDescription,
handleClick,
handleKeyPress,
isActive,
priceTextBottomLexic,
priceTextTopLexic,
} = usePackage(matchPackage)
return (
<Wrapper
onClick={handleClick}
onKeyDown={handleKeyPress}
active={isActive}
isMainPackage={matchPackage.isMainPackage}
>
<Header>
<HeaderTitle
width={matchPackage.id.startsWith('team_') ? 190 : undefined}
t={matchPackage.originalObject.sub.lexic1}
/>
{matchPackage.isMainPackage && (
<BestChoice>
<T9n t='best_choice' />
</BestChoice>
)}
</Header>
<InfoWrapper>
<Description>
{firstDescription}
</Description>
<Description>
<T9n t={matchPackage.originalObject.sub.lexic3} />
</Description>
<PriceBlock isMonthSubscription={matchPackage.isMonthSubscription}>
<PriceTextWrapper>
<PriceTextTop t={priceTextTopLexic} />
<PriceTextBottom t={priceTextBottomLexic} />
</PriceTextWrapper>
<Price
amount={matchPackage.originalObject.price}
currency={matchPackage.currency}
perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined}
/>
</PriceBlock>
</InfoWrapper>
</Wrapper>
)
}

@ -0,0 +1,140 @@
import styled, { css } from 'styled-components/macro'
import { PriceDetails } from 'features/Price/styled'
import { T9n } from 'features/T9n'
export const Description = styled.p`
margin-bottom: 20px;
font-weight: 400;
font-size: 12px;
line-height: 20px;
white-space: initial;
:empty, :has(span:empty) {
display: none;
}
:last-of-type {
margin-bottom: 0;
}
`
type PriceBlockProps = {
isMonthSubscription?: boolean,
}
export const PriceBlock = styled.div<PriceBlockProps>`
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 218px;
left: 0;
right: 0;
padding-left: 30px;
padding-right: ${({ isMonthSubscription }) => (isMonthSubscription ? 10 : 20)}px;
`
export const PriceTextWrapper = styled.div`
display: flex;
flex-direction: column;
white-space: initial;
`
export const PriceTextTop = styled(T9n)`
margin-bottom: 2px;
font-weight: 600;
font-size: 10px;
font-style: italic;
letter-spacing: 0.03em;
`
export const PriceTextBottom = styled(T9n)`
font-weight: 400;
font-size: 8px;
font-style: italic;
letter-spacing: 0.03em;
`
type WrapperProps = {
active?: boolean,
isMainPackage?: boolean,
}
export const Wrapper = styled.div.attrs({
tabIndex: 0,
})<WrapperProps>`
width: 100%;
height: 380px;
border-radius: 2px;
background: ${({ theme }) => theme.colors.packageBackground};
box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s;
cursor: pointer;
${({ active, theme: { colors } }) => (active
? css`
background-color: ${colors.button};
`
: css`
:hover {
background-color: #2F3E6D;
}`
)}
${({ isMainPackage, theme: { colors } }) => (isMainPackage
? css`
border: 2px solid ${colors.white};
`
: ''
)}
`
export const Header = styled.header`
display: flex;
flex-direction: column;
justify-content: center;
height: 100px;
background: rgba(255, 255, 255, 0.2);
`
type HeaderTitleProps = {
width?: number,
}
export const HeaderTitle = styled(T9n)<HeaderTitleProps>`
flex: 1;
display: flex;
align-items: center;
width: ${({ width }) => (width ? `${width}px` : 'auto')};
padding-left: 30px;
white-space: initial;
font-size: 16px;
font-weight: 600;
`
export const BestChoice = styled.div`
display: flex;
align-items: center;
height: 22px;
margin-top: auto;
padding-left: 32px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
font-variant: all-small-caps;
color: #464646;
background-color: #FDFDFE;
`
export const InfoWrapper = styled.div`
position: relative;
display: flex;
flex-direction: column;
height: calc(100% - 100px);
padding: 20px 30px 30px;
${PriceDetails} {
margin-top: 5px;
}
`

@ -0,0 +1,70 @@
import type { KeyboardEvent } from 'react'
import { KEYBOARD_KEYS } from 'config'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup'
import { useLexicsStore } from 'features/LexicsStore'
export const usePackage = (matchPackage: MatchPackage) => {
const {
onPackageSelect,
selectedPackage,
} = useBuyMatchPopupStore()
const { suffix, translate } = useLexicsStore()
const isActive = matchPackage.id === selectedPackage?.id
const priceTextTopLexic = matchPackage.isMonthSubscription ? 'subscription' : 'purchase'
const priceTextBottomLexic = matchPackage.isMonthSubscription ? 'cancel_anytime' : 'one_off_payment'
const getFirstDescription = () => {
const tournament = matchPackage.match.tournament[`name_${suffix}`]
const season = typeof matchPackage.description.values.season === 'string'
? `${matchPackage.description.values.season.slice(0, 4)}/${matchPackage.description.values.season.slice(-2)}`
: ''
switch (true) {
case matchPackage.originalObject.sub.lexic2 !== null:
return translate(matchPackage.originalObject.sub.lexic2!)
case matchPackage.id === '0':
return matchPackage.name
case matchPackage.id.startsWith('team1'):
case matchPackage.id.startsWith('team2'):
return `${translate('all_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}`
case matchPackage.id.startsWith('team_home'):
return `${translate('all_home_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}`
case matchPackage.id.startsWith('team_away'):
return `${translate('all_away_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}`
case matchPackage.id.startsWith('all'):
return `${tournament} ${translate('in_season')} ${season}`
default: return ''
}
}
const handleClick = () => {
onPackageSelect(matchPackage)
}
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === KEYBOARD_KEYS.Enter) {
onPackageSelect(matchPackage)
}
}
return {
firstDescription: getFirstDescription(),
handleClick,
handleKeyPress,
isActive,
priceTextBottomLexic,
priceTextTopLexic,
}
}

@ -1,115 +0,0 @@
import { useCallback } from 'react'
import map from 'lodash/map'
import { MDASH } from 'config'
import { isSubscribePopup } from 'helpers'
import { Name as Names } from 'features/Name'
import { T9n } from 'features/T9n'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
import { CloseButton, HeaderActions } from 'features/PopupComponents'
import {
Body,
Button,
Footer,
Header,
HeaderTitle,
Wrapper,
} from 'features/BuyMatchPopup/styled'
import { MatchPackage, SubscriptionType } from '../../types'
import {
Description,
InfoWrapper,
Name,
Pass,
} from '../PackagesList/styled'
import {
Price,
ChooseSub,
ChooseSubItem,
ChooseSubList,
} from './styled'
export const SelectSubscriptionStep = () => {
const {
close,
match,
matchSubscriptions,
onNext,
onSubscriptionSelect,
selectedSubscription,
} = useBuyMatchPopupStore()
const getPackagesCurrency = useCallback((
packages: Record<SubscriptionType, Array<MatchPackage>>,
) => {
const packageWithValue = Object.entries(packages).find(([key, value]) => value.length)?.[1][0]
return packageWithValue ? packageWithValue.currency : 'RUB'
}, [])
if (!match || !matchSubscriptions) return null
return (
<Wrapper>
<Header>
{!isSubscribePopup()
&& (
<HeaderTitle>
<Names nameObj={match.team1} />
{` ${MDASH} `}
<Names nameObj={match.team2} />
<ChooseSub>
<T9n t='choose_subscription' />
</ChooseSub>
</HeaderTitle>
)}
<HeaderActions position='right'>
<CloseButton onClick={close} />
</HeaderActions>
</Header>
<Body marginTop={15}>
<ChooseSubList>
{map(matchSubscriptions, (subscription) => (
<ChooseSubItem
key={subscription.id}
onClick={() => onSubscriptionSelect(subscription)}
active={subscription === selectedSubscription}
>
<InfoWrapper>
<Pass>
<T9n t={subscription.lexic} />
</Pass>
<Name>
<T9n t={subscription.lexic2} />
</Name>
<Description>
<T9n t={subscription.lexic3} />
</Description>
</InfoWrapper>
<Price
amount={subscription.min_price || 0}
currency={getPackagesCurrency(subscription.packages)}
isFrom={Boolean(subscription.min_price)}
/>
</ChooseSubItem>
))}
</ChooseSubList>
</Body>
<Footer>
<Button
disabled={!selectedSubscription}
onClick={onNext}
id='purchase_next'
>
<T9n t='next_choose' />
</Button>
</Footer>
</Wrapper>
)
}

@ -1,33 +0,0 @@
import styled from 'styled-components/macro'
import { Price as BasePrice } from 'features/Price'
import { PriceAmount, PriceDetails } from 'features/Price/styled'
import {
Item,
List,
} from '../PackagesList/styled'
export const Price = styled(BasePrice)`
${PriceAmount} {
font-size: 24px;
font-weight: normal;
}
${PriceDetails} {
padding-top: 5px;
font-size: 12px;
}
`
export const ChooseSub = styled.div`
font-weight: 600;
font-size: 16px;
margin: 35px 0 17px;
`
export const ChooseSubItem = styled(Item)`
min-height: auto;
`
export const ChooseSubList = styled(List)`
height: auto;
`

@ -1,7 +1,4 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import capitalize from 'lodash/capitalize'
import styled from 'styled-components/macro'
import { useCardsStore } from 'features/CardsStore'
import { ButtonOutline } from 'features/Common'
@ -9,61 +6,40 @@ import { T9n } from 'features/T9n'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
import { Steps } from 'features/BuyMatchPopup/types'
const Wrapper = styled.div`
display: flex;
margin-top: 25px;
padding: 0 40px;
${isMobileDevice
? css`
padding: 0;
justify-content: center;
align-items: center;
`
: ''};
type WrapperProps = {
hasOnlyOneSubscription: boolean,
}
const Wrapper = styled.div<WrapperProps>`
margin: ${({ hasOnlyOneSubscription }) => (hasOnlyOneSubscription ? '0 0 37px auto' : '0 auto 20px 30px')};
letter-spacing: 0.03em;
`
const CardInfo = styled.span`
font-weight: 500;
font-size: 18px;
line-height: 20px;
color: rgba(255, 255, 255, 0.7);
${isMobileDevice
? css`
font-size: 14px;
`
: ''};
font-size: 16px;
font-weight: 600;
font-style: italic;
`
const ChangeCardButton = styled(ButtonOutline)`
border: none;
width: auto;
height: auto;
padding: 0 10px;
margin-left: 10px;
line-height: 20px;
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
margin-left: 5px;
font-size: 13px;
font-style: italic;
cursor: pointer;
:hover {
color: rgba(255, 255, 255);
}
${isMobileDevice
? css`
font-size: 12px;
`
: ''};
`
export const SelectedCard = () => {
const { goTo } = useBuyMatchPopupStore()
const { goTo, matchPackages } = useBuyMatchPopupStore()
const { defaultCard } = useCardsStore()
if (!defaultCard) return null
return (
<Wrapper>
<CardInfo>{capitalize(defaultCard?.brand)} {defaultCard?.last4}</CardInfo>
<Wrapper hasOnlyOneSubscription={matchPackages.length === 1}>
<CardInfo>{defaultCard.brand} {defaultCard.last4}</CardInfo>
<ChangeCardButton onClick={(e) => goTo(Steps.CardSelection, e)}>
<T9n t='change_card' />
</ChangeCardButton>

@ -0,0 +1,52 @@
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { T9n } from 'features/T9n'
import { Price } from 'features/Price'
import { usePackage } from '../RegularPackage/usePackage'
import {
Wrapper,
Description,
PriceBlock,
PriceTextWrapper,
PriceTextTop,
PriceTextBottom,
} from './styled'
type Props = {
matchPackage: MatchPackage,
}
export const SinglePackage = ({ matchPackage }: Props) => {
const {
firstDescription,
priceTextBottomLexic,
priceTextTopLexic,
} = usePackage(matchPackage)
return (
<Wrapper>
<Description>
<T9n t={matchPackage.originalObject.sub.lexic1} />
</Description>
<Description>
{firstDescription}
</Description>
<Description>
<T9n t={matchPackage.originalObject.sub.lexic3} />
</Description>
<PriceBlock>
<PriceTextWrapper>
<PriceTextTop t={priceTextTopLexic} />
<PriceTextBottom t={priceTextBottomLexic} />
</PriceTextWrapper>
<Price
amount={matchPackage.originalObject.price}
currency={matchPackage.currency}
perPeriod={`per_${SubscriptionType.Month}`}
/>
</PriceBlock>
</Wrapper>
)
}

@ -0,0 +1,100 @@
import styled from 'styled-components/macro'
import {
PriceAmount,
PriceDetails,
Period,
Divider,
Currency,
PerPeriod,
} from 'features/Price/styled'
import { T9n } from 'features/T9n'
export const Description = styled.p`
margin-bottom: 20px;
line-height: 20px;
font-size: 18px;
font-weight: 400;
:empty, :has(span:empty) {
display: none;
}
:last-of-type {
margin-bottom: 0;
}
:first-child {
font-size: 20px;
font-weight: 700;
}
`
export const PriceBlock = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 60px;
`
export const PriceTextWrapper = styled.div`
display: flex;
flex-direction: column;
white-space: initial;
`
export const PriceTextTop = styled(T9n)`
margin-bottom: 2px;
font-weight: 600;
font-style: italic;
font-size: 16px;
letter-spacing: 0.03em;
`
export const PriceTextBottom = styled(T9n)`
font-weight: 400;
font-size: 16px;
font-style: italic;
letter-spacing: 0.03em;
`
export const Wrapper = styled.div`
position: relative;
display: flex;
flex-direction: column;
height: calc(100% - 100px);
${PriceDetails} {
font-size: 50px;
}
${PriceAmount} {
font-size: 50px;
font-weight: 600;
}
${Divider} {
width: 5px;
height: 45px;
margin-right: 3px;
translate: 0 7px;
}
${Currency} {
margin-right: 10px;
translate: none;
font-size: 50px;
font-weight: 600;
}
${Period} {
font-size: 16px;
}
${PerPeriod} {
display: inline-block;
translate: 0 4px;
}
`

@ -1,8 +1,11 @@
import { useHistory } from 'react-router-dom'
import { isMobileDevice } from 'config'
import { CardStep } from './components/CardStep'
import { ErrorStep } from './components/ErrorStep'
import { SuccessStep } from './components/SuccessStep'
import { PackageSelectionStep } from './components/PackageSelectionStep'
import { SelectSubscriptionStep } from './components/SelectSubscription'
import { Steps } from './types'
import { Modal } from './styled'
@ -16,7 +19,6 @@ const components = {
[Steps.CardSelection]: CardStep,
[Steps.Success]: SuccessStep,
[Steps.Error]: ErrorStep,
[Steps.SelectSubscription]: SelectSubscriptionStep,
}
export const BuyMatchPopup = () => {
@ -24,9 +26,12 @@ export const BuyMatchPopup = () => {
close,
currentStep,
isPopupOpen,
matchPackages,
} = useBuyMatchPopupStore()
if (!isPopupOpen || !currentStep) return null
const history = useHistory()
if (!isPopupOpen || !currentStep || matchPackages.length === 0) return null
const Step = components[currentStep]
@ -37,10 +42,17 @@ export const BuyMatchPopup = () => {
}
}
if (isMobileDevice && currentStep === Steps.SelectPackage) return <Step />
const handleClose = () => {
close()
isMobileDevice && currentStep !== Steps.Success && history.goBack()
}
return (
<Modal
isOpen={Boolean(1)}
close={close}
isOpen
close={handleClose}
withCloseButton={false}
>
<Step />

@ -34,6 +34,23 @@ type SubscriptionArgs = {
suffix: string,
}
const getOrder = (id: string, packageId: number) => {
const ordersMap = {
[`all ${packageId} month`]: 2,
[`all ${packageId} year`]: 3,
[`team1 ${packageId} month`]: 4,
[`team2 ${packageId} month`]: 5,
[`team1 ${packageId} year`]: 6,
[`team2 ${packageId} year`]: 7,
[`team_home ${packageId} year`]: 8,
[`team_home ${packageId} month`]: 9,
[`team_away ${packageId} year`]: 10,
[`team_away ${packageId} month`]: 11,
}
return ordersMap[id]
}
const transformPackage = ({
match,
season,
@ -49,25 +66,27 @@ const transformPackage = ({
nameObj: match[passNameKeys[key]],
suffix,
})
const description: Desciption = isLeaguePass
? {
lexic: subscription.lexic3,
values: {},
}
: {
lexic: descriptionLexics[key],
values: {
season: season.name,
team: teamName,
},
}
const description: Desciption = {
lexic: descriptionLexics[key],
values: {
season: season.name,
team: teamName,
},
}
const nameLexic = isLeaguePass ? subscription.lexic2 : null
const subscriptionPackageId = subscriptionPackage.sub.id
const id = `${key} ${subscriptionPackageId} ${type}`
const order = getOrder(id, subscriptionPackageId)
return {
currency: currencySymbols[subscriptionPackage.currency_iso],
description,
id: `${key} ${subscriptionPackage.sub.id}`,
id,
isMainPackage: subscriptionPackage.sub.id === 21,
isMonthSubscription: type === 'month',
match,
name: teamName,
nameLexic,
order,
originalObject: subscriptionPackage,
pass: passLexics[key],
price: subscriptionPackage.price,
@ -111,7 +130,9 @@ const transformPackages = ({
values: {},
},
id: '0',
match,
name: `${team1Name} - ${team2Name}`,
order: 1,
originalObject: payPerView,
pass: 'pass_match_access',
price: payPerView.price,

@ -3,6 +3,7 @@ import {
useCallback,
useState,
useEffect,
useRef,
} from 'react'
import { useHistory } from 'react-router-dom'
@ -27,11 +28,8 @@ import type { OnFailedPaymentActionData } from 'requests/buySubscription'
import { dataForPayHighlights } from 'pages/HighlightsPage/storeHighlightsAtoms'
import { useCardsStore } from 'features/CardsStore'
import {
Match,
Steps,
SubscriptionType,
} from 'features/BuyMatchPopup/types'
import type { Match, MatchPackage } from 'features/BuyMatchPopup/types'
import { Steps, SubscriptionType } from 'features/BuyMatchPopup/types'
import { getProfileUrl } from 'features/ProfileLink/helpers'
import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction'
@ -65,6 +63,7 @@ export const useBuyMatchPopup = () => {
isOpen,
open,
} = useToggle()
const matchPackageRef = useRef<MatchPackage | null>(null)
const setDataHighlights = useSetRecoilState(dataForPayHighlights)
const goTo = useCallback(
@ -85,10 +84,10 @@ export const useBuyMatchPopup = () => {
const {
fetchSubscriptions,
matchPackages,
matchSubscriptions,
onPackageSelect,
onPeriodSelect,
onSubscriptionSelect,
resetSubscriptions,
selectedPackage,
selectedPeriod,
@ -97,24 +96,22 @@ export const useBuyMatchPopup = () => {
subscriptions,
} = useSubscriptions()
const onNext = (e: MouseEvent<HTMLButtonElement>) => goTo(Steps.SelectPackage, e)
const openPopup = useCallback((matchData: Match) => {
setMatch(matchData)
open()
setSteps([])
setSteps([Steps.SelectPackage])
}, [open])
useEffect(() => {
if (isEmpty(matchSubscriptions)) return
if (isEmpty(matchPackages)) return
if (size(matchSubscriptions) === 1) {
setSteps([Steps.SelectPackage])
onSubscriptionSelect(matchSubscriptions[0])
} else {
setSteps([Steps.SelectSubscription])
setSteps([Steps.SelectPackage])
if (size(matchPackages) === 1) {
onPackageSelect(matchPackages[0])
}
}, [matchSubscriptions, onSubscriptionSelect])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matchPackages])
const closePopup = () => {
close()
@ -122,6 +119,7 @@ export const useBuyMatchPopup = () => {
setError('')
resetSubscriptions()
setSelectedPackage(null)
matchPackageRef.current = null
if (isSubscribePopup()) {
history.replace({ search: '' })
@ -155,8 +153,10 @@ export const useBuyMatchPopup = () => {
}
const onConfirmationSuccess = ({ id }: PaymentIntent) => {
if (!selectedPackage) return
const item = selectedPackage?.originalObject
const subscriptionPackage = matchPackageRef.current || selectedPackage
if (!subscriptionPackage) return
const item = subscriptionPackage?.originalObject
notifySuccessfulSubscription({ item, paymentIntentId: id })
.then(onSuccessfulSubscription, goToError)
}
@ -191,10 +191,12 @@ export const useBuyMatchPopup = () => {
}
const subscribeToMatch = () => {
if (!selectedPackage || !defaultCard) return
const subscriptionPackage = matchPackageRef.current || selectedPackage
const item = selectedPackage.originalObject
const buy = requests[selectedPackage.type]
if (!subscriptionPackage || !defaultCard) return
const item = subscriptionPackage.originalObject
const buy = requests[subscriptionPackage.type]
setLoader(true)
buy({ cardId: defaultCard.id, item }).then(
onSuccessfulSubscription,
@ -202,8 +204,13 @@ export const useBuyMatchPopup = () => {
)
}
const onBuyClick = (e?: MouseEvent<HTMLButtonElement>) => {
e?.stopPropagation()
const onBuyClick = (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => {
e.stopPropagation()
if (matchPackage) {
matchPackageRef.current = matchPackage
}
if (defaultCard) {
subscribeToMatch()
} else {
@ -212,10 +219,10 @@ export const useBuyMatchPopup = () => {
}
useEffect(() => {
if (match) {
if (match && isOpen) {
fetchSubscriptions(match)
}
}, [match, fetchSubscriptions])
}, [match, isOpen, fetchSubscriptions])
return {
close: closePopup,
@ -229,14 +236,13 @@ export const useBuyMatchPopup = () => {
lastSelectedPackage,
loader,
match,
matchPackages,
matchSubscriptions,
onBuyClick,
onConfirmationSuccess,
onConfirmationSuccessHiglights,
onNext,
onPackageSelect,
onPeriodSelect,
onSubscriptionSelect,
onSuccessfulHighlights,
onUnsuccessfulSubscription,
open: openPopup,
@ -246,6 +252,7 @@ export const useBuyMatchPopup = () => {
selectedSubscription,
setDisabledBuyBtn,
setLastSelectedPackage,
setSelectedPackage,
setShowClearBtn,
showClearBtn,
subscriptions,

@ -2,12 +2,14 @@ import {
useState,
useCallback,
useEffect,
useMemo,
} from 'react'
import find from 'lodash/find'
import isEmpty from 'lodash/isEmpty'
import first from 'lodash/first'
import size from 'lodash/size'
import flatMap from 'lodash/flatMap'
import { getSubscriptions } from 'requests/getSubscriptions'
import { getSelectedSubscriptions } from 'requests/getSelectedSubscriptions'
@ -61,6 +63,12 @@ export const useSubscriptions = () => {
setSelectedPackage,
] = useState<MatchPackage | null>(null)
const matchPackages: Array<MatchPackage> = useMemo(() => [
...flatMap(matchSubscriptions, 'packages.year'),
...flatMap(matchSubscriptions, 'packages.month'),
...flatMap(matchSubscriptions, 'packages.pay_per_view'),
], [matchSubscriptions])
const fetchSubscriptions = useCallback(async (match: Match) => {
let subscriptions
if (checkUrlParams('id') && checkUrlParams('subscribe')) {
@ -117,6 +125,7 @@ export const useSubscriptions = () => {
return {
fetchSubscriptions,
matchPackages,
matchSubscriptions,
onPackageSelect,
onPeriodSelect: setSelectedPeriod,

@ -76,14 +76,15 @@ export const Button = styled(ButtonSolid)`
type WrapperProps = {
height?: number,
padding?: string,
width?: number,
}
export const Wrapper = styled.div<WrapperProps>`
position: relative;
height: ${({ height }) => (height ? `${height}px` : 'auto')};
width: ${({ width }) => (width ? `${width}px` : '830px')};
padding: 40px 10px;
width: ${({ width }) => (width ? `${width}px` : 'auto')};
padding: ${({ padding }) => (padding || '40px 10px')};
@media (max-width: 750px){
width: 100%;
@ -188,7 +189,20 @@ export const ButtonPrevious = styled.button`
top: 30px;
left: 20px;
cursor: pointer;
${isMobileDevice
? css`
top: 15px;
left: 10px;
span[direction='left'] {
padding: 3px;
border-color: ${({ theme }) => theme.colors.white};
}
`
: ''}
`
export const ButtonClear = styled(ButtonOutline)`
border: 1px solid #FFFFFF;
border-radius: 4px;

@ -8,7 +8,6 @@ export enum Steps {
CardSelection = 'CardSelection',
Error = 'Error',
SelectPackage = 'SelectPackage',
SelectSubscription = 'SelectSubscription',
Success = 'Success',
}
@ -27,8 +26,12 @@ export type MatchPackage = {
currency: string,
description: Desciption,
id: string,
isMainPackage?: boolean,
isMonthSubscription?: boolean,
match: Match,
name: string,
nameLexic?: LexicsId | null,
order: number,
originalObject: SubscriptionResponse,
pass: string,
price: number,

@ -10,11 +10,15 @@ import {
addDays,
} from 'date-fns'
import { useToggle } from 'hooks'
import isObject from 'lodash/isObject'
import { useLocalStore, useToggle } from 'hooks'
import { useLexicsStore } from 'features/LexicsStore'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { ViewsType } from 'components/Ads/types'
import {
getDisplayDate,
getMonths,
@ -61,6 +65,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
@ -72,6 +85,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,7 +43,7 @@ const getTimezoneOffset = (date: Date) => {
const getDate = (date: Date) => format(date, 'yyyy-MM-dd')
export const useHomePage = () => {
const { user, userInfo } = useAuthStore()
const { userInfo } = useAuthStore()
const {
isMonthMode,
selectedDate,
@ -44,6 +53,9 @@ export const useHomePage = () => {
const [isOpenDownload, setIsOpenDownload] = useState(false)
const [isShowConfirmPopup, setIsShowConfirmPopup] = useState(false)
const setIsSportFilterShown = useSetRecoilState(isSportFilterShownAtom)
const { ads } = useAds({})
const date = isMonthMode ? selectedMonthModeDate : selectedDate
const dateTo = isMonthMode
? `${getDate(endOfMonth(selectedMonthModeDate))} 00:00:00`
@ -55,6 +67,13 @@ export const useHomePage = () => {
// 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 () => {
@ -74,7 +93,10 @@ export const useHomePage = () => {
) {
setIsOpenDownload(true)
}
countryCode()
}, [])
const fetchMatches = useCallback(
(limit: number, offset: number) => getHomeMatches({
date: getDate(date),
@ -86,7 +108,7 @@ export const useHomePage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
[
selectedDate,
user,
userInfo?.email,
dateTo,
date,
],
@ -99,6 +121,7 @@ export const useHomePage = () => {
}, [setIsSportFilterShown])
return {
ads,
fetchMatches,
handleCloseConfirmPopup,
isOpenDownload,

@ -1,20 +1,22 @@
import { PAGES } from 'config'
import { isMobileDevice } from 'config/userAgent'
import { PAGES, isMobileDevice } from 'config'
import { usePageLogger } from 'hooks/usePageLogger'
import { usePageLogger } from 'hooks'
import { ConfirmPopup } from 'features/AuthServiceApp/components/ConfirmPopup'
import { Matches } from 'features/Matches'
import {
HeaderFiltersStore,
} from 'features/HeaderFilters'
import {
PageWrapper,
Main,
Content,
} from 'features/PageLayout'
import { UserFavorites } from 'features/UserFavorites'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { HEADER_MOBILE_ADS } from 'components/Ads/types'
import { HeaderAds } from 'components/Ads'
import { useHomePage } from './hooks'
import { Header } from './components/Header'
@ -24,6 +26,7 @@ import { HeaderFilters } from './components/HeaderFilters'
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}
@ -62,6 +74,7 @@ const Home = () => {
const HomePage = () => (
<HeaderFiltersStore>
<Home />
{!isMobileDevice && <BuyMatchPopup />}
</HeaderFiltersStore>
)

@ -20,7 +20,7 @@ import { useLexicsStore } from 'features/LexicsStore'
import { useAuthStore } from 'features/AuthStore'
import { getLandingName, isPastLandingDate } from './helpers'
import { useName } from '../Name'
import { getName, useName } from '../Name'
export const useLandings = () => {
const [tournamentInfo, setTournamentInfo] = useState<Landing | null>(null)
@ -29,7 +29,7 @@ export const useLandings = () => {
const [nonExistLogoSrc, setNonExistLogoSrc] = useState('')
const [tournamentProfile, setTournamentProfile] = useState<TournamentInfo>(null)
const { addLexicsConfig } = useLexicsStore()
const { addLexicsConfig, suffix } = useLexicsStore()
const {
landingUrlFrom,
setIsFromLanding,
@ -67,15 +67,15 @@ export const useLandings = () => {
useEffect(() => {
(async () => {
const landingData = sessionStorage.getItem('landingData')
const parseLandingDate = landingData && JSON.parse(landingData)
const parseLandingData = landingData && JSON.parse(landingData)
if (parseLandingDate && parseLandingDate.defaultLanding) {
setIsNonExistLanding(true)
getTournamentInfo(parseLandingDate.sportType, parseLandingDate.tournamentId)
if (parseLandingData && parseLandingData.defaultLanding) {
getTournamentInfo(parseLandingData.sportType, parseLandingData.tournamentId)
.then(setTournamentProfile)
setIsNonExistLanding(true)
return getLandingLogo({
sport_id: parseLandingDate.sportType,
tournament_id: parseLandingDate.tournamentId,
sport_id: parseLandingData.sportType,
tournament_id: parseLandingData.tournamentId,
})
.then(({ logo_url }) => setNonExistLogoSrc(logo_url || '/images/tournament-fallback.png'))
}
@ -83,15 +83,17 @@ export const useLandings = () => {
try {
const data = landingUrlFrom
? await getLanding({
landingName: parseLandingDate.landing_id || parseLandingDate.url_landing,
seasonId: parseLandingDate.season_id,
sportId: parseLandingDate.sport_id,
tournamentId: parseLandingDate.tournament_id,
landingName: parseLandingData.landing_id || parseLandingData.url_landing,
seasonId: parseLandingData.season_id,
sportId: parseLandingData.sport_id,
tournamentId: parseLandingData.tournament_id,
})
: await getLanding({ landingName: getLandingName() })
if (user) return redirectToUrl(data.url_button || '')
if (isPastLandingDate(data.date_to)) setIsInactiveLanding(true)
return setTournamentInfo(data)
} catch (err) {
return redirectToHomePage()
@ -115,6 +117,28 @@ export const useLandings = () => {
return () => clearInterval(getSliderInterval)
}, [imgCounter, sliderItemId])
const inActiveLandingData = () => {
if (!tournamentInfo?.tournaments || !isInactiveLanding) return null
const {
season,
tournament_eng,
tournament_rus,
} = tournamentInfo.tournaments[0]
const currentTournamentsTitle = {
name_eng: tournament_eng,
name_rus: tournament_rus,
}
const tournamentsTitle = getName({ nameObj: currentTournamentsTitle, suffix })
return {
season,
tournamentsTitle,
}
}
const defaultTournamentName = useName(tournamentProfile || {})
return {
@ -123,6 +147,7 @@ export const useLandings = () => {
defaultTournamentName,
description,
gallery,
inActiveLandingData,
isInactiveLanding,
isNonExistLanding,
logo: tournamentInfo?.media.logo,

@ -41,15 +41,13 @@ import {
} from './styled'
const Landings = () => {
const season = checkUrlParams('season')
const tournamentName = checkUrlParams('tournament')
const {
buttonColor,
buttonLexic,
defaultTournamentName,
description,
gallery,
inActiveLandingData,
isInactiveLanding,
isNonExistLanding,
logo,
@ -65,6 +63,9 @@ const Landings = () => {
tournamentInfo,
} = useLandings()
const season = checkUrlParams('season') ?? inActiveLandingData()?.season
const tournamentName = checkUrlParams('tournament') ?? inActiveLandingData()?.tournamentsTitle
const fallbackSrc = '/images/tournament-fallback.png'
if ((!tournamentInfo && !isNonExistLanding)
@ -121,7 +122,11 @@ const Landings = () => {
<Fragment>
<DateInfo>
{isInactiveLanding
? season
? (
<>
<T9n t='inactive_period' />&nbsp;{season}
</>
)
: <T9n t='default_season' />}&nbsp;
</DateInfo>

@ -7,7 +7,6 @@ import {
ProfileTypes,
PAGES,
client,
isLffClient,
} from 'config'
import type { Match } from 'features/Matches'
@ -19,8 +18,6 @@ import { useUserFavoritesStore } from 'features/UserFavorites/store'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { Icon } from 'features/Icon'
import { getCardColor } from 'helpers/getCardColor'
import type { LiveScore } from 'requests'
import { NoAccessMessage } from '../../NoAccessMessage'
@ -73,6 +70,7 @@ export const CardFrontsideMobile = ({
const {
access,
date,
group,
live,
preview,
previewURL,
@ -88,6 +86,7 @@ export const CardFrontsideMobile = ({
const { isInFavorites } = useUserFavoritesStore()
const { isScoreHidden } = useMatchSwitchesStore()
const { isMonthMode } = useHeaderFiltersStore()
const { color } = tournament
const isInFuture = getUnixTime(date) > getUnixTime(new Date())
const showScore = !(isInFuture || isScoreHidden) || (live && !isScoreHidden)
const team1InFavorites = isInFavorites(ProfileTypes.TEAMS, team1.id)
@ -111,10 +110,10 @@ export const CardFrontsideMobile = ({
return (
<CardWrapperOuter onClick={onClick} onKeyPress={onKeyPress}>
<CardWrapper>
<CardWrapper gradientColor={color || group?.color}>
<HoverFrame />
<PreviewWrapper isGradientPreview={isLffClient} color={getCardColor(tournament.id)}>
{!isLffClient && previewImage && (
<PreviewWrapper>
{previewImage && (
<Preview title={tournamentName} src={previewImage} />
)}
{access === MatchAccess.NoCountryAccess ? (

@ -27,7 +27,11 @@ export const CardWrapperOuter = styled.li.attrs({
: ''};
`
export const CardWrapper = styled.div`
type TCardWrapper = {
gradientColor?: string,
}
export const CardWrapper = styled.div<TCardWrapper>`
position: absolute;
top: 0;
left: 0;
@ -38,6 +42,15 @@ export const CardWrapper = styled.div`
background-color: ${({ theme }) => theme.colors.matchCardBackground};
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
cursor: pointer;
${({ gradientColor }) => (
gradientColor
? css`
background: linear-gradient(270deg, ${gradientColor} -4.49%, #000000 68.29%), #000000;`
: '')
}
${isMobileDevice
? css`
padding-bottom: 0;
@ -63,19 +76,11 @@ export const HoverFrame = styled.div`
}
`
type TPreviewWrapper = {
color?: string,
isGradientPreview?: boolean,
}
export const PreviewWrapper = styled.div<TPreviewWrapper>`
export const PreviewWrapper = styled.div`
position: relative;
display: flex;
width: 100%;
height: 60%;
${({ color, isGradientPreview }) => isGradientPreview
&& css`
background: ${color};`}
${isMobileDevice
? css`
width: 40%;

@ -4,12 +4,10 @@ import { useLocation, useRouteMatch } from 'react-router'
import getUnixTime from 'date-fns/getUnixTime'
import { ProfileTypes, PAGES } from 'config'
import { client, isLffClient } from 'config/clients'
import { client } 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'
@ -18,7 +16,6 @@ import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction'
import { useUserFavoritesStore } from 'features/UserFavorites/store'
import { TournamentSubtitle } from 'features/TournamentSubtitle'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { NoAccessMessage } from '../NoAccessMessage'
import { Score } from '../Score'
import {
@ -86,6 +83,7 @@ export const CardFrontside = ({
const { isInFavorites } = useUserFavoritesStore()
const { isScoreHidden } = useMatchSwitchesStore()
const { isMonthMode } = useHeaderFiltersStore()
const { color } = tournament
const isInFuture = getUnixTime(date) > getUnixTime(new Date())
const showScore = !(
isInFuture
@ -116,14 +114,13 @@ export const CardFrontside = ({
onKeyPress={onKeyPress}
isMatchPage={isMatchPage}
>
<CardWrapper isMatchPage={isMatchPage}>
<CardWrapper
isMatchPage={isMatchPage}
gradientColor={color || group?.color}
>
<HoverFrame />
<PreviewWrapper
isGradientPreview={isLffClient}
color={getCardColor(tournament.id)}
isMatchPage={isMatchPage}
>
{!isLffClient && previewImage && (
<PreviewWrapper isMatchPage={isMatchPage}>
{previewImage && (
<Preview
isMatchPage={isMatchPage}
title={tournamentName}

@ -4,7 +4,11 @@ import { useHistory } from 'react-router-dom'
import includes from 'lodash/includes'
import { PAGES, ProfileTypes } from 'config'
import {
isMobileDevice,
PAGES,
ProfileTypes,
} from 'config'
import type { Match } from 'features/Matches'
import { useMatchPopupStore } from 'features/MatchPopup'
@ -12,7 +16,7 @@ import { useBuyMatchPopupStore } from 'features/BuyMatchPopup'
import { useAuthStore } from 'features/AuthStore'
import { getProfileUrl } from 'features/ProfileLink/helpers'
import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction'
import { checkPage } from 'helpers/checkPage'
import { checkPage } from 'helpers'
export const useCard = (match: Match) => {
const { openMatchPopup } = useMatchPopupStore()
@ -39,6 +43,7 @@ export const useCard = (match: Match) => {
break
case MatchAccess.CanBuyMatch:
openBuyMatchPopup(match)
isMobileDevice && history.push(PAGES.subscriptions)
break
case MatchAccess.ViewMatchPopup:
openMatchPopup(match)
@ -56,6 +61,7 @@ export const useCard = (match: Match) => {
openMatchPopup,
openBuyMatchPopup,
redirectToMatchPage,
history,
])
const onKeyPress = useCallback((e: KeyboardEvent<HTMLLIElement>) => {

@ -7,6 +7,7 @@ import { Name } from 'features/Name'
import { ProfileLogo } from 'features/ProfileLogo'
type CardProps = {
gradientColor?: string,
isMatchPage?: boolean,
}
@ -42,6 +43,13 @@ export const CardWrapper = styled.div<CardProps>`
padding: ${({ isMatchPage }) => (isMatchPage ? '0.5rem 0.625rem 1.8rem' : '0 0 0.75rem')};
border-radius: 3px;
background-color: ${({ theme }) => theme.colors.matchCardBackground};
${({ gradientColor }) => (
gradientColor
? css`
background: linear-gradient(187deg, ${gradientColor} -4.49%, #000000 68.29%), #000000;`
: '')
}
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
cursor: pointer;
@ -71,8 +79,6 @@ export const HoverFrame = styled.div`
`
type TPreviewWrapper = {
color?: string,
isGradientPreview?: boolean,
isMatchPage?: boolean,
}
@ -81,9 +87,6 @@ export const PreviewWrapper = styled.div<TPreviewWrapper>`
display: flex;
width: 100%;
height: 60%;
${({ color, isGradientPreview }) => isGradientPreview
&& css`
background: ${color};`}
${({ isMatchPage }) => (
isMatchPage

@ -2,7 +2,7 @@ import type { ReactNode } from 'react'
import { Fragment, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { PAGES } from 'config'
import { isMobileDevice, PAGES } from 'config'
import { usePageParams } from 'hooks'
@ -49,6 +49,7 @@ export const SubscriptionGuard = ({ children }: Props) => {
sportType,
})
openBuyMatchPopup(profile)
isMobileDevice && history.replace(PAGES.subscriptions)
}
if (match?.access === MatchAccess.RedirectToProfile) {
@ -70,6 +71,7 @@ export const SubscriptionGuard = ({ children }: Props) => {
sportType,
user,
match?.access,
history,
])
return (

@ -1,10 +1,12 @@
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import { useHistory } from 'react-router'
import { useTour } from '@reactour/tour'
import { useTheme } from 'styled-components'
import find from 'lodash/find'
import { ProfileHeader } from 'features/ProfileHeader'
import { UserFavorites } from 'features/UserFavorites'
import { useUserFavoritesStore } from 'features/UserFavorites/store'
@ -12,6 +14,7 @@ import {
PageWrapper,
Main,
} from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { FavoritesActions } from 'requests'
@ -19,9 +22,14 @@ import {
ProfileTypes,
isIOS,
client,
isMobileDevice,
} from 'config'
import { usePageLogger, usePageParams } from 'hooks'
import {
useScreenOrientation,
usePageLogger,
usePageParams,
} from 'hooks'
import { checkUrlParams } from 'helpers/parseUrlParams/parseUrlParams'
@ -31,15 +39,20 @@ import { MatchPageStore, useMatchPageStore } from './store'
import { SubscriptionGuard } from './components/SubscriptionGuard'
import { LiveMatch } from './components/LiveMatch'
import { FavouriteTeamPopup } from './components/FavouriteTeam'
import { PLAYER_MOBILE_FULL_SCREEN } from '../../components/Ads/types'
import { MobileAd } from '../../components/Ads/components/MobileAd'
import { Wrapper } from './styled'
const MatchPageComponent = () => {
usePageLogger()
const history = useHistory()
const orientation = useScreenOrientation()
const { addRemoveFavorite, userFavorites } = useUserFavoritesStore()
const { colors } = useTheme()
const {
ads,
isStarted,
profile,
user,
@ -54,6 +67,11 @@ const MatchPageComponent = () => {
const { isOpen } = useTour()
const {
HORIZONTAL_FULL_SCREEN,
VERTICAL_FULL_SCREEN,
} = PLAYER_MOBILE_FULL_SCREEN
useEffect(() => {
let timer = 0
timer = window.setTimeout(() => {
@ -95,11 +113,23 @@ const MatchPageComponent = () => {
history.push(`/${sportName}/tournaments/${profile.tournament.id}`)
}
const currentOrientation = orientation === 0
? VERTICAL_FULL_SCREEN
: HORIZONTAL_FULL_SCREEN
const currentAds = useMemo(
() => (find(ads, (ad) => ad.position.id === currentOrientation)),
[ads, currentOrientation],
)
return (
<PageWrapper
isIOS={isIOS}
isTourOpen={Boolean(isOpen)}
>
{isMobileDevice
&& currentAds
&& <MobileAd ad={currentAds} />}
<ProfileHeader color={colors.matchHeaderBackground} height={client.name === 'facr' ? 5 : 4.5} />
<Main>
<UserFavorites />
@ -123,6 +153,7 @@ const MatchPage = () => (
<MatchPageStore>
<TourProvider>
<MatchPageComponent />
{!isMobileDevice && <BuyMatchPopup />}
</TourProvider>
</MatchPageStore>
)

@ -9,7 +9,7 @@ import includes from 'lodash/includes'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
import { PAGES } from 'config'
import { isMobileDevice, PAGES } from 'config'
import { useAuthStore } from 'features/AuthStore'
import { Tabs } from 'features/MatchSidePlaylists/config'
@ -29,6 +29,8 @@ import { usePageParams, useToggle } from 'hooks'
import { redirectToUrl } from 'helpers/redirectToUrl'
import { parseDate } from 'helpers/parseDate'
import { useAds } from 'components/Ads/hooks'
import { useTournamentData } from './useTournamentData'
import { useMatchData } from './useMatchData'
import { useFiltersPopup } from './useFitersPopup'
@ -71,6 +73,7 @@ const ACCESS_TIME = 60
export const useMatchPage = () => {
const [matchProfile, setMatchProfile] = useState<MatchInfo>(null)
const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false)
const [isFullscreen, setIsFullScreen] = useState(false)
const [access, setAccess] = useState(true)
const [playingProgress, setPlayingProgress] = useState(0)
const [playingData, setPlayingData] = useState<PlayingData>(initPlayingData)
@ -87,6 +90,12 @@ export const useMatchPage = () => {
const { profileId: matchId, sportType } = usePageParams()
const { ads } = useAds({
matchId,
sportType,
tournamentId: matchProfile?.tournament?.id,
})
useEffect(() => {
sessionStorage.removeItem('isFromLanding')
}, [])
@ -401,6 +410,7 @@ export const useMatchPage = () => {
activeFirstTeamPlayers,
activeSecondTeamPlayers,
activeStatus,
ads: isMobileDevice ? ads.mobile : ads.match,
allActionsToggle,
allPlayersToggle,
applyFilters,
@ -421,6 +431,7 @@ export const useMatchPage = () => {
isEmptyFilters,
isExpanded,
isFirstTeamPlayersChecked,
isFullscreen,
isLiveMatch,
isOpenFiltersPopup,
isPlayFilterEpisodes: isStatsPlaylist ? isStatsPlayFilterEpisodes : isPlayFilterEpisodes,
@ -450,6 +461,7 @@ export const useMatchPage = () => {
setCircleAnimation: isStatsPlaylist ? setStatsCircleAnimation : setCircleAnimation,
setEpisodeInfo,
setFullMatchPlaylistDuration,
setIsFullScreen,
setIsPlayingFiltersEpisodes: isStatsPlaylist
? setStatsIsPlayinFiltersEpisodes
: setIsPlayersStatsFetching,

@ -3,9 +3,14 @@ import { useEffect } from 'react'
import map from 'lodash/map'
import find from 'lodash/find'
import type { Events, MatchInfo } from 'requests'
import type {
Events,
MatchInfo,
AdType,
} from 'requests'
import { isLffClient } from 'config/clients'
import { isMobileDevice } from 'config/userAgent'
import { T9n } from 'features/T9n'
import type {
@ -13,11 +18,14 @@ import type {
PlaylistOption,
} from 'features/MatchPage/types'
import { PlaylistTypes } from 'features/MatchPage/types'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import { Tabs } from 'features/MatchSidePlaylists/config'
import { AdComponent } from 'components/Ads/components/AdComponent'
import { MobileAd } from 'components/Ads/components/MobileAd'
import { MATCH_ADS } from 'components/Ads/types'
import { isEqual } from '../../helpers'
import { EventButton } from '../EventButton'
import {
@ -30,6 +38,7 @@ import {
type Props = {
disablePlayingEpisodes?: () => void,
events: Events,
isFirstBlock?: boolean,
onSelect: (option: PlaylistOption) => void,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
@ -39,12 +48,14 @@ type Props = {
export const EventsList = ({
disablePlayingEpisodes,
events,
isFirstBlock,
onSelect,
profile,
selectedPlaylist,
setWatchAllEpisodesTimer,
}: Props) => {
const {
ads,
filteredEvents,
isPlayingEpisode,
selectedTab,
@ -52,6 +63,9 @@ export const EventsList = ({
} = useMatchPageStore()
const { suffix, translate } = useLexicsStore()
const { PLAYS_BOTTOM, PLAYS_BOTTOM_MOBILE } = MATCH_ADS
const adsPositionId = isMobileDevice ? PLAYS_BOTTOM_MOBILE : PLAYS_BOTTOM
useEffect(() => {
if (selectedPlaylist?.tab === Tabs.EVENTS && isPlayingEpisode) {
const {
@ -84,6 +98,12 @@ export const EventsList = ({
return (
<List>
{ads
&& isFirstBlock
&& map(ads, (ad: AdType) => ad?.position.id === adsPositionId
&& (isMobileDevice
? <MobileAd ad={ad} key={ad.id} />
: <AdComponent ad={ad} key={ad.id} />))}
{map(events, (event) => {
if (!event.t && !event.pl) {
return (

@ -5,6 +5,7 @@ import styled, { css } from 'styled-components/macro'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import some from 'lodash/some'
import { isMobileDevice } from 'config'
@ -19,6 +20,8 @@ import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import { MATCH_ADS } from 'components/Ads/types'
import { PlayButton } from '../PlayButton'
import { MatchDownloadButton } from '../MatchDownloadButton'
@ -31,8 +34,12 @@ type Props = {
selectedMathPlaylist?: PlaylistOption,
}
const List = styled.ul`
margin-bottom: ${LIST_INDENT}px;
type ListProps = {
isAdsExist?: boolean,
}
const List = styled.ul<ListProps>`
margin-bottom: ${({ isAdsExist }) => (isAdsExist ? '15px' : `${LIST_INDENT}px`)};
`
export const Item = styled.li`
@ -62,7 +69,7 @@ export const MatchPlaylists = forwardRef(
}: Props,
ref: ForwardedRef<HTMLUListElement>,
) => {
const { setEpisodeInfo } = useMatchPageStore()
const { ads, setEpisodeInfo } = useMatchPageStore()
const { translate } = useLexicsStore()
const handleButtonClick = (playlist: MatchPlaylistOption) => {
@ -76,7 +83,10 @@ export const MatchPlaylists = forwardRef(
}
return (
<List ref={ref}>
<List
ref={ref}
isAdsExist={some(ads, ({ position }) => position.id === MATCH_ADS.WATCH_TOP)}
>
{
map(playlists, (playlist) => (
<Item

@ -129,6 +129,7 @@ export const TabEvents = ({
profile={profile}
selectedPlaylist={selectedPlaylist}
setWatchAllEpisodesTimer={setWatchAllEpisodesTimer}
isFirstBlock={isFirstBlock}
/>
</HalfEvents>
)

@ -25,7 +25,10 @@ export const HalfList = styled.ul`
export const HalfEvents = styled.li``
export const List = styled.ul``
export const List = styled.ul`
display: grid;
grid-template-columns: 100%;
`
export const Event = styled.li`
width: 100%;

@ -6,13 +6,21 @@ import {
import size from 'lodash/size'
import filter from 'lodash/filter'
import map from 'lodash/map'
import { isMobileDevice } from 'config'
import type {
PlaylistOption,
Playlists,
TournamentData,
} from 'features/MatchPage/types'
import type { MatchInfo } from 'requests'
import { useMatchPageStore } from 'features/MatchPage/store'
import type { MatchInfo, AdType } from 'requests'
import { AdComponent } from 'components/Ads/components/AdComponent'
import { MATCH_ADS } from 'components/Ads/types'
import { DropdownSection } from '../DropdownSection'
import { MatchPlaylists, LIST_INDENT } from '../MatchPlaylists'
@ -34,6 +42,8 @@ export const TabWatch = ({
selectedPlaylist,
tournamentData,
}: Props) => {
const { ads } = useMatchPageStore()
const matchPlaylistsRef = useRef<HTMLUListElement>(null)
const additionalScrollHeight = (matchPlaylistsRef.current?.clientHeight || 0) + LIST_INDENT
@ -55,6 +65,12 @@ export const TabWatch = ({
onSelect={onSelect}
live={profile?.live}
/>
{!isMobileDevice
&& ads
&& (
map(ads, (ad: AdType) => ad?.position.id === MATCH_ADS.WATCH_TOP
&& <AdComponent ad={ad} key={ad.id} />)
)}
<DropdownSection
itemsCount={size(playlists.interview)}
title={playlists.lexics?.interview}

@ -19,16 +19,19 @@ import { Overlay } from 'components/Overlay'
import { useEventListener, useModalRoot } from 'hooks'
import { isIOS } from 'config/userAgent'
import { isIOS, isMobileDevice } from 'config/userAgent'
import { getLocalStorageItem } from 'helpers/getLocalStorage'
import { MATCH_ADS } from 'components/Ads/types'
import { Tabs } from './config'
import { TabEvents } from './components/TabEvents'
import { TabWatch } from './components/TabWatch'
import { TabPlayers } from './components/TabPlayers'
import { TabStats } from './components/TabStats'
import { useMatchSidePlaylists } from './hooks'
import {
Wrapper,
TabsWrapper,
@ -38,7 +41,9 @@ import {
TabTitle,
Container,
TabButton,
EventsAdsWrapper,
} from './styled'
import { HeaderAds } from '../../components/Ads'
const tabPanes = {
[Tabs.WATCH]: TabWatch,
@ -57,6 +62,7 @@ export const MatchSidePlaylists = ({
selectedPlaylist,
}: Props) => {
const {
ads,
hideProfileCard,
matchPlaylists: playlists,
profile,
@ -86,6 +92,9 @@ export const MatchSidePlaylists = ({
const [hasTabPaneScroll, setTabPaneScroll] = useState(false)
const { PLAYS_TOP, PLAYS_TOP_MOBILE } = MATCH_ADS
const adsPositionId = isMobileDevice ? PLAYS_TOP_MOBILE : PLAYS_TOP
useEffect(() => {
const {
clientHeight = 0,
@ -139,6 +148,13 @@ export const MatchSidePlaylists = ({
{showTabs
&& (
<TabsWrapper>
{selectedTab === Tabs.EVENTS
&& ads
&& (
<EventsAdsWrapper hasScroll={hasTabPaneScroll}>
<HeaderAds ads={ads.filter(({ position }) => position.id === adsPositionId)} />
</EventsAdsWrapper>
)}
<TabsGroup>
<Tab
aria-pressed={selectedTab === Tabs.WATCH}

@ -17,7 +17,6 @@ type WrapperProps = {
export const Wrapper = styled.div<WrapperProps>`
padding-right: 14px;
padding-top: 10px;
${({ highlighted }) => (highlighted
? css`
@ -58,6 +57,7 @@ export const TabsGroup = styled.div.attrs({ role: 'tablist' })`
display: flex;
justify-content: center;
gap: ${isMobileDevice ? 30 : 20}px;
padding-top: 10px;
`
export const TabTitle = styled(T9n)`
@ -129,7 +129,7 @@ export const Container = styled.div<TContainer>`
width: 320px;
margin-top: 14px;
max-height: calc(100vh - 130px);
overflow-y: ${({ forWatchTab }) => (forWatchTab ? 'hidden' : 'auto')};
overflow-y: auto;
padding-right: ${({ forWatchTab }) => (forWatchTab ? '0' : '')};
padding-left: 14px;
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')};
@ -253,3 +253,14 @@ export const BlockTitle = styled.span`
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
`
export const EventsAdsWrapper = styled.div<TContainer>`
width: ${({ hasScroll }) => (hasScroll ? '288px' : '306px')};
margin-bottom: 0.6rem;
${isMobileDevice
? css`
width: 100%;
`
: ''};
`

@ -1,10 +1,12 @@
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 +14,14 @@ import { TournamentList } from 'features/TournamentList'
import type { Match } from 'features/Matches'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { Wrapper } from './styled'
import { readToken } from 'helpers'
import { useMatchSwitchesStore } from '../MatchSwitches'
import { useHomePage } from '../HomePage/hooks'
import { querieKeys } from '../../config'
import { AdComponent } from '../../components/Ads/components/AdComponent'
import { Wrapper } from './styled'
type MatchesGridProps = {
matches: Array<Match>,
@ -24,6 +31,10 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
const isHomePage = useRouteMatch(PAGES.home)?.isExact
const { isScoreHidden } = useMatchSwitchesStore()
const { ads } = useHomePage()
const currentAds = ads.match_cell?.length ? ads.match_cell : ads.block
const {
compareSport,
isShowTournament,
@ -76,6 +87,12 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
return (
<Wrapper>
{!isMobileDevice
&& readToken()
&& currentAds
&& (
currentAds.map((ad: AdType) => <AdComponent ad={ad} />)
)}
{isHomePage && isShowTournament ? (
<TournamentList matches={filteredMatches()} />
) : (

@ -6,6 +6,7 @@ export const Wrapper = styled.ul`
display: grid;
grid-gap: 0.9rem;
grid-template-columns: repeat(6, 15.7%);
${isMobileDevice
? css`
display: flex;

@ -1,31 +0,0 @@
import omitBy from 'lodash/omitBy'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import type { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types'
type PaymentTab = {
tabLexic: string,
type: SubscriptionType,
}
const getTabLexic = (type: string) => {
switch (type) {
case 'month':
return 'for_month'
case 'year':
return 'for_year'
case 'pay_per_view':
return 'for_view'
default:
return ''
}
}
export const getCorrectPaymentTabs = (matchSubscriptions: MatchSubscription) => {
const matchSubscriptionsWithValues = omitBy(matchSubscriptions.packages, isEmpty)
return map(matchSubscriptionsWithValues, (matchSubscription, key) => ({
tabLexic: getTabLexic(key),
type: key,
})) as Array<PaymentTab>
}

@ -1,115 +0,0 @@
import { useMemo } from 'react'
import map from 'lodash/map'
import size from 'lodash/size'
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import type { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types'
import { T9n } from 'features/T9n'
import { getCorrectPaymentTabs } from './helpers'
type ListProps = {
countSubscriptions: number,
}
const List = styled.ul<ListProps>`
display: flex;
min-width: 395px;
justify-content: ${({ countSubscriptions }) => (countSubscriptions === 1
? 'center'
: 'space-between')};
${isMobileDevice
? css`
min-width: 90%;
width: 90%;
position: relative;
justify-content: center;
@media screen and (orientation: landscape){
margin-top: -5px;
width: 50%;
min-width: 50%;
}
`
: ''};
`
type ItemProps = {
active?: boolean,
}
const Item = styled.li<ItemProps>`
position: relative;
font-weight: 600;
font-size: 16px;
line-height: 47px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: color 0.3s;
::after {
transition: background-color 0.3s;
position: absolute;
content: '';
bottom: 0px;
width: 130px;
height: 3px;
}
${({ active }) => (
active
? css`
color: #fff;
::after {
background-color: #fff;
}
`
: ''
)}
${isMobileDevice
? css`
font-size: 10px;
width: 33%;
white-space: nowrap;
::after {
height: 2px;
}
`
: ''};
`
type Props = {
className?: string,
onPeriodSelect: (period: SubscriptionType) => void,
selectedPeriod: SubscriptionType,
selectedSubscription: MatchSubscription,
}
export const PaymentPeriodTabs = ({
className,
onPeriodSelect,
selectedPeriod,
selectedSubscription,
}: Props) => {
const matchSubscriptionsWithValues = useMemo(() => (
getCorrectPaymentTabs(selectedSubscription)
), [selectedSubscription])
return (
<List className={className} countSubscriptions={size(matchSubscriptionsWithValues)}>
{map(matchSubscriptionsWithValues, ({ tabLexic, type }) => (
<Item
key={type}
active={selectedPeriod === type}
onClick={() => onPeriodSelect(type)}
>
<T9n t={tabLexic} />
</Item>
))}
</List>
)
}

@ -63,6 +63,7 @@ export const usePlayerPage = () => {
return {
fetchMatches,
playerProfile,
profile,
teamId: playerProfile?.club_team?.id,
}

@ -1,4 +1,6 @@
import { usePageLogger } from 'hooks/usePageLogger'
import { isMobileDevice } from 'config'
import { usePageLogger } from 'hooks'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
@ -9,6 +11,7 @@ import {
Main,
Content,
} from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { usePlayerPage } from './hooks'
@ -16,13 +19,17 @@ const PlayerPage = () => {
usePageLogger()
const {
fetchMatches,
playerProfile,
profile,
teamId,
} = usePlayerPage()
return (
<PageWrapper>
<ProfileHeader profileId={teamId}>
<ProfileHeader
profileId={teamId}
color={playerProfile?.tournament.color}
>
{profile && <ProfileCard profile={profile} />}
</ProfileHeader>
<Main>
@ -31,6 +38,7 @@ const PlayerPage = () => {
<Matches fetch={fetchMatches} />
</Content>
</Main>
{!isMobileDevice && <BuyMatchPopup />}
</PageWrapper>
)
}

@ -1,51 +0,0 @@
import { currencySymbols } from 'config'
import { T9n } from 'features/T9n'
import {
Prefix,
PriceAmount,
PriceDetails,
PriceWrapper,
Currency,
Period,
} from '../../styled'
type Props = {
amount: number,
className?: string,
currency?: string,
isFrom?: boolean,
perPeriod?: string | null,
}
export const BasePrice = ({
amount,
className,
currency = currencySymbols.RUB,
isFrom,
perPeriod,
}: Props) => (
<PriceWrapper className={className}>
{
isFrom
? (
<Prefix>
<T9n t='from_price' />
</Prefix>
)
: ''
}
<PriceAmount>{amount}</PriceAmount>
<PriceDetails>
<Currency>{currency}</Currency>
{
perPeriod && (
<Period>
/ <T9n t={perPeriod} />
</Period>
)
}
</PriceDetails>
</PriceWrapper>
)

@ -1,51 +0,0 @@
import { currencySymbols } from 'config'
import { T9n } from 'features/T9n'
import {
Prefix,
PriceDetails,
PriceWrapper,
Currency,
Period,
} from '../../styled'
import { PriceAmount } from './styled'
type Props = {
amount: number,
className?: string,
currency?: string,
isFrom?: boolean,
perPeriod?: string | null,
}
export const BrazilianPrice = ({
amount,
className,
currency = currencySymbols.BRL,
isFrom,
perPeriod,
}: Props) => (
<PriceWrapper className={className}>
{
isFrom
? (
<Prefix>
<T9n t='from_price' />
</Prefix>
)
: ''
}
<PriceDetails>
<Currency>{currency}</Currency>
<PriceAmount>{amount.toFixed(2).replace('.', ',')}</PriceAmount>
{
perPeriod && (
<Period>
/ <T9n t={perPeriod} />
</Period>
)
}
</PriceDetails>
</PriceWrapper>
)

@ -1,6 +0,0 @@
import styled from 'styled-components'
import { PriceAmount as BasePriceAmount } from '../../styled'
export const PriceAmount = styled(BasePriceAmount)`
margin-right: 4px;
`

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save