diff --git a/.drone.yml b/.drone.yml index 05861eed..02b7db11 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 diff --git a/Makefile b/Makefile index 22c0ada7..61a42fb1 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -135,7 +135,7 @@ tunisia-build: clean REACT_APP_ENV=staging \ REACT_APP_CLIENT=tunisia \ npm run build - + fqtv-build: clean REACT_APP_TYPE=ott \ REACT_APP_ENV=staging \ @@ -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 diff --git a/package-lock.json b/package-lock.json index a69b3e22..57fa0531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6a0a67bf..7101187a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Ads/components/AdComponent/hooks.tsx b/src/components/Ads/components/AdComponent/hooks.tsx new file mode 100644 index 00000000..de130741 --- /dev/null +++ b/src/components/Ads/components/AdComponent/hooks.tsx @@ -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, + } +} diff --git a/src/components/Ads/components/AdComponent/index.tsx b/src/components/Ads/components/AdComponent/index.tsx new file mode 100644 index 00000000..790eeb6a --- /dev/null +++ b/src/components/Ads/components/AdComponent/index.tsx @@ -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) => { + e.stopPropagation() + handleClose() + } + + const onLinkClick = () => { + link && sendBannerClickEvent(EventGA.CLICK) + } + + return ( + position && isOpenAd && isNeedToShow + ? ( + + {isOpenCloseBtn && ( + + )} + + {isVideo + ? ( + + ) + : ( + + )} + + + ) : null + ) +}) diff --git a/src/components/Ads/components/AdComponent/styled.tsx b/src/components/Ads/components/AdComponent/styled.tsx new file mode 100644 index 00000000..fa310903 --- /dev/null +++ b/src/components/Ads/components/AdComponent/styled.tsx @@ -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` + 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` + 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` + 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)` + 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`` diff --git a/src/components/Ads/components/MobileAd/index.tsx b/src/components/Ads/components/MobileAd/index.tsx new file mode 100644 index 00000000..4fc72105 --- /dev/null +++ b/src/components/Ads/components/MobileAd/index.tsx @@ -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) => { + e.stopPropagation() + handleClose() + } + + const onLinkClick = () => { + if (link) { + sendBannerClickEvent(EventGA.CLICK) + window.open(link, '_blank') + } + } + + return ( + position && isOpenAd && isNeedToShow ? ( + + {isOpenCloseBtn + && ( + + )} + {isVideo + ? + ) : null + ) +} diff --git a/src/components/Ads/components/MobileAd/styled.tsx b/src/components/Ads/components/MobileAd/styled.tsx new file mode 100644 index 00000000..8be5ded7 --- /dev/null +++ b/src/components/Ads/components/MobileAd/styled.tsx @@ -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` + position: relative; + width: 100%; + z-index: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '101' : '100')}; + + ${({ position }) => chooseStyle(position)}; +` + +export const AdsCloseButton = styled(CloseButton)` + 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` + 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` + 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' + } + }} +` diff --git a/src/components/Ads/helpers/calcMaxDurationAds.tsx b/src/components/Ads/helpers/calcMaxDurationAds.tsx new file mode 100644 index 00000000..d052d7bb --- /dev/null +++ b/src/components/Ads/helpers/calcMaxDurationAds.tsx @@ -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 +} diff --git a/src/components/Ads/helpers/index.tsx b/src/components/Ads/helpers/index.tsx new file mode 100644 index 00000000..6be4e5f8 --- /dev/null +++ b/src/components/Ads/helpers/index.tsx @@ -0,0 +1 @@ +export * from './isVideo' diff --git a/src/components/Ads/helpers/isVideo.tsx b/src/components/Ads/helpers/isVideo.tsx new file mode 100644 index 00000000..cb35abc1 --- /dev/null +++ b/src/components/Ads/helpers/isVideo.tsx @@ -0,0 +1,2 @@ +const regexp = /^https?:\/\/\S+(?:mp4)$/ +export const checkVideo = (url: string) => regexp.test(url) diff --git a/src/components/Ads/hooks.tsx b/src/components/Ads/hooks.tsx new file mode 100644 index 00000000..4ec84537 --- /dev/null +++ b/src/components/Ads/hooks.tsx @@ -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, + } +} diff --git a/src/components/Ads/index.tsx b/src/components/Ads/index.tsx new file mode 100644 index 00000000..3123c802 --- /dev/null +++ b/src/components/Ads/index.tsx @@ -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 ? ( + + {ads.map((ad: AdType) => ( + !isMobileDevice ? ( + + ) : ( + + ) + ))} + + ) : null +) diff --git a/src/components/Ads/styled.tsx b/src/components/Ads/styled.tsx new file mode 100644 index 00000000..51fda9f1 --- /dev/null +++ b/src/components/Ads/styled.tsx @@ -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; + `}} +` diff --git a/src/components/Ads/types.tsx b/src/components/Ads/types.tsx new file mode 100644 index 00000000..a8d7205a --- /dev/null +++ b/src/components/Ads/types.tsx @@ -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> + +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' diff --git a/src/components/Carousel/helpers/common.tsx b/src/components/Carousel/helpers/common.tsx new file mode 100644 index 00000000..3da87df8 --- /dev/null +++ b/src/components/Carousel/helpers/common.tsx @@ -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, + } +} diff --git a/src/components/Carousel/helpers/elements.tsx b/src/components/Carousel/helpers/elements.tsx new file mode 100644 index 00000000..72105e03 --- /dev/null +++ b/src/components/Carousel/helpers/elements.tsx @@ -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 = [] + + const { spaceBetween = 0 } = props + + const children: Array = Array.from(el?.children || []) + + coords = children.reduce>(( + 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, + 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 = [] + + const { spaceBetween = 0 } = props + + const width = getItemWidth({ + galleryWidth: listWidth, + itemsInSlide, + props, + }) + + coords = children.reduce>(( + 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, + 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) => { + 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 +} diff --git a/src/components/Carousel/helpers/index.tsx b/src/components/Carousel/helpers/index.tsx new file mode 100644 index 00000000..412e9262 --- /dev/null +++ b/src/components/Carousel/helpers/index.tsx @@ -0,0 +1,4 @@ +export * from './common' +export * from './elements' +export * from './math' +export * from './mappers' diff --git a/src/components/Carousel/helpers/mappers.tsx b/src/components/Carousel/helpers/mappers.tsx new file mode 100644 index 00000000..85e3249c --- /dev/null +++ b/src/components/Carousel/helpers/mappers.tsx @@ -0,0 +1,12 @@ +import type { ItemCoords } from '../types' + +export const mapPartialCoords = (coords: Array) => ( + coords.map(({ width }) => ({ position: 0, width })) +) + +export const mapPositionCoords = (coords: Array, position = 0) => coords.map((item) => { + if (item.position > position) { + return { ...item, position } + } + return item +}) diff --git a/src/components/Carousel/helpers/math.tsx b/src/components/Carousel/helpers/math.tsx new file mode 100644 index 00000000..dadf3054 --- /dev/null +++ b/src/components/Carousel/helpers/math.tsx @@ -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 = [], + position = 0, +) => transformationSet.findIndex((item) => item.position >= Math.abs(position)) + diff --git a/src/components/Carousel/hooks.tsx b/src/components/Carousel/hooks.tsx new file mode 100644 index 00000000..05ebb207 --- /dev/null +++ b/src/components/Carousel/hooks.tsx @@ -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(Utils.calculateInitialState(props, null)) + const isAnimationDisabledRef = useRef(false) + const slideEndTimeoutIdRef = useRef(null) + const listElementRef = useRef(null) + const rootElementRef = useRef(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 ( + + {item} + + ) + } + + 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, + } +} diff --git a/src/components/Carousel/index.tsx b/src/components/Carousel/index.tsx new file mode 100644 index 00000000..61521613 --- /dev/null +++ b/src/components/Carousel/index.tsx @@ -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) => ( + + + +) + +export const Carousel = (props: Props) => { + const { + infinite, + renderNextButton, + renderPrevButton, + } = props + + const { + activeIndex, + clones, + itemsCount, + itemsInSlide, + listElementRef, + listElementStyles, + renderItem, + rootElementRef, + slideNext, + slidePrev, + } = useCarousel(props) + + return ( + + + {clones.map(renderItem)} + + + {renderPrevButton + ? renderPrevButton({ + disabled: !infinite && activeIndex === 0, + onClick: slidePrev, + }) + : ( + + )} + {renderNextButton + ? renderNextButton({ + disabled: !infinite && itemsInSlide + activeIndex === itemsCount, + onClick: slideNext, + }) + : ( + + )} + + + ) +} diff --git a/src/components/Carousel/styled.tsx b/src/components/Carousel/styled.tsx new file mode 100644 index 00000000..bf8b3e76 --- /dev/null +++ b/src/components/Carousel/styled.tsx @@ -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; +` diff --git a/src/components/Carousel/types.tsx b/src/components/Carousel/types.tsx new file mode 100644 index 00000000..f7eea746 --- /dev/null +++ b/src/components/Carousel/types.tsx @@ -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, + 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, + infinite?: boolean, + itemsCount: number, + itemsInSlide: number, + itemsOffset: number, + listWidth: number, + transformationSet: Array, + transition: string, + translate3d: number, + variableSizing: boolean, +} diff --git a/src/config/clients/facr.tsx b/src/config/clients/facr.tsx index a05987e8..b5d4d8c5 100644 --- a/src/config/clients/facr.tsx +++ b/src/config/clients/facr.tsx @@ -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: { diff --git a/src/config/clients/fqtv.tsx b/src/config/clients/fqtv.tsx index 538e559e..ca98cd46 100644 --- a/src/config/clients/fqtv.tsx +++ b/src/config/clients/fqtv.tsx @@ -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, diff --git a/src/config/clients/india.tsx b/src/config/clients/india.tsx index 3cf869c5..79d9ade4 100644 --- a/src/config/clients/india.tsx +++ b/src/config/clients/india.tsx @@ -17,5 +17,7 @@ export const india: ClientConfig = { sign: 'Rupee', }, disabledHighlights: true, + host: 'india.insports.tv', name: ClientNames.India, + userAccountCardsHidden: true, } diff --git a/src/config/clients/insports.tsx b/src/config/clients/insports.tsx index 4a80cb4b..592bbb1b 100644 --- a/src/config/clients/insports.tsx +++ b/src/config/clients/insports.tsx @@ -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, diff --git a/src/config/clients/instat.tsx b/src/config/clients/instat.tsx index c57d0a53..d10ce351 100644 --- a/src/config/clients/instat.tsx +++ b/src/config/clients/instat.tsx @@ -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, diff --git a/src/config/clients/lff.tsx b/src/config/clients/lff.tsx index 02dedb6b..ae710046 100644 --- a/src/config/clients/lff.tsx +++ b/src/config/clients/lff.tsx @@ -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, diff --git a/src/config/clients/tunisia.tsx b/src/config/clients/tunisia.tsx index e27d68a8..68b29253 100644 --- a/src/config/clients/tunisia.tsx +++ b/src/config/clients/tunisia.tsx @@ -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, } diff --git a/src/config/clients/types.tsx b/src/config/clients/types.tsx index d7b21fac..aa6672a9 100644 --- a/src/config/clients/types.tsx +++ b/src/config/clients/types.tsx @@ -42,6 +42,7 @@ export type ClientConfig = { disabledFilters?: boolean, disabledHighlights?: boolean, disabledPreferences?: boolean, + host: string, name: ClientNames, privacyLink: string, requests?: Record, @@ -64,5 +65,6 @@ export type ClientConfig = { }, termsLink: string, title: string, + userAccountCardsHidden?: boolean, userAccountLinksDisabled?: boolean, } diff --git a/src/config/index.tsx b/src/config/index.tsx index a5fd70ae..792f6aeb 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -12,3 +12,5 @@ export * from './userAgent' export * from './queries' export * from './keyboardKeys' export * from './clients' +export * from './localStorageKeys' +export * from './payments' diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index a4c9227f..0c3b5835 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -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, } diff --git a/src/config/lexics/landingLexics.tsx b/src/config/lexics/landingLexics.tsx index b6184100..fb81f158 100644 --- a/src/config/lexics/landingLexics.tsx +++ b/src/config/lexics/landingLexics.tsx @@ -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, } diff --git a/src/config/localStorageKeys.tsx b/src/config/localStorageKeys.tsx new file mode 100644 index 00000000..c571f751 --- /dev/null +++ b/src/config/localStorageKeys.tsx @@ -0,0 +1 @@ +export const COUNTRY = 'COUNTRY' diff --git a/src/config/pages.tsx b/src/config/pages.tsx index a3ca9614..410e3e23 100644 --- a/src/config/pages.tsx +++ b/src/config/pages.tsx @@ -7,6 +7,7 @@ export const PAGES = { mailings: '/useraccount/mailings', match: '/matches', player: '/players', + subscriptions: '/subscriptions', team: '/teams', thanksForSubscribe: '/thanks-for-subscription', tournament: '/tournaments', diff --git a/src/config/payments.tsx b/src/config/payments.tsx index 56b5a5a0..cb803b31 100644 --- a/src/config/payments.tsx +++ b/src/config/payments.tsx @@ -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, diff --git a/src/config/queries.tsx b/src/config/queries.tsx index 79580931..45fbda9d 100644 --- a/src/config/queries.tsx +++ b/src/config/queries.tsx @@ -1,4 +1,5 @@ export const querieKeys = { + ads: 'ads', liveMatchScores: 'liveMatchScores', matchScore: 'matchScore', sportsList: 'sportsList', diff --git a/src/config/routes.tsx b/src/config/routes.tsx index 15a67b41..c7d33438 100644 --- a/src/config/routes.tsx +++ b/src/config/routes.tsx @@ -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', }, } diff --git a/src/config/userAgent.tsx b/src/config/userAgent.tsx index 15dcc250..f0f11cfb 100644 --- a/src/config/userAgent.tsx +++ b/src/config/userAgent.tsx @@ -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) diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx index cddfc571..7cb4c33a 100644 --- a/src/features/App/AuthenticatedApp.tsx +++ b/src/features/App/AuthenticatedApp.tsx @@ -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 = () => { - { client.name === 'facr' ? : } {/* в Switch как прямой children @@ -98,6 +101,11 @@ export const AuthenticatedApp = () => { + {isMobileDevice && ( + + + + )} {!isProduction && } diff --git a/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx b/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx index 8fa2f3f9..f7209771 100644 --- a/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx +++ b/src/features/AuthServiceApp/components/ChangePassword/hooks.tsx @@ -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(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 = ({ diff --git a/src/features/AuthServiceApp/config/clients/index.tsx b/src/features/AuthServiceApp/config/clients/index.tsx index 79309820..6fa1a439 100644 --- a/src/features/AuthServiceApp/config/clients/index.tsx +++ b/src/features/AuthServiceApp/config/clients/index.tsx @@ -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, diff --git a/src/features/AuthServiceApp/config/clients/types.tsx b/src/features/AuthServiceApp/config/clients/types.tsx index c785db54..546b0b60 100644 --- a/src/features/AuthServiceApp/config/clients/types.tsx +++ b/src/features/AuthServiceApp/config/clients/types.tsx @@ -12,6 +12,7 @@ export type ClientConfig = { background: FC<{ children: ReactNode }>, defaultLanguage: string, description: string, + host?: string, isHideSelectLanguages?: boolean, name: ClientNames, privacyLink: string, diff --git a/src/features/BuyMatchPopup/components/CardStep/index.tsx b/src/features/BuyMatchPopup/components/CardStep/index.tsx index a0519f15..03eb1406 100644 --- a/src/features/BuyMatchPopup/components/CardStep/index.tsx +++ b/src/features/BuyMatchPopup/components/CardStep/index.tsx @@ -51,7 +51,7 @@ export const CardStep = ({ return (
- + {isHighlightsPage ? '' : } diff --git a/src/features/BuyMatchPopup/components/ErrorStep/index.tsx b/src/features/BuyMatchPopup/components/ErrorStep/index.tsx index 461d99c0..abfc90a3 100644 --- a/src/features/BuyMatchPopup/components/ErrorStep/index.tsx +++ b/src/features/BuyMatchPopup/components/ErrorStep/index.tsx @@ -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 (
@@ -29,7 +40,7 @@ export const ErrorStep = () => {
- + Ок
diff --git a/src/features/BuyMatchPopup/components/IframePayment/hooks.tsx b/src/features/BuyMatchPopup/components/IframePayment/hooks.tsx index 54b3f4d3..d12f5cae 100644 --- a/src/features/BuyMatchPopup/components/IframePayment/hooks.tsx +++ b/src/features/BuyMatchPopup/components/IframePayment/hooks.tsx @@ -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 let timeout: ReturnType @@ -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') } diff --git a/src/features/BuyMatchPopup/components/PackageMobile/index.tsx b/src/features/BuyMatchPopup/components/PackageMobile/index.tsx new file mode 100644 index 00000000..55993ef9 --- /dev/null +++ b/src/features/BuyMatchPopup/components/PackageMobile/index.tsx @@ -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, 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) => { + setSelectedPackage(matchPackage) + onButtonClick?.(e, matchPackage) + setLoader(true) + } + + return ( + +
+ + <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> + ) +} diff --git a/src/features/BuyMatchPopup/components/PackageMobile/styled.tsx b/src/features/BuyMatchPopup/components/PackageMobile/styled.tsx new file mode 100644 index 00000000..9c99c7d9 --- /dev/null +++ b/src/features/BuyMatchPopup/components/PackageMobile/styled.tsx @@ -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); +` diff --git a/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx b/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx index 2fcec4fc..f65af167 100644 --- a/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx +++ b/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx @@ -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> diff --git a/src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx b/src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx new file mode 100644 index 00000000..76fe3def --- /dev/null +++ b/src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx @@ -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; +` diff --git a/src/features/BuyMatchPopup/components/Packages/index.tsx b/src/features/BuyMatchPopup/components/Packages/index.tsx index 749f674a..626af78c 100644 --- a/src/features/BuyMatchPopup/components/Packages/index.tsx +++ b/src/features/BuyMatchPopup/components/Packages/index.tsx @@ -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> ) } diff --git a/src/features/BuyMatchPopup/components/Packages/styled.tsx b/src/features/BuyMatchPopup/components/Packages/styled.tsx new file mode 100644 index 00000000..4e8e2e53 --- /dev/null +++ b/src/features/BuyMatchPopup/components/Packages/styled.tsx @@ -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; +` diff --git a/src/features/BuyMatchPopup/components/PackagesList/index.tsx b/src/features/BuyMatchPopup/components/PackagesList/index.tsx deleted file mode 100644 index a92415c2..00000000 --- a/src/features/BuyMatchPopup/components/PackagesList/index.tsx +++ /dev/null @@ -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> -) diff --git a/src/features/BuyMatchPopup/components/PackagesList/styled.tsx b/src/features/BuyMatchPopup/components/PackagesList/styled.tsx deleted file mode 100644 index ba648f67..00000000 --- a/src/features/BuyMatchPopup/components/PackagesList/styled.tsx +++ /dev/null @@ -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; -` diff --git a/src/features/BuyMatchPopup/components/RegularPackage/index.tsx b/src/features/BuyMatchPopup/components/RegularPackage/index.tsx new file mode 100644 index 00000000..3fbceeab --- /dev/null +++ b/src/features/BuyMatchPopup/components/RegularPackage/index.tsx @@ -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> + ) +} diff --git a/src/features/BuyMatchPopup/components/RegularPackage/styled.tsx b/src/features/BuyMatchPopup/components/RegularPackage/styled.tsx new file mode 100644 index 00000000..c8137834 --- /dev/null +++ b/src/features/BuyMatchPopup/components/RegularPackage/styled.tsx @@ -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; + } +` diff --git a/src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx b/src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx new file mode 100644 index 00000000..175bad64 --- /dev/null +++ b/src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx @@ -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, + } +} diff --git a/src/features/BuyMatchPopup/components/SelectSubscription/index.tsx b/src/features/BuyMatchPopup/components/SelectSubscription/index.tsx deleted file mode 100644 index be9f9513..00000000 --- a/src/features/BuyMatchPopup/components/SelectSubscription/index.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx b/src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx deleted file mode 100644 index eab2b6aa..00000000 --- a/src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx +++ /dev/null @@ -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; -` diff --git a/src/features/BuyMatchPopup/components/SelectedCard/index.tsx b/src/features/BuyMatchPopup/components/SelectedCard/index.tsx index a7ccfc80..add44b85 100644 --- a/src/features/BuyMatchPopup/components/SelectedCard/index.tsx +++ b/src/features/BuyMatchPopup/components/SelectedCard/index.tsx @@ -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> diff --git a/src/features/BuyMatchPopup/components/SinglePackage/index.tsx b/src/features/BuyMatchPopup/components/SinglePackage/index.tsx new file mode 100644 index 00000000..8161a12b --- /dev/null +++ b/src/features/BuyMatchPopup/components/SinglePackage/index.tsx @@ -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> + ) +} diff --git a/src/features/BuyMatchPopup/components/SinglePackage/styled.tsx b/src/features/BuyMatchPopup/components/SinglePackage/styled.tsx new file mode 100644 index 00000000..cebb3bc1 --- /dev/null +++ b/src/features/BuyMatchPopup/components/SinglePackage/styled.tsx @@ -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; + } +` + diff --git a/src/features/BuyMatchPopup/index.tsx b/src/features/BuyMatchPopup/index.tsx index a0932bc6..8d473cd9 100644 --- a/src/features/BuyMatchPopup/index.tsx +++ b/src/features/BuyMatchPopup/index.tsx @@ -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 /> diff --git a/src/features/BuyMatchPopup/store/helpers.tsx b/src/features/BuyMatchPopup/store/helpers.tsx index 33480d7d..2b7419ca 100644 --- a/src/features/BuyMatchPopup/store/helpers.tsx +++ b/src/features/BuyMatchPopup/store/helpers.tsx @@ -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, diff --git a/src/features/BuyMatchPopup/store/hooks/index.tsx b/src/features/BuyMatchPopup/store/hooks/index.tsx index 2f16a9d7..a385d21c 100644 --- a/src/features/BuyMatchPopup/store/hooks/index.tsx +++ b/src/features/BuyMatchPopup/store/hooks/index.tsx @@ -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, diff --git a/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx b/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx index 7855c7a2..9fce37d2 100644 --- a/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx +++ b/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx @@ -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, diff --git a/src/features/BuyMatchPopup/styled.tsx b/src/features/BuyMatchPopup/styled.tsx index 5b95c481..2d86e5a6 100644 --- a/src/features/BuyMatchPopup/styled.tsx +++ b/src/features/BuyMatchPopup/styled.tsx @@ -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; diff --git a/src/features/BuyMatchPopup/types.tsx b/src/features/BuyMatchPopup/types.tsx index 370fc8cb..e8145b99 100644 --- a/src/features/BuyMatchPopup/types.tsx +++ b/src/features/BuyMatchPopup/types.tsx @@ -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, diff --git a/src/features/HeaderFilters/components/DateFilter/hooks/index.tsx b/src/features/HeaderFilters/components/DateFilter/hooks/index.tsx index 7225e678..7bda0907 100644 --- a/src/features/HeaderFilters/components/DateFilter/hooks/index.tsx +++ b/src/features/HeaderFilters/components/DateFilter/hooks/index.tsx @@ -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]) diff --git a/src/features/HomePage/hooks.tsx b/src/features/HomePage/hooks.tsx index 3cfc48f9..4fb99efc 100644 --- a/src/features/HomePage/hooks.tsx +++ b/src/features/HomePage/hooks.tsx @@ -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, diff --git a/src/features/HomePage/index.tsx b/src/features/HomePage/index.tsx index f5d94791..2b90ebc0 100644 --- a/src/features/HomePage/index.tsx +++ b/src/features/HomePage/index.tsx @@ -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> ) diff --git a/src/features/Landings/hooks.tsx b/src/features/Landings/hooks.tsx index bdad0f12..3b07af12 100644 --- a/src/features/Landings/hooks.tsx +++ b/src/features/Landings/hooks.tsx @@ -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, diff --git a/src/features/Landings/index.tsx b/src/features/Landings/index.tsx index c25cf191..4f2b98ac 100644 --- a/src/features/Landings/index.tsx +++ b/src/features/Landings/index.tsx @@ -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' /> {season} + </> + ) : <T9n t='default_season' />}  </DateInfo> diff --git a/src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx b/src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx index 5b30d040..f7850103 100644 --- a/src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx +++ b/src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx @@ -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 ? ( diff --git a/src/features/MatchCard/CardFrontside/MatchCardMobile/styled.tsx b/src/features/MatchCard/CardFrontside/MatchCardMobile/styled.tsx index 773b761e..a5f32dcf 100644 --- a/src/features/MatchCard/CardFrontside/MatchCardMobile/styled.tsx +++ b/src/features/MatchCard/CardFrontside/MatchCardMobile/styled.tsx @@ -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%; diff --git a/src/features/MatchCard/CardFrontside/index.tsx b/src/features/MatchCard/CardFrontside/index.tsx index 4b1c5d2b..0c1b047a 100644 --- a/src/features/MatchCard/CardFrontside/index.tsx +++ b/src/features/MatchCard/CardFrontside/index.tsx @@ -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} diff --git a/src/features/MatchCard/hooks.tsx b/src/features/MatchCard/hooks.tsx index 2083e3af..696d3272 100644 --- a/src/features/MatchCard/hooks.tsx +++ b/src/features/MatchCard/hooks.tsx @@ -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>) => { diff --git a/src/features/MatchCard/styled.tsx b/src/features/MatchCard/styled.tsx index adc35f51..3d5d3677 100644 --- a/src/features/MatchCard/styled.tsx +++ b/src/features/MatchCard/styled.tsx @@ -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 diff --git a/src/features/MatchPage/components/SubscriptionGuard/index.tsx b/src/features/MatchPage/components/SubscriptionGuard/index.tsx index c9cb51ac..4f341705 100644 --- a/src/features/MatchPage/components/SubscriptionGuard/index.tsx +++ b/src/features/MatchPage/components/SubscriptionGuard/index.tsx @@ -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 ( diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 4a0fb852..13de524f 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -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> ) diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index 280c4ede..96ccef89 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -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, diff --git a/src/features/MatchSidePlaylists/components/EventsList/index.tsx b/src/features/MatchSidePlaylists/components/EventsList/index.tsx index 6bbbfaca..aa09063f 100644 --- a/src/features/MatchSidePlaylists/components/EventsList/index.tsx +++ b/src/features/MatchSidePlaylists/components/EventsList/index.tsx @@ -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 ( diff --git a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx index f9f8766e..b66725a6 100644 --- a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx @@ -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 diff --git a/src/features/MatchSidePlaylists/components/TabEvents/index.tsx b/src/features/MatchSidePlaylists/components/TabEvents/index.tsx index c8a97fdf..b09252f5 100644 --- a/src/features/MatchSidePlaylists/components/TabEvents/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabEvents/index.tsx @@ -129,6 +129,7 @@ export const TabEvents = ({ profile={profile} selectedPlaylist={selectedPlaylist} setWatchAllEpisodesTimer={setWatchAllEpisodesTimer} + isFirstBlock={isFirstBlock} /> </HalfEvents> ) diff --git a/src/features/MatchSidePlaylists/components/TabEvents/styled.tsx b/src/features/MatchSidePlaylists/components/TabEvents/styled.tsx index 552961b3..311908b8 100644 --- a/src/features/MatchSidePlaylists/components/TabEvents/styled.tsx +++ b/src/features/MatchSidePlaylists/components/TabEvents/styled.tsx @@ -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%; diff --git a/src/features/MatchSidePlaylists/components/TabWatch/index.tsx b/src/features/MatchSidePlaylists/components/TabWatch/index.tsx index 8c7ab360..82da43d6 100644 --- a/src/features/MatchSidePlaylists/components/TabWatch/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabWatch/index.tsx @@ -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} diff --git a/src/features/MatchSidePlaylists/index.tsx b/src/features/MatchSidePlaylists/index.tsx index 4af4749a..8c1122c3 100644 --- a/src/features/MatchSidePlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/index.tsx @@ -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} diff --git a/src/features/MatchSidePlaylists/styled.tsx b/src/features/MatchSidePlaylists/styled.tsx index e4e15c3d..4a6894ac 100644 --- a/src/features/MatchSidePlaylists/styled.tsx +++ b/src/features/MatchSidePlaylists/styled.tsx @@ -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%; + ` + : ''}; +` diff --git a/src/features/MatchesGrid/index.tsx b/src/features/MatchesGrid/index.tsx index d2b93089..e8f6c451 100644 --- a/src/features/MatchesGrid/index.tsx +++ b/src/features/MatchesGrid/index.tsx @@ -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()} /> ) : ( diff --git a/src/features/MatchesGrid/styled.tsx b/src/features/MatchesGrid/styled.tsx index 7bcbc34d..18382782 100644 --- a/src/features/MatchesGrid/styled.tsx +++ b/src/features/MatchesGrid/styled.tsx @@ -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; diff --git a/src/features/PaymentPeriodTabs/helpers.tsx b/src/features/PaymentPeriodTabs/helpers.tsx deleted file mode 100644 index e8ade685..00000000 --- a/src/features/PaymentPeriodTabs/helpers.tsx +++ /dev/null @@ -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> -} diff --git a/src/features/PaymentPeriodTabs/index.tsx b/src/features/PaymentPeriodTabs/index.tsx deleted file mode 100644 index 9e4c7beb..00000000 --- a/src/features/PaymentPeriodTabs/index.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/features/PlayerPage/hooks.tsx b/src/features/PlayerPage/hooks.tsx index eb56f013..c2b597c6 100644 --- a/src/features/PlayerPage/hooks.tsx +++ b/src/features/PlayerPage/hooks.tsx @@ -63,6 +63,7 @@ export const usePlayerPage = () => { return { fetchMatches, + playerProfile, profile, teamId: playerProfile?.club_team?.id, } diff --git a/src/features/PlayerPage/index.tsx b/src/features/PlayerPage/index.tsx index 56004820..fa282812 100644 --- a/src/features/PlayerPage/index.tsx +++ b/src/features/PlayerPage/index.tsx @@ -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> ) } diff --git a/src/features/Price/components/BasePrice/index.tsx b/src/features/Price/components/BasePrice/index.tsx deleted file mode 100644 index 4080f83a..00000000 --- a/src/features/Price/components/BasePrice/index.tsx +++ /dev/null @@ -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> -) diff --git a/src/features/Price/components/BrazilianPrice/index.tsx b/src/features/Price/components/BrazilianPrice/index.tsx deleted file mode 100644 index d62bb6b0..00000000 --- a/src/features/Price/components/BrazilianPrice/index.tsx +++ /dev/null @@ -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> -) diff --git a/src/features/Price/components/BrazilianPrice/styled.tsx b/src/features/Price/components/BrazilianPrice/styled.tsx deleted file mode 100644 index 69bbe934..00000000 --- a/src/features/Price/components/BrazilianPrice/styled.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import styled from 'styled-components' -import { PriceAmount as BasePriceAmount } from '../../styled' - -export const PriceAmount = styled(BasePriceAmount)` - margin-right: 4px; -` diff --git a/src/features/Price/index.tsx b/src/features/Price/index.tsx index 1c1d85d5..25971f9f 100644 --- a/src/features/Price/index.tsx +++ b/src/features/Price/index.tsx @@ -1,48 +1,39 @@ import { currencySymbols } from 'config' -import { useMemo } from 'react' -import { BasePrice } from './components/BasePrice' -import { BrazilianPrice } from './components/BrazilianPrice' +import { + PriceAmount, + PriceDetails, + PriceWrapper, + Currency, + Period, + Divider, + PerPeriod, +} from './styled' -export type PriceProps = { +type Props = { amount: number, className?: string, currency?: string, - isFrom?: boolean, perPeriod?: string | null, } -export const Price: React.FC<PriceProps> = ({ +export const Price = ({ amount, className, - currency, - isFrom, + currency = currencySymbols.RUB, perPeriod, -}) => { - const priceContent = useMemo(() => { - switch (currency) { - case currencySymbols.BRL: - return ( - <BrazilianPrice - amount={amount} - className={className} - currency={currency} - isFrom={isFrom} - perPeriod={perPeriod} - /> +}: Props) => ( + <PriceWrapper className={className}> + <PriceAmount>{amount}</PriceAmount> + <PriceDetails> + <Currency>{currency}</Currency> + { + perPeriod && ( + <Period> + <Divider /><PerPeriod t={perPeriod} /> + </Period> ) - default: - return ( - <BasePrice - amount={amount} - className={className} - currency={currency} - isFrom={isFrom} - perPeriod={perPeriod} - /> - ) - } - }, [amount, className, currency, isFrom, perPeriod]) - - return priceContent -} + } + </PriceDetails> + </PriceWrapper> +) diff --git a/src/features/Price/styled.tsx b/src/features/Price/styled.tsx index 4b583c49..74fd7831 100644 --- a/src/features/Price/styled.tsx +++ b/src/features/Price/styled.tsx @@ -1,62 +1,48 @@ -import styled, { css } from 'styled-components/macro' +import styled from 'styled-components/macro' -import { isMobileDevice } from 'config/userAgent' -import { devices } from 'config/devices' +import { isMobileDevice } from 'config' + +import { T9n } from 'features/T9n' export const PriceWrapper = styled.div` display: flex; align-items: flex-start; - - @media ${devices.tablet} { - justify-content: center; - } - ${isMobileDevice - ? css` - min-width: 80px; - ` - : ''}; ` export const PriceAmount = styled.span` font-style: normal; - font-weight: 600; - font-size: 48px; - line-height: 40px; + font-weight: 400; + font-size: 32px; color: ${({ theme: { colors } }) => colors.white}; - - @media ${devices.tablet} { - font-size: 36px; - } ` export const PriceDetails = styled.span` + display: flex; margin-left: 5px; font-style: normal; font-weight: normal; - font-size: 18px; - line-height: 21px; + font-size: 30px; color: ${({ theme: { colors } }) => colors.white}; - text-transform: uppercase; - - @media ${devices.tablet} { - font-size: 11px; - } ` export const Currency = styled.span` text-transform: uppercase; - margin-right: 4px; + font-size: ${isMobileDevice ? 8 : 10}px; ` export const Period = styled.span` - text-transform: uppercase; + font-style: italic; + font-weight: 400; + font-size: ${isMobileDevice ? 8 : 10}px; ` -export const Prefix = styled.span` - padding-top: 5px; - text-transform: lowercase; - line-height: 40px; - font-size: 12px; - font-weight: normal; - margin-right: 5px; +export const Divider = styled.div` + display: inline-block; + width: 0.5px; + height: 8px; + margin: 0 2px -1px 4px; + transform: skew(-20deg, 0); + background-color: ${({ theme: { colors } }) => colors.white}; ` + +export const PerPeriod = styled(T9n)`` diff --git a/src/features/ProfileHeader/hooks.tsx b/src/features/ProfileHeader/hooks.tsx deleted file mode 100644 index 9e353386..00000000 --- a/src/features/ProfileHeader/hooks.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useState } from 'react' - -import { getProfileColor } from 'requests/getProfileColor' - -import { usePageParams } from 'hooks/usePageParams' -import { getColor } from 'helpers/getColor' - -import { client } from 'config/clients' -import { DEFAULT_HEADER_COLOR } from './styled' - -export const useProfileColor = (profileId?: number) => { - const { - profileType, - sportType, - } = usePageParams() - const [color, setColor] = useState(DEFAULT_HEADER_COLOR) - - useEffect(() => { - if (!profileId) return - - getProfileColor({ - profileId, - profileType, - sportType, - }).then(setColor) - }, [ - profileId, - profileType, - sportType, - ]) - - // TODO remove this logic when backend will return the correct colors - const prifileWithConfifColor = [ - 227, 946, 3067, 5665, 23, 2719, 528, 17018, 567, 16306, 1189, 480, 16920, 6032, 17624, 114440, - ] - if ( - client.name === 'facr' - && sportType === 1 && profileId && prifileWithConfifColor.includes(profileId) - ) { - return getColor(profileId) - } - const lffColorConfig = [ - 262, - 928, - 1620, - 5858, - 5975, - 5976, - 6004, - 1000045, - 1000046, - 1000047, - 1000048, - ] - // eslint-disable-next-line postro4no/function-args - - if (client.name === 'lff' && profileId && lffColorConfig.includes(profileId)) { - return getColor(profileId) - } - return color -} diff --git a/src/features/ProfileHeader/index.tsx b/src/features/ProfileHeader/index.tsx index bbed7782..241a7664 100644 --- a/src/features/ProfileHeader/index.tsx +++ b/src/features/ProfileHeader/index.tsx @@ -14,7 +14,6 @@ import { Search } from 'features/Search' import { ScoreSwitch } from 'features/MatchSwitches' import { usePageParams } from 'hooks/usePageParams' -import { useProfileColor } from './hooks' import { HeaderStyled, @@ -48,7 +47,6 @@ export const ProfileHeader = ({ sportType, } = usePageParams() - const color = useProfileColor(profileId) // @ts-ignore const imageHeader = Boolean(profileWithImage[profileId]) && sportType === 1 @@ -57,7 +55,7 @@ export const ProfileHeader = ({ return ( <HeaderStyled headerImage={imageHeader} - color={headerColor || color} + color={headerColor} height={height} > <Position diff --git a/src/features/ProfileHeader/styled.tsx b/src/features/ProfileHeader/styled.tsx index 03f0d4f6..a02779f8 100644 --- a/src/features/ProfileHeader/styled.tsx +++ b/src/features/ProfileHeader/styled.tsx @@ -11,10 +11,10 @@ import { ScoreSwitch } from 'features/MatchSwitches' import { isMatchPage } from 'helpers/isMatchPage' -export const DEFAULT_HEADER_COLOR = 'rgba(53, 96, 225, 0.56)' +export const DEFAULT_HEADER_COLOR = client.styles.homePageHeader?.toString() || 'rgba(53, 96, 225, 0.56)' export const defaultHeaderStyles = ( - color: string = DEFAULT_HEADER_COLOR, + color: string | undefined = DEFAULT_HEADER_COLOR, headerImage: string | undefined | null, ) => { if ([ @@ -36,21 +36,25 @@ export const defaultHeaderStyles = ( } ` } - if (color.includes('linear-gradient')) { + if (color) { return css` - background: ${color}; + background: linear-gradient( + 187deg, + ${color} -4.49%, + #000000 68.29%), + #000000; z-index: 10; -` + ` } return css` background: linear-gradient( 187deg, - ${color} -4.49%, + ${DEFAULT_HEADER_COLOR} -4.49%, #000000 68.29%), #000000; - z-index: 10; - ` + z-index: 10; + ` } type HeaderProps = { @@ -72,9 +76,11 @@ export const HeaderStyled = styled.header<HeaderProps>` client.name === ClientNames.Lff || client.name === ClientNames.Facr || client.name === ClientNames.Fqtv - || client.name === ClientNames.Tunisia ? css` - background: ${color}; - ` : '' + || client.name === ClientNames.Tunisia + ? css` + background: linear-gradient(187deg, ${color} -4.49%, #000000 68.29%), #000000; + z-index: 10; + ` : '' )} ${isMobileDevice diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index 787875a7..e3a764ca 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -118,6 +118,7 @@ export const useVideoPlayer = ({ playNextEpisode, selectedPlaylist, setCircleAnimation, + setIsFullScreen, setPlayingProgress, } = useMatchPageStore() @@ -673,6 +674,13 @@ export const useVideoPlayer = ({ profileId, ]) + useEffect(() => { + setIsFullScreen(isFullscreen) + }, [ + isFullscreen, + setIsFullScreen, + ]) + return { activeChapterIndex, allPlayedProgress: playedProgress + getActiveChapter().startMs, diff --git a/src/features/StreamPlayer/index.tsx b/src/features/StreamPlayer/index.tsx index e81c6261..b5d1d2bc 100644 --- a/src/features/StreamPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -1,5 +1,7 @@ import { Fragment } from 'react' +import includes from 'lodash/includes' + import { isMobileDevice } from 'config' import { Loader } from 'features/Loader' @@ -10,8 +12,15 @@ import { FiltersPopup } from 'features/MatchSidePlaylists/components/FiltersPopu import { WaterMark } from 'components/WaterMark' import { AccessTimer } from 'components/AccessTimer' +import { PLAYER_ADS, PLAYER_MOBILE_ADS } from 'components/Ads/types' +import { HeaderAds } from 'components/Ads' import { REWIND_SECONDS } from './config' +import type { Props } from './hooks' +import { useVideoPlayer } from './hooks' +import { useAuthStore } from '../AuthStore' +import { Controls } from './components/Controls' +import RewindMobile from './components/RewindMobile' import { PlayerWrapper, @@ -27,12 +36,8 @@ import { EpisodeInfoOrder, EpisodeInfoDivider, CloseButton, + PlayerAdsWrapper, } from './styled' -import type { Props } from './hooks' -import { useVideoPlayer } from './hooks' -import { useAuthStore } from '../AuthStore' -import { Controls } from './components/Controls' -import RewindMobile from './components/RewindMobile' const tournamentsWithWatermark = { 316: 'Tunisia', @@ -44,6 +49,7 @@ const tournamentsWithWatermark = { export const StreamPlayer = (props: Props) => { const { access, + ads, episodeInfo, isOpenFiltersPopup, isPlayingEpisode, @@ -120,6 +126,15 @@ export const StreamPlayer = (props: Props) => { onTouchEnd={onTouchEnd} isPlayingEpisode={isPlayingEpisode} > + {ads + && ( + <PlayerAdsWrapper isFullscreen={isFullscreen}> + <HeaderAds ads={ads.filter(({ position }) => (isMobileDevice + ? position.id === PLAYER_MOBILE_ADS + : includes(PLAYER_ADS, position.id)))} + /> + </PlayerAdsWrapper> + )} {isPlayingEpisode && ( <EpisodeInfo> <EpisodeInfoName> diff --git a/src/features/StreamPlayer/styled.tsx b/src/features/StreamPlayer/styled.tsx index 2a183d2c..1cab2be7 100644 --- a/src/features/StreamPlayer/styled.tsx +++ b/src/features/StreamPlayer/styled.tsx @@ -17,6 +17,10 @@ type HoverStylesProps = { visible: boolean, } +type PlayerAdsProps = { + isFullscreen: boolean, +} + export const hoverStyles = css<HoverStylesProps>` transition: opacity 0.3s ease-in-out; ${({ visible }) => (visible @@ -485,3 +489,8 @@ export const CloseButton = styled(CloseButtonBase)` height: max(1.2rem, 23px); } ` + +export const PlayerAdsWrapper = styled.div<PlayerAdsProps>` + opacity: ${({ isFullscreen }) => (isFullscreen ? 0 : 1)}; +` + diff --git a/src/features/TeamPage/hooks.tsx b/src/features/TeamPage/hooks.tsx index e8ab92d3..b92b9cae 100644 --- a/src/features/TeamPage/hooks.tsx +++ b/src/features/TeamPage/hooks.tsx @@ -66,5 +66,6 @@ export const useTeamPage = () => { profile, sportType, teamId, + teamProfile, } } diff --git a/src/features/TeamPage/index.tsx b/src/features/TeamPage/index.tsx index f8b57fef..45c062ce 100644 --- a/src/features/TeamPage/index.tsx +++ b/src/features/TeamPage/index.tsx @@ -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 { useTeamPage } from './hooks' @@ -19,11 +22,13 @@ const TeamPage = () => { headerImage, profile, teamId, + teamProfile, } = useTeamPage() return ( <PageWrapper> <ProfileHeader + color={teamProfile?.tournament.color} headerImage={headerImage} profileId={teamId} > @@ -35,6 +40,7 @@ const TeamPage = () => { <Matches fetch={fetchMatches} /> </Content> </Main> + {!isMobileDevice && <BuyMatchPopup />} </PageWrapper> ) } diff --git a/src/features/TournamentList/components/TournamentMobile/index.tsx b/src/features/TournamentList/components/TournamentMobile/index.tsx index 233eed92..8bed7cd4 100644 --- a/src/features/TournamentList/components/TournamentMobile/index.tsx +++ b/src/features/TournamentList/components/TournamentMobile/index.tsx @@ -1,9 +1,15 @@ import { Fragment, useState } from 'react' +import { useRecoilValue } from 'recoil' + import { isLffClient } from 'config/clients' -import { URL_AWS } from 'config' +import { + isMobileDevice, + URL_AWS, +} from 'config' + +import { AdType } from 'requests' -import { SportIcon } from 'components/SportIcon/SportIcon' import { T9n } from 'features/T9n' import { Icon } from 'features/Icon' import type { Match } from 'features/Matches' @@ -14,6 +20,11 @@ import { LiveSign, } from 'features/MatchCard/CardFrontside/MatchCardMobile/styled' +import { VIEW_ADS } from 'components/Ads/types' +import { SportIcon } from 'components/SportIcon/SportIcon' + +import { adsStore } from 'pages/HighlightsPage/storeHighlightsAtoms' + import { CardWrapperOuter, CardWrapper, @@ -22,6 +33,7 @@ import { ScFirstInfo, ScMatchesWrapper, ScSecondInfo, + TournamentMobileAd, } from './styled' import { TournamentProps } from '../..' @@ -40,6 +52,8 @@ export const TournamentMobile = ({ const currentColor = open ? '#ffffff' : 'rgba(255, 255, 255, 0.5)' + const ads = useRecoilValue(adsStore) + return ( <CardWrapperOuter> <CardWrapper @@ -75,10 +89,27 @@ export const TournamentMobile = ({ </ScSecondInfo> </CardWrapper> <ScMatchesWrapper> - {open - && tournamentMatches?.map((match: Match) => ( - <MatchCard key={match.id} match={match} /> - ))} + {open && ( + <Fragment> + {isMobileDevice && ads.mobile?.map((ad: AdType) => ( + ad.position.id === VIEW_ADS.MOBILE_IN_COLLAPSE_HEADER && ( + <TournamentMobileAd + ad={ad} + key={ad.id} + /> + )))} + {tournamentMatches?.map((match: Match) => ( + <MatchCard key={match.id} match={match} /> + ))} + {isMobileDevice && ads.mobile?.map((ad: AdType) => ( + ad.position.id === VIEW_ADS.MOBILE_IN_COLLAPSE_FOOTER && ( + <TournamentMobileAd + ad={ad} + key={ad.id} + /> + )))} + </Fragment> + )} </ScMatchesWrapper> </CardWrapperOuter> ) diff --git a/src/features/TournamentList/components/TournamentMobile/styled.tsx b/src/features/TournamentList/components/TournamentMobile/styled.tsx index 3a103dc1..1b4bbd7f 100644 --- a/src/features/TournamentList/components/TournamentMobile/styled.tsx +++ b/src/features/TournamentList/components/TournamentMobile/styled.tsx @@ -1,7 +1,10 @@ import styled, { css } from 'styled-components/macro' + import { Name } from 'features/Name' import { Icon } from 'features/Icon' +import { MobileAd } from 'components/Ads/components/MobileAd' + import { isMobileDevice } from 'config/userAgent' export const CardWrapperOuter = styled.li.attrs({ @@ -82,6 +85,10 @@ export const ScMatchesWrapper = styled.ul` flex-direction: column; ` +export const TournamentMobileAd = styled(MobileAd)` + margin: 6px 0 0; +` + export const ScStar = styled(Icon)` display: flex; justify-content: center; diff --git a/src/features/TournamentPage/hooks.tsx b/src/features/TournamentPage/hooks.tsx index 2e93603f..a7b428ee 100644 --- a/src/features/TournamentPage/hooks.tsx +++ b/src/features/TournamentPage/hooks.tsx @@ -16,6 +16,7 @@ import { openSubscribePopup, redirectToUrl } from 'helpers' import { PAGES } from 'config' import { useName } from 'features/Name' +import { DEFAULT_HEADER_COLOR } from 'features/ProfileHeader/styled' import { checkUrlParams } from 'helpers/parseUrlParams/parseUrlParams' @@ -126,6 +127,7 @@ export const useTournamentPage = () => { }, []) return { + color: tournamentProfile?.color || DEFAULT_HEADER_COLOR, fetchMatches, headerImage: tournamentProfile?.header_image, infoItems: [country], diff --git a/src/features/TournamentPage/index.tsx b/src/features/TournamentPage/index.tsx index 662c4793..cf21dd64 100644 --- a/src/features/TournamentPage/index.tsx +++ b/src/features/TournamentPage/index.tsx @@ -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 { useTournamentPage } from './hooks' @@ -17,6 +20,7 @@ import { FavouriteTeamPopup } from '../MatchPage/components/FavouriteTeam' const TournamentPage = () => { usePageLogger() const { + color, fetchMatches, headerImage, profile, @@ -27,6 +31,7 @@ const TournamentPage = () => { return ( <PageWrapper> <ProfileHeader + color={color} headerImage={headerImage} profileId={tournamentId} > @@ -39,6 +44,7 @@ const TournamentPage = () => { </Content> </Main> {user && (tournamentId === 131 || tournamentId === 2032) && <FavouriteTeamPopup />} + {!isMobileDevice && <BuyMatchPopup />} </PageWrapper> ) } diff --git a/src/features/UserAccount/components/UserSubscriptionsList/index.tsx b/src/features/UserAccount/components/UserSubscriptionsList/index.tsx deleted file mode 100644 index 3a13cc44..00000000 --- a/src/features/UserAccount/components/UserSubscriptionsList/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import isEmpty from 'lodash/isEmpty' -import map from 'lodash/map' - -import type { MatchSubscriptions } from '../PageSubscriptions' -import { InlineButton } from '../../styled' -import { - Wrapper, - SportName, - List, - Item, - Subscription, - InfoWrapper, - Header, - Description, - Price, - SubscriptionEnd, -} from './styled' - -type Props = { - list: MatchSubscriptions, - sport: number, -} - -export const UserSubscriptionsList = ({ list, sport }: Props) => { - if (isEmpty(list)) return null - return ( - <Wrapper> - <SportName sport={sport} /> - <List> - { - map(list, ({ - description, - header, - isActive, - price, - subscription_id, - type, - }) => ( - <Item key={subscription_id}> - <Subscription> - <InfoWrapper> - <Header> - {header} - </Header> - <Description> - {description} - </Description> - </InfoWrapper> - - <Price amount={price} perPeriod={`per_${type}`} /> - <InlineButton color={isActive ? '#eb5757' : '#294FC3'}> - {isActive ? 'Удалить' : 'Восстановить'} - </InlineButton> - </Subscription> - <SubscriptionEnd>Следующее списание 31.02.2020</SubscriptionEnd> - </Item> - )) - } - </List> - </Wrapper> - ) -} diff --git a/src/features/UserAccount/components/UserSubscriptionsList/styled.tsx b/src/features/UserAccount/components/UserSubscriptionsList/styled.tsx deleted file mode 100644 index 2ab664a0..00000000 --- a/src/features/UserAccount/components/UserSubscriptionsList/styled.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import styled, { css } from 'styled-components/macro' - -import { isMobileDevice } from 'config/userAgent' - -import { SportName as SportNameBase } from 'features/Common/SportName' -import { Price as BasePrice } from 'features/Price' -import { PriceAmount, PriceDetails } from 'features/Price/styled' - -import { InlineButton } from '../../styled' - -export const Wrapper = styled.div` - :not(:first-child) { - margin-top: 24px; - } -` - -export const SportName = styled(SportNameBase)` - font-style: normal; - font-weight: 500; - font-size: 18px; - line-height: 22px; - color: rgba(255, 255, 255, 0.6); - ${isMobileDevice - ? css` - font-size: 14px; - ` - : ''} -` - -export const List = styled.ul` - margin-top: 6px; -` - -export const Item = styled.li` - display: flex; - align-items: center; - - ${isMobileDevice - ? css` - display: block; - ` - : ''} -` - -export const Price = styled(BasePrice)` - margin-right: 20px; - - ${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: 7px; - ` - : ''} - } -` - -export const Subscription = styled.div.attrs( - () => ({ - tabIndex: 0, - }), -)` - position: relative; - width: 800px; - height: 70px; - display: flex; - margin: 5px 0; - align-items: center; - justify-content: space-between; - background-color: #3F3F3F; - box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); - border-radius: 2px; - overflow: hidden; - - ${InlineButton} { - width: 133px; - } - - :focus-within, :hover { - ${InlineButton} { - transform: translateX(0); - } - } - ${isMobileDevice - ? css` - height: 86px; - width: 100%; - ` - : ''} -` - -export const InfoWrapper = styled.div` - width: 68%; - display: flex; - padding: 14px 0 14px 20px; - flex-direction: column; - color: #fff; - ${isMobileDevice - ? css` - padding: 10px 0 10px 15px; - ` - : ''} -` - -export const Header = styled.span` - font-style: normal; - font-weight: 600; - font-size: 16px; - line-height: 23px; - text-transform: uppercase; - ${isMobileDevice - ? css` - font-size: 14px; - line-height: 20px; - ` - : ''} -` - -export const Description = styled.p` - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 20px; - text-transform: capitalize; - ${isMobileDevice - ? css` - font-size: 10px; - line-height: 12px; - ` - : ''} -` - -export const SubscriptionEnd = styled.span` - margin-left: 24px; - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 24px; - color: rgba(255, 255, 255, 0.3); - ${isMobileDevice - ? css` - font-size: 10px; - margin-left: 0; - ` - : ''} -` diff --git a/src/features/UserAccount/index.tsx b/src/features/UserAccount/index.tsx index 62563155..bd4f44ed 100644 --- a/src/features/UserAccount/index.tsx +++ b/src/features/UserAccount/index.tsx @@ -54,13 +54,15 @@ const UserAccount = () => { </StyledLink> {!isLffClient && ( <Fragment> - <StyledLink - disabled={user?.profile?.country_code === 'BR'} - to={`${PAGES.useraccount}/bank-cards`} - id='personal_cards' - > - <T9n t='bank_card' /> - </StyledLink> + {!client.userAccountCardsHidden && ( + <StyledLink + disabled={user?.profile?.country_code === 'BR'} + to={`${PAGES.useraccount}/bank-cards`} + id='personal_cards' + > + <T9n t='bank_card' /> + </StyledLink> + )} <StyledLink to={`${PAGES.useraccount}/subscriptions`} id='personal_subscriptions' diff --git a/src/helpers/getCardColor/index.tsx b/src/helpers/getCardColor/index.tsx deleted file mode 100644 index 59890051..00000000 --- a/src/helpers/getCardColor/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { isMobileDevice } from 'config/userAgent' - -const degree = isMobileDevice ? '270deg' : '0deg' - -export const getCardColor = (id: number | undefined): string => { - const defaultColor = 'none' - - if (!id) return defaultColor - - switch (id) { - case 928: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(49,186,177,1) 100%);` - case 1620: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(96,24,75,1) 100%);` - case 5976: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(0,160,228,1) 100%);` - case 5975: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(225,27,74,1) 100%);` - case 5858: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(252,162,78, 1) 100%);` - case 262: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(192,166,96,1) 100%);` - case 6004: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(79,81,205,1) 100%);` - case 100045: - case 100046: - case 100047: - case 100048: - return `linear-gradient(${degree}, rgba(63,63,63,1) 35%, rgba(55, 179, 72,1) 100%);` - default: - return defaultColor - } -} diff --git a/src/helpers/getColor/index.ts b/src/helpers/getColor/index.ts deleted file mode 100644 index ae5cc766..00000000 --- a/src/helpers/getColor/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { client } from 'config/clients' - -const DEFAULT_FACR_COLOR = 'linear-gradient(83.42deg, #00257A 53.04%, rgba(0, 0, 0, 0) 94.83%), #000000' -const DEFAULT_INSTAT_COLOR = 'linear-gradient( 187deg, rgba(53,96,225,0.56) -4.49%, #000000 68.29%), #000000' - -export const getColor = (id: number | undefined): string => { - const currentDefaultColor = client.name === 'facr' ? DEFAULT_FACR_COLOR : DEFAULT_INSTAT_COLOR - - if (!id) return currentDefaultColor - switch (id) { - case 227: - return 'linear-gradient(83.42deg, #004A49 53.04%, #071616 94.83%), #000000' - case 946: - return 'linear-gradient(83.42deg, #000000 53.04%, #000000 94.83%), #000000' - case 3067: - return 'linear-gradient(83.42deg, #007179 53.04%, #007179 94.83%), #000000' - case 928: - return 'linear-gradient(90deg, rgba(21,83,79,1) 0%, rgba(49,186,177,1) 100%);' - case 1620: - return 'linear-gradient(90deg, rgba(47,4,39,1) 0%, rgba(96,24,75,1) 100%);' - case 5976: - return 'linear-gradient(90deg, rgba(13,94,129,1) 0%, rgba(0,160,228,1) 100%);' - case 5975: - return 'linear-gradient(90deg, rgba(140,13,43,1) 0%, rgba(225,27,74,1) 100%);' - case 5858: - return 'linear-gradient(90deg, rgba(184,99,21,1) 0%, rgba(252,162,78, 1) 100%);' - case 262: - return 'linear-gradient(90deg, rgba(114,103,76,1) 0%, rgba(192,166,96,1) 100%);' - case 6004: - return 'linear-gradient(255.69deg, #474AF1 15.59%, #202284 94.93%);' - case 1000045: - case 1000046: - case 1000047: - case 1000048: - return 'linear-gradient(255.69deg, #37B348 15.59%, #003E08 94.93%);' - case 5665: - case 23: - case 2719: - case 528: - case 17018: - case 567: - case 16306: - case 1189: - case 480: - case 16920: - case 6032: - case 17624: - case 114440: - return 'linear-gradient(83.42deg, #01257B 53.04%, #0144B5 94.83%), #000000' - default: - return currentDefaultColor - } -} diff --git a/src/helpers/getProfileLogo/__tests__/index.tsx b/src/helpers/getProfileLogo/__tests__/index.tsx index 0d36c43f..4cf5e109 100644 --- a/src/helpers/getProfileLogo/__tests__/index.tsx +++ b/src/helpers/getProfileLogo/__tests__/index.tsx @@ -14,12 +14,12 @@ describe('getLogo helper', () => { id: 1, profileType: ProfileTypes.TEAMS, sportType: 3, - })).toBe('https://cf-aws.insports.tv/media/teams/3/1/logo.jpg') + })).toBe('https://cf-aws.insports.tv/media/teams/3/1/logo.png') expect(getProfileLogo({ id: 1, profileType: ProfileTypes.TOURNAMENTS, sportType: 2, - })).toBe('https://cf-aws.insports.tv/media/tournaments/2/1/logo.jpg') + })).toBe('https://cf-aws.insports.tv/media/tournaments/2/1/logo.png') }) }) diff --git a/src/helpers/getProfileLogo/index.tsx b/src/helpers/getProfileLogo/index.tsx index c2df139d..4db7f887 100644 --- a/src/helpers/getProfileLogo/index.tsx +++ b/src/helpers/getProfileLogo/index.tsx @@ -15,4 +15,4 @@ export const getProfileLogo = ({ id, profileType, sportType, -}: GetLogoArgs) => `${URL_AWS}/media/${PROFILE_NAMES[profileType]}/${sportType}/${id}/${profileType === ProfileTypes.PLAYERS ? 'photo.png' : 'logo.jpg'}` +}: GetLogoArgs) => `${URL_AWS}/media/${PROFILE_NAMES[profileType]}/${sportType}/${id}/${profileType === ProfileTypes.PLAYERS ? 'photo.png' : 'logo.png'}` diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 6ec4e5c7..814a4711 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -16,3 +16,4 @@ export * from './isMatchPage' export * from './languageUrlParam' export * from './bodyScrollLock' export * from './getLocalStorage' +export * from './checkPage' diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index ab8a1aa5..46ea864e 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -9,3 +9,4 @@ export * from './useTooltip' export * from './useModalRoot' export * from './usePageLogger' export * from './useDuration' +export * from './useScreenOrientation' diff --git a/src/hooks/useScreenOrientation.tsx b/src/hooks/useScreenOrientation.tsx new file mode 100644 index 00000000..14cd2035 --- /dev/null +++ b/src/hooks/useScreenOrientation.tsx @@ -0,0 +1,14 @@ +import { useState } from 'react' + +import { useEventListener } from './useEventListener' + +export const useScreenOrientation = () => { + const [orientation, setOrientation] = useState(window.orientation) + + useEventListener({ + callback: (e) => setOrientation(window.orientation), + event: 'orientationchange', + }) + + return orientation +} diff --git a/src/pages/HighlightsPage/storeHighlightsAtoms.tsx b/src/pages/HighlightsPage/storeHighlightsAtoms.tsx index 2c0227dd..542aed44 100644 --- a/src/pages/HighlightsPage/storeHighlightsAtoms.tsx +++ b/src/pages/HighlightsPage/storeHighlightsAtoms.tsx @@ -1,6 +1,6 @@ import { atom, selector } from 'recoil' -import type { Match } from 'requests' +import type { AdResponse, Match } from 'requests' export type MatchType = Match & { isChecked: boolean, @@ -43,6 +43,11 @@ export const fetchingMatches = atom({ key: 'fetchingMatches', }) +export const adsStore = atom({ + default: {} as AdResponse, + key: 'adsStore', +}) + export const checkedMatches = selector({ get: ({ get }) => { const matches = get(playerMatchesState) diff --git a/src/pages/SubscriptionsPage/index.tsx b/src/pages/SubscriptionsPage/index.tsx new file mode 100644 index 00000000..c0ecaeba --- /dev/null +++ b/src/pages/SubscriptionsPage/index.tsx @@ -0,0 +1,16 @@ +import { ProfileHeader } from 'features/ProfileHeader' +import { Main } from 'features/PageLayout' +import { BuyMatchPopup } from 'features/BuyMatchPopup' + +import { Wrapper } from './styled' + +const SubscriptionsPage = () => ( + <Wrapper> + <ProfileHeader /> + <Main> + <BuyMatchPopup /> + </Main> + </Wrapper> +) + +export default SubscriptionsPage diff --git a/src/pages/SubscriptionsPage/styled.tsx b/src/pages/SubscriptionsPage/styled.tsx new file mode 100644 index 00000000..c655b166 --- /dev/null +++ b/src/pages/SubscriptionsPage/styled.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components/macro' + +import { PageWrapper } from 'features/PageLayout' +import { Form } from 'features/Search/styled' +import { MenuList } from 'features/Menu/styled' +import { HeaderStyled } from 'features/ProfileHeader/styled' +import { Logo } from 'features/Logo' +import { Body, Wrapper as ContentWrapper } from 'features/BuyMatchPopup/styled' + +export const Wrapper = styled(PageWrapper)` + ${Form}, ${MenuList} { + display: none; + } + + ${HeaderStyled} { + height: 60px; + } + + ${Logo} { + top: 7.5px; + } + + ${Body} { + margin-top: 18px; + padding: 0; + } + + ${ContentWrapper} { + padding: 20px 16px; + } +` diff --git a/src/requests/getAds/getAds.tsx b/src/requests/getAds/getAds.tsx new file mode 100644 index 00000000..f094ad40 --- /dev/null +++ b/src/requests/getAds/getAds.tsx @@ -0,0 +1,65 @@ +import { callApi } from 'helpers' + +import { ADS_API_URL } from 'config' + +import { DeviceType } from '../../components/Ads/types' + +type MatchesParams = { + match_id?: number, + sport_id?: number, +} + +type TournamentsParams = { + sport_id?: number, + tournament_id?: number, +} + +export type AdsParams = { + client_type: DeviceType, + language: string, + matches?: Array<MatchesParams>, + tournaments?: Array<TournamentsParams>, + type_id: number, +} + +export type AdType = { + duration: number, + frequency: number, + id: number, + impressions: number, + link: string, + media: { + url: string, + }, + name: string, + position: { + id: number, + name_eng: string, + name_rus: string, + source_type: string, + }, + remaining_views: number, + time_close: number, + type: { + id: number, + name_eng: string, + name_rus: string, + }, +} + +export type PositionName = 'header' | 'block' | 'match_cell' | 'mobile' | 'match' + +export type AdResponse = Record<PositionName, AdsListType> + +export type AdsListType = Array<AdType> + +export const getAds = (params: AdsParams): Promise<AdResponse> => { + const config = { + body: params, + } + + return callApi({ + config, + url: ADS_API_URL, + }) +} diff --git a/src/requests/getAds/index.tsx b/src/requests/getAds/index.tsx new file mode 100644 index 00000000..106b0b5a --- /dev/null +++ b/src/requests/getAds/index.tsx @@ -0,0 +1,2 @@ +export * from './getAds' +export * from './updateAdsView' diff --git a/src/requests/getAds/updateAdsView.tsx b/src/requests/getAds/updateAdsView.tsx new file mode 100644 index 00000000..71f0f86e --- /dev/null +++ b/src/requests/getAds/updateAdsView.tsx @@ -0,0 +1,29 @@ +import { callApi } from 'helpers' + +import { ADS_API_URL } from 'config' + +export type AdsViewParams = { + adv_id: number, +} + +type AdsViewResponse = { + data: string, + message: string, + reason: string, + status: 'failed' | 'success', +} + +export const updateAdsView = ( + { adv_id }: AdsViewParams, +): Promise<AdsViewResponse> => { + const config = { + body: { + adv_id, + }, + } + + return callApi({ + config, + url: `${ADS_API_URL}/${adv_id}/view`, + }) +} diff --git a/src/requests/getGeoInfo.tsx b/src/requests/getGeoInfo.tsx index 6a3e46cc..be59b9ac 100644 --- a/src/requests/getGeoInfo.tsx +++ b/src/requests/getGeoInfo.tsx @@ -1,4 +1,4 @@ -import { AUTH_SERVICE } from 'config' +import { API_ROOT } from 'features/AuthServiceApp/config/routes' import { callApi } from 'helpers' export type GeoInfoType = { @@ -14,6 +14,6 @@ export const getGeoInfo = async (): Promise<GeoInfoType> => { return callApi({ config, - url: `${AUTH_SERVICE}/geoinfo`, + url: `${API_ROOT}/geoinfo`, }) } diff --git a/src/requests/getMatchInfo.tsx b/src/requests/getMatchInfo.tsx index 2e49db4b..14504d6d 100644 --- a/src/requests/getMatchInfo.tsx +++ b/src/requests/getMatchInfo.tsx @@ -50,7 +50,7 @@ export enum MatchStatuses { } export type MatchInfo = { - access?: boolean, + access?: boolean | null, c_match_calc_status: MatchStatuses | null, calc: boolean, country: TournamentType, diff --git a/src/requests/getMatches/types.tsx b/src/requests/getMatches/types.tsx index cbf15a0c..4afa6458 100644 --- a/src/requests/getMatches/types.tsx +++ b/src/requests/getMatches/types.tsx @@ -9,6 +9,7 @@ type AwsTournamentMedia = AwsTeamMedia & { } export type TournamentType = { + color?: string, id: number, is_super_tournament?: boolean, media?: AwsTournamentMedia, diff --git a/src/requests/getPaymentUrl.tsx b/src/requests/getPaymentOTTUrl.tsx similarity index 94% rename from src/requests/getPaymentUrl.tsx rename to src/requests/getPaymentOTTUrl.tsx index 781a3a81..ff7eb73f 100644 --- a/src/requests/getPaymentUrl.tsx +++ b/src/requests/getPaymentOTTUrl.tsx @@ -18,7 +18,7 @@ type Props = { url_return?: string | null, } -export const getPaymentUrl = async ({ +export const getPaymentOTTUrl = async ({ action, item, product_name, diff --git a/src/requests/getPaymentPayUrl.tsx b/src/requests/getPaymentPayUrl.tsx new file mode 100644 index 00000000..fea2bc56 --- /dev/null +++ b/src/requests/getPaymentPayUrl.tsx @@ -0,0 +1,31 @@ +import { PAYMENT_API_URL } from 'config' + +import { callApi } from 'helpers' + +import type { SubscriptionResponse } from 'requests/getSubscriptions' + +type Props = { + item: SubscriptionResponse, + url_return?: string | null, +} + +type PaymentResponse = { + url: string, +} + +export const getPaymentPayUrl = async ({ + item, + url_return, +}: Props): Promise<PaymentResponse> => { + const config = { + body: { + item, + url_return, + }, + } + + return callApi({ + config, + url: `${PAYMENT_API_URL}/api/v2/phonepe/create-payment-link`, + }) +} diff --git a/src/requests/getPlayerInfo.tsx b/src/requests/getPlayerInfo.tsx index 1ce69eca..ef65c642 100644 --- a/src/requests/getPlayerInfo.tsx +++ b/src/requests/getPlayerInfo.tsx @@ -4,6 +4,7 @@ import { } from 'config' import { callApi } from 'helpers' +import type { TournamentType } from './getMatches' const proc = PROCEDURES.get_player_info @@ -29,6 +30,7 @@ export type PlayerProfile = { lastname_rus: string, nickname_eng: string | null, nickname_rus: string | null, + tournament: TournamentType, weight: number | null, } | null diff --git a/src/requests/getProfileColor.tsx b/src/requests/getProfileColor.tsx deleted file mode 100644 index 2bdb016a..00000000 --- a/src/requests/getProfileColor.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - API_ROOT, - ProfileTypes, - -} from 'config' -import { callApi } from 'helpers' - -const profiles = { - [ProfileTypes.TEAMS]: 'team', - [ProfileTypes.TOURNAMENTS]: 'tournament', - [ProfileTypes.PLAYERS]: 'team', - [ProfileTypes.MATCHES]: '', - [ProfileTypes.SUPERTOURNAMENTS]: '', -} - -type Response = { - b: number, - code: string, - g: number, - r: number, -} - -type Args = { - profileId: number, - profileType: ProfileTypes, - sportType: number, -} - -export const getProfileColor = async ({ - profileId, - profileType, - sportType, -}: Args): Promise<string> => { - const config = { - body: { - profile_id: profileId, - profile_type: profiles[profileType], - sport_id: sportType, - }, - } - - const response: Response = await callApi({ - config, - url: `${API_ROOT}/profile/color`, - }) - - return `rgba(${response.r}, ${response.g}, ${response.b}, 0.56)` -} diff --git a/src/requests/getSubscriptions.tsx b/src/requests/getSubscriptions.tsx index c4b1edbf..65c93a18 100644 --- a/src/requests/getSubscriptions.tsx +++ b/src/requests/getSubscriptions.tsx @@ -50,6 +50,9 @@ type Sub = { active_to: string, currency_id: number, id: number, + lexic1: number, + lexic2: number | null, + lexic3: number, match_id?: number, option: number, price: number, diff --git a/src/requests/getTeamInfo.tsx b/src/requests/getTeamInfo.tsx index 5e0d6cb0..c99c6f88 100644 --- a/src/requests/getTeamInfo.tsx +++ b/src/requests/getTeamInfo.tsx @@ -8,6 +8,7 @@ import { callApi } from 'helpers' const proc = PROCEDURES.get_team_info type NameObject = { + color?: string, id: number, name_eng: string, name_rus: string, diff --git a/src/requests/getTournamentInfo.tsx b/src/requests/getTournamentInfo.tsx index a1d8d2b4..3ac8bdac 100644 --- a/src/requests/getTournamentInfo.tsx +++ b/src/requests/getTournamentInfo.tsx @@ -7,6 +7,7 @@ import { callApi } from 'helpers' const proc = PROCEDURES.get_tournament_info export type TournamentInfo = { + color: string, country: { id: number, name_eng: string, diff --git a/src/requests/index.tsx b/src/requests/index.tsx index 0613c71a..d16ce922 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -34,3 +34,9 @@ export * from './getMatchParticipants' export * from './getStatsEvents' export * from './getTokenVirtualUser' export * from './checkDevice' +export * from './getPaymentOTTUrl' +export * from './getPaymentPayUrl' +export * from './getAds' +export * from './getFavouriteTeam' +export * from './getCountryCode' +export * from './getAgreements' diff --git a/src/utilits/mirage/Mirage.tsx b/src/utilits/mirage/Mirage.tsx deleted file mode 100644 index b685e049..00000000 --- a/src/utilits/mirage/Mirage.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable */ -import { - createServer, - Model, -} from 'miragejs' - -import { ResponseType } from 'requests/getFavouriteTeam' - -import { surveys } from './fixtures/surveys' - -export function makeServer({ environment = 'test' } = {}) { - const server = createServer({ - environment, - factories: {}, - fixtures: { - surveys, - }, - models: { - surveys: Model.extend<Partial<ResponseType>>({}), - }, - routes() { - this.passthrough('https://api.insports.tv/***') - this.passthrough('https://insports.tv/***') - this.passthrough('https://images.insports.tv/***') - this.passthrough('https://auth.insports.tv/***') - this.passthrough('${URL_AWS}/***') - this.get('https://api.insports.tv/v1/survey/teams/1/131/30', (schema: any) => schema.all('surveys').models[0].attrs) - }, - }) - return server -} diff --git a/src/utilits/mirage/fixtures/getAds.tsx b/src/utilits/mirage/fixtures/getAds.tsx new file mode 100644 index 00000000..b144bd23 --- /dev/null +++ b/src/utilits/mirage/fixtures/getAds.tsx @@ -0,0 +1,592 @@ +/* eslint-disable */ +export const getAds = () => { + return { + "mobile": [ + { + "id": 71, + "name": "Test 2", + "type": { + "id": 1, + "name_eng": "Main", + "name_rus": "Главная" + }, + "position": { + "id": 12, + "source_type": "Web", + "name_eng": "Web main ad 2 (1x1)", + "name_rus": "Веб: главная 2 (1x1)" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 2, + "duration": 120, + "time_close": 150, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png" + }, + "remaining_views": 9 + },{ + "id": 71, + "name": "Test 2", + "type": { + "id": 1, + "name_eng": "Main", + "name_rus": "Главная" + }, + "position": { + "id": 25, + "source_type": "Web", + "name_eng": "Web main ad 2 (1x1)", + "name_rus": "Веб: главная 2 (1x1)" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 2, + "duration": 120, + "time_close": 150, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png" + }, + "remaining_views": 9 + }, + { + "id": 72, + "name": "Best mens boots for sport advertise here", + "type": { + "id": 1, + "name_eng": "Main", + "name_rus": "Главная" + }, + "position": { + "id": 11, + "source_type": "Web", + "name_eng": "Web main ad 7 (1x1)", + "name_rus": "Веб: главная 7 (1x1)" + }, + "link": "https://www.google.com/", + "impressions": 2, + "frequency": 1, + "duration": 300, + "time_close": 500, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/72/en/web.png" + }, + "remaining_views": 9 + }, + { + "id": 85, + "name": "Test 16", + "type": { + "id": 2, + "name_eng": "Match", + "name_rus": "Матч" + }, + "position": { + "id": 16, + "source_type": "Mobile", + "name_eng": "Web top side ad horizontal", + "name_rus": "Веб: верхняя, боковая, по горизонтали" + }, + "link": "https://www.google.com/", + "impressions": 60, + "frequency": 5, + "duration": 12000, + "time_close": 1, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/85/en/mobile.png" + }, + "remaining_views": 60 + }, + + { + "id": 86, + "name": "Test 17", + "type": { + "id": 2, + "name_eng": "Match", + "name_rus": "Матч" + }, + "position": { + "id": 17, + "source_type": "Mobile", + "name_eng": "Web top side ad horizontal", + "name_rus": "Веб: верхняя, боковая, по горизонтали" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 5, + "duration": 12000, + "time_close": 1, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/86/en/mobile.png" + }, + "remaining_views": 10 + }, + { + "id": 91, + "name": "Test 22", + "type": { + "id": 3, + "name_eng": "Player", + "name_rus": "Плеер" + }, + "position": { + "id": 22, + "source_type": "Mobile", + "name_eng": "Web top side ad horizontal", + "name_rus": "Веб: верхняя, боковая, по горизонтали" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 10, + "duration": 12000, + "time_close": 1, + "media": { + "url": "https://i.imgur.com/5Tbygu1.png" + }, + "remaining_views": 10 + }, + { + "id": 92, + "name": "Test 23", + "type": { + "id": 3, + "name_eng": "Player", + "name_rus": "Плеер" + }, + "position": { + "id": 23, + "source_type": "Mobile", + "name_eng": "Web top side ad horizontal", + "name_rus": "Веб: верхняя, боковая, по горизонтали" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 5, + "duration": 12000, + "time_close": 1, + "media": { + "url": "https://i.imgur.com/PyZwEDq.png" + }, + "remaining_views": 10 + }, + // { + // "id": 93, + // "name": "Test 24", + // "type": { + // "id": 3, + // "name_eng": "Player", + // "name_rus": "Плеер" + // }, + // "position": { + // "id": 24, + // "source_type": "Mobile", + // "name_eng": "Web top side ad horizontal", + // "name_rus": "Веб: верхняя, боковая, по горизонтали" + // }, + // "link": "https://www.google.com/", + // "impressions": 10, + // "frequency": 5, + // "duration": 12000, + // "time_close": 1, + // "media": { + // "url": "https://i.imgur.com/iwihMdx.png" + // }, + // "remaining_views": 10 + // }, + ], + "match_cell": [ + { + "id": 71, + "name": "Test 2", + "type": { + "id": 1, + "name_eng": "Main", + "name_rus": "Главная" + }, + "position": { + "id": 1, + "source_type": "Web", + "name_eng": "Web main ad 2 (1x1)", + "name_rus": "Веб: главная 2 (1x1)" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 2, + "duration": 15, + "time_close": 15, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png" + }, + "remaining_views": 9 + }, + { + "id": 71, + "name": "Test 2", + "type": { + "id": 1, + "name_eng": "Main", + "name_rus": "Главная" + }, + "position": { + "id": 2, + "source_type": "Web", + "name_eng": "Web main ad 2 (1x1)", + "name_rus": "Веб: главная 2 (1x1)" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 2, + "duration": 12, + "time_close": 15, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/71/en/web.png" + }, + "remaining_views": 9 + }, + { + "id": 72, + "name": "Test 3", + "type": { + "id": 1, + "name_eng": "Main", + "name_rus": "Главная" + }, + "position": { + "id": 3, + "source_type": "Web", + "name_eng": "Web main ad 7 (1x1)", + "name_rus": "Веб: главная 7 (1x1)" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 3, + "duration": 15, + "time_close": 15, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/72/en/web.png" + }, + "remaining_views": 9 + } + ], + "header": [ + // { + // "id": 77, + // "name": "Test 8", + // "type": { + // "id": 1, + // "name_eng": "Main", + // "name_rus": "Главная" + // }, + // "position": { + // "id": 8, + // "source_type": "Web", + // "name_eng": "Web main ad 2 (3x1)", + // "name_rus": "Веб: главная 2(3x1)" + // }, + // "link": null, + // "impressions": 10, + // "frequency": 2, + // "duration": 120, + // "time_close": 15, + // "media": { + // "url": "https://cf-aws-staging.insports.tv/media/folder/77/en/web.png" + // }, + // "remaining_views": 10 + // }, + // { + // "id": 78, + // "name": "Test 9", + // "type": { + // "id": 1, + // "name_eng": "Main", + // "name_rus": "Главная" + // }, + // "position": { + // "id": 9, + // "source_type": "Web", + // "name_eng": "Web main ad (6x1)", + // "name_rus": "Веб: главная (6x1)" + // }, + // "link": null, + // "impressions": 10, + // "frequency": 2, + // "duration": 10, + // "time_close": 15, + // "media": { + // "url": "https://cf-aws-staging.insports.tv/media/folder/78/en/web.png" + // }, + // "remaining_views": 15 + // }, + // { + // "id": 76, + // "name": "Test 7", + // "type": { + // "id": 1, + // "name_eng": "Main", + // "name_rus": "Главная" + // }, + // "position": { + // "id": 7, + // "source_type": "Web", + // "name_eng": "Web main ad (3x1)", + // "name_rus": "Веб: главная (3x1)" + // }, + // "link": null, + // "impressions": 10, + // "frequency": 4, + // "duration": 10, + // "time_close": 15, + // "media": { + // "url": "https://cf-aws-staging.insports.tv/media/folder/76/en/web.png" + // }, + // "remaining_views": 15 + // } + ], + "block": [ + // { + // "id": 75, + // "name": "Test 6", + // "type": { + // "id": 1, + // "name_eng": "Main", + // "name_rus": "Главная" + // }, + // "position": { + // "id": 6, + // "source_type": "Web", + // "name_eng": "Web main ad (2x2)", + // "name_rus": "Веб: главная (2x2)" + // }, + // "link": null, + // "impressions": 10, + // "frequency": 3, + // "duration": 120, + // "time_close": null, + // "media": { + // "url": "https://cf-aws-staging.insports.tv/media/folder/75/en/web.png" + // }, + // "remaining_views": 10 + // }, + // { + // "id": 73, + // "name": "Test 4", + // "type": { + // "id": 1, + // "name_eng": "Main", + // "name_rus": "Главная" + // }, + // "position": { + // "id": 4, + // "source_type": "Web", + // "name_eng": "Web main ad (2x1)", + // "name_rus": "Веб: главная (2x1)" + // }, + // "link": null, + // "impressions": 10, + // "frequency": 1, + // "duration": 120, + // "time_close": null, + // "media": { + // "url": "https://cf-aws-staging.insports.tv/media/folder/73/en/web.png" + // }, + // "remaining_views": 9 + // }, + // { + // "id": 74, + // "name": "Test 5", + // "type": { + // "id": 1, + // "name_eng": "Main", + // "name_rus": "Главная" + // }, + // "position": { + // "id": 5, + // "source_type": "Web", + // "name_eng": "Web main ad (1x2)", + // "name_rus": "Веб: главная (1x2)" + // }, + // "link": null, + // "impressions": 10, + // "frequency": 2, + // "duration": 120000, + // "time_close": 1, + // "media": { + // "url": "https://cf-aws-staging.insports.tv/media/folder/74/en/web.png" + // }, + // "remaining_views": 9 + // } + ], + "match": [ + // { + // "id": 82, + // "name": "Test 13", + // "type": { + // "id": 2, + // "name_eng": "Match", + // "name_rus": "Матч" + // }, + // "position": { + // "id": 13, + // "source_type": "Web", + // "name_eng": "Web top side ad horizontal", + // "name_rus": "Веб: верхняя, боковая, по горизонтали" + // }, + // "link": "https://www.google.com/", + // "impressions": 10, + // "frequency": 10, + // "duration": 10, + // "time_close": 3, + // "media": { + // "url": "https://i.imgur.com/B0odUkf.png" + // }, + // "remaining_views": 10 + // }, + // { + // "id": 83, + // "name": "Test 14", + // "type": { + // "id": 2, + // "name_eng": "Match", + // "name_rus": "Матч" + // }, + // "position": { + // "id": 14, + // "source_type": "Web", + // "name_eng": "Web top side ad horizontal", + // "name_rus": "Веб: верхняя, боковая, по горизонтали" + // }, + // "link": "https://www.google.com/", + // "impressions": 10, + // "frequency": 5, + // "duration": 12000, + // "time_close": 1, + // "media": { + // "url": "https://i.imgur.com/KcJhEcu.jpg" + // }, + // "remaining_views": 10 + // }, + { + "id": 84, + "name": "Test 15", + "type": { + "id": 2, + "name_eng": "Match", + "name_rus": "Матч" + }, + "position": { + "id": 15, + "source_type": "Web", + "name_eng": "Web top side ad horizontal", + "name_rus": "Веб: верхняя, боковая, по горизонтали" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 10, + "duration": 5, + "time_close": 1, + "media": { + "url": "https://cf-aws-staging.insports.tv/media/folder/84/en/web.png" + }, + "remaining_views": 10 + }, + // { + // "id": 87, + // "name": "Test 18", + // "type": { + // "id": 3, + // "name_eng": "Player", + // "name_rus": "Плеер" + // }, + // "position": { + // "id": 18, + // "source_type": "Web", + // "name_eng": "Web top side ad horizontal", + // "name_rus": "Веб: верхняя, боковая, по горизонтали" + // }, + // "link": "https://www.google.com/", + // "impressions": 10, + // "frequency": 100, + // "duration": 12000, + // "time_close": 1, + // "media": { + // "url": "https://i.imgur.com/xWlogZt.png" + // }, + // "remaining_views": 10 + // }, + // { + // "id": 88, + // "name": "Test 19", + // "type": { + // "id": 3, + // "name_eng": "Player", + // "name_rus": "Плеер" + // }, + // "position": { + // "id": 19, + // "source_type": "Web", + // "name_eng": "Web top side ad horizontal", + // "name_rus": "Веб: верхняя, боковая, по горизонтали" + // }, + // "link": "https://www.google.com/", + // "impressions": 10, + // "frequency": 100, + // "duration": 12000, + // "time_close": 1, + // "media": { + // "url": "https://i.imgur.com/u9cfGGs.png" + // }, + // "remaining_views": 10 + // }, + // { + // "id": 89, + // "name": "Test 20", + // "type": { + // "id": 3, + // "name_eng": "Player", + // "name_rus": "Плеер" + // }, + // "position": { + // "id": 20, + // "source_type": "Web", + // "name_eng": "Web top side ad horizontal", + // "name_rus": "Веб: верхняя, боковая, по горизонтали" + // }, + // "link": "https://www.google.com/", + // "impressions": 10, + // "frequency": 100, + // "duration": 12000, + // "time_close": 1, + // "media": { + // "url": "https://i.imgur.com/XP1ZIQC.png" + // }, + // "remaining_views": 10 + // }, + // + { + "id": 90, + "name": "Test 21", + "type": { + "id": 3, + "name_eng": "Player", + "name_rus": "Плеер" + }, + "position": { + "id": 21, + "source_type": "Web", + "name_eng": "Web top side ad horizontal", + "name_rus": "Веб: верхняя, боковая, по горизонтали" + }, + "link": "https://www.google.com/", + "impressions": 10, + "frequency": 5, + "duration": 10, + "time_close": 3, + "media": { + "url": "https://i.imgur.com/PaiZdTd.png" + }, + "remaining_views": 10 + }, + ], + } +} diff --git a/src/utilits/mirage/fixtures/index.tsx b/src/utilits/mirage/fixtures/index.tsx new file mode 100644 index 00000000..b1fcdff4 --- /dev/null +++ b/src/utilits/mirage/fixtures/index.tsx @@ -0,0 +1,2 @@ +export * from './getAds' +export * from './surveys' diff --git a/src/utilits/mirage/models/index.tsx b/src/utilits/mirage/models/index.tsx new file mode 100644 index 00000000..d4274eff --- /dev/null +++ b/src/utilits/mirage/models/index.tsx @@ -0,0 +1,8 @@ +/* eslint-disable */ +import { Model } from 'miragejs' +import type { ResponseType, AdResponse } from 'requests' + +export const models = { + ads: Model.extend<Partial<AdResponse>>({}), + surveys: Model.extend<Partial<ResponseType>>({}), +} diff --git a/src/utilits/mirage/server.ts b/src/utilits/mirage/server.ts new file mode 100644 index 00000000..ed3b2b49 --- /dev/null +++ b/src/utilits/mirage/server.ts @@ -0,0 +1,44 @@ +/* eslint-disable */ +import { + createServer, +} from 'miragejs' + +import { + ADS_API_URL, + APIS, + AUTH_SERVICE, + STATS_API_URL, + URL_AWS, + VIEWS_API, +} from 'config' +import { API_ROOT } from 'features/AuthServiceApp/config/routes' + +import { surveys, getAds } from './fixtures' +import { models } from './models' + + +const mainDomain = 'insports.tv' +export function makeServer({ environment = 'test' } = {}) { + const server = createServer({ + environment, + fixtures: { + surveys, + ads: getAds(), + }, + models: models, + routes() { + this.passthrough(`${API_ROOT}/***`) + this.passthrough(`${VIEWS_API}/***`) + this.passthrough(`${STATS_API_URL}/***`) + this.passthrough(`${APIS.production.api}/***`) + this.passthrough(`${APIS.staging.api}/***`) + this.passthrough(`${AUTH_SERVICE}/***`) + this.passthrough(`https://${mainDomain}/***`) + this.passthrough(`https://images.${mainDomain}/***`) + this.passthrough(`${URL_AWS}/***`) + this.post(`${ADS_API_URL}`, getAds) + this.logging = true; + }, + }) + return server +}