@ -0,0 +1,156 @@ |
|||||||
|
import { useEffect, useState } from 'react' |
||||||
|
|
||||||
|
import ReactGA from 'react-ga' |
||||||
|
|
||||||
|
import { updateAdsView } from 'requests' |
||||||
|
|
||||||
|
import { useToggle } from 'hooks' |
||||||
|
|
||||||
|
import { getLocalStorageItem, isMatchPage } from 'helpers' |
||||||
|
|
||||||
|
import { |
||||||
|
device, |
||||||
|
COUNTRY, |
||||||
|
} from 'config' |
||||||
|
|
||||||
|
import { useMatchPageStore } from 'features/MatchPage/store' |
||||||
|
|
||||||
|
import type { AdComponentType } from './index' |
||||||
|
|
||||||
|
import { checkVideo } from '../../helpers' |
||||||
|
import { |
||||||
|
adsViews, |
||||||
|
EventGA, |
||||||
|
ViewsType, |
||||||
|
} from '../../types' |
||||||
|
|
||||||
|
const countryCode = getLocalStorageItem(COUNTRY) |
||||||
|
|
||||||
|
export const useAd = ({ ad }: AdComponentType) => { |
||||||
|
const [isOpenAd, setIsOpenAd] = useState(true) |
||||||
|
const [isNeedToShow, setIsNeedToShow] = useState(true) |
||||||
|
const [shownTime, setShownTime] = useState(0) |
||||||
|
|
||||||
|
const { isFullscreen } = useMatchPageStore() |
||||||
|
|
||||||
|
const views = getLocalStorageItem(adsViews) as ViewsType |
||||||
|
const { |
||||||
|
duration, |
||||||
|
frequency, |
||||||
|
id, |
||||||
|
media, |
||||||
|
name, |
||||||
|
time_close, |
||||||
|
} = ad |
||||||
|
|
||||||
|
const { |
||||||
|
close, |
||||||
|
isOpen: isOpenCloseBtn, |
||||||
|
open: showCloseBtn, |
||||||
|
} = useToggle() |
||||||
|
|
||||||
|
const isNeedBanner = Number(views?.HOME) % frequency === 0 |
||||||
|
const isVideo = checkVideo(media.url) |
||||||
|
const currentAdsTime = duration - shownTime |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!isFullscreen) { |
||||||
|
if (currentAdsTime === 0) { |
||||||
|
setShownTime(0) |
||||||
|
} else { |
||||||
|
const stopWatch = setInterval(() => { |
||||||
|
setShownTime((prev) => prev + 1) |
||||||
|
}, 1000) |
||||||
|
return () => clearInterval(stopWatch) |
||||||
|
} |
||||||
|
} |
||||||
|
return undefined |
||||||
|
}, [ |
||||||
|
isFullscreen, |
||||||
|
currentAdsTime, |
||||||
|
]) |
||||||
|
|
||||||
|
const handleClose = () => { |
||||||
|
setIsOpenAd(false) |
||||||
|
isMatchPage() && setIsNeedToShow(false) |
||||||
|
sendBannerClickEvent(EventGA.CLOSE) |
||||||
|
} |
||||||
|
|
||||||
|
const sendBannerClickEvent = (event: EventGA) => { |
||||||
|
ReactGA.initialize('Advertisement') |
||||||
|
ReactGA.event({ |
||||||
|
action: event, |
||||||
|
category: 'Advertisement', |
||||||
|
label: `${name}_${countryCode ?? ''}_${device}`, |
||||||
|
value: id, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setShownTime(0) |
||||||
|
|
||||||
|
if (isMatchPage()) { |
||||||
|
const interval = setInterval(() => { |
||||||
|
setIsNeedToShow(true) |
||||||
|
setIsOpenAd(true) |
||||||
|
}, frequency * 1000) |
||||||
|
|
||||||
|
return () => clearInterval(interval) |
||||||
|
} |
||||||
|
|
||||||
|
setIsNeedToShow(isNeedBanner) |
||||||
|
return setIsOpenAd(isNeedBanner) |
||||||
|
}, [ |
||||||
|
frequency, |
||||||
|
isNeedToShow, |
||||||
|
views?.HOME, |
||||||
|
isNeedBanner, |
||||||
|
]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (isFullscreen || !isOpenAd) return undefined |
||||||
|
|
||||||
|
const timeoutCloseAd = setTimeout(handleClose, currentAdsTime * 1000) |
||||||
|
return () => { |
||||||
|
clearTimeout(timeoutCloseAd) |
||||||
|
} |
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [ |
||||||
|
isNeedToShow, |
||||||
|
isOpenAd, |
||||||
|
isFullscreen, |
||||||
|
currentAdsTime, |
||||||
|
]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
close() |
||||||
|
const timeoutCloseBtn = time_close && setTimeout(showCloseBtn, time_close * 1000) |
||||||
|
return () => { |
||||||
|
time_close && clearTimeout(timeoutCloseBtn) |
||||||
|
} |
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [ |
||||||
|
isNeedToShow, |
||||||
|
isOpenAd, |
||||||
|
views?.HOME, |
||||||
|
]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!isNeedToShow || (!isMatchPage() && !isNeedBanner)) return |
||||||
|
|
||||||
|
(async () => { |
||||||
|
await updateAdsView({ adv_id: id }) |
||||||
|
})() |
||||||
|
sendBannerClickEvent(EventGA.DISPLAY) |
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [id, isNeedToShow]) |
||||||
|
|
||||||
|
return { |
||||||
|
handleClose, |
||||||
|
isNeedToShow, |
||||||
|
isOpenAd, |
||||||
|
isOpenCloseBtn, |
||||||
|
isVideo, |
||||||
|
sendBannerClickEvent, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,91 @@ |
|||||||
|
import type { MouseEvent } from 'react' |
||||||
|
import { memo } from 'react' |
||||||
|
|
||||||
|
import type { AdType } from 'requests' |
||||||
|
|
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
|
||||||
|
import { useAd } from './hooks' |
||||||
|
|
||||||
|
import { EventGA } from '../../types' |
||||||
|
|
||||||
|
import { |
||||||
|
AdImg, |
||||||
|
AdVideo, |
||||||
|
AdWrapper, |
||||||
|
LinkWrapper, |
||||||
|
AdsCloseButton, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export type AdComponentType = { |
||||||
|
ad: AdType, |
||||||
|
} |
||||||
|
export const AdComponent = memo(({ ad }: AdComponentType) => { |
||||||
|
const { |
||||||
|
link, |
||||||
|
media, |
||||||
|
position, |
||||||
|
} = ad |
||||||
|
|
||||||
|
const { |
||||||
|
handleClose, |
||||||
|
isNeedToShow, |
||||||
|
isOpenAd, |
||||||
|
isOpenCloseBtn, |
||||||
|
isVideo, |
||||||
|
sendBannerClickEvent, |
||||||
|
} = useAd({ ad }) |
||||||
|
|
||||||
|
const { suffix } = useLexicsStore() |
||||||
|
|
||||||
|
const close = (e: MouseEvent<HTMLButtonElement>) => { |
||||||
|
e.stopPropagation() |
||||||
|
handleClose() |
||||||
|
} |
||||||
|
|
||||||
|
const onLinkClick = () => { |
||||||
|
link && sendBannerClickEvent(EventGA.CLICK) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
position && isOpenAd && isNeedToShow |
||||||
|
? ( |
||||||
|
<AdWrapper |
||||||
|
position={position.id} |
||||||
|
isOpenAd={isOpenAd} |
||||||
|
> |
||||||
|
{isOpenCloseBtn && ( |
||||||
|
<AdsCloseButton |
||||||
|
onClick={close} |
||||||
|
size={12} |
||||||
|
position={position.id} |
||||||
|
/> |
||||||
|
)} |
||||||
|
<LinkWrapper |
||||||
|
href={link} |
||||||
|
target='_blank' |
||||||
|
rel='noreferrer' |
||||||
|
onClick={onLinkClick} |
||||||
|
> |
||||||
|
{isVideo |
||||||
|
? ( |
||||||
|
<AdVideo |
||||||
|
muted={isVideo} |
||||||
|
autoPlay={isVideo} |
||||||
|
loop={isVideo} |
||||||
|
src={media.url} |
||||||
|
position={position.id} |
||||||
|
/> |
||||||
|
) |
||||||
|
: ( |
||||||
|
<AdImg |
||||||
|
src={media.url} |
||||||
|
position={position.id} |
||||||
|
alt={`name_${suffix}`} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</LinkWrapper> |
||||||
|
</AdWrapper> |
||||||
|
) : null |
||||||
|
) |
||||||
|
}) |
||||||
@ -0,0 +1,109 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import includes from 'lodash/includes' |
||||||
|
|
||||||
|
import { CloseButton } from 'features/PopupComponents' |
||||||
|
|
||||||
|
import { |
||||||
|
MATCH_ADS, |
||||||
|
PLAYER_ADS, |
||||||
|
VIEW_ADS, |
||||||
|
} from '../../types' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
position: number, |
||||||
|
} |
||||||
|
|
||||||
|
const header = [7, 8, 9] |
||||||
|
|
||||||
|
const chooseStyle = (type: number) => { |
||||||
|
switch (true) { |
||||||
|
case VIEW_ADS.COLUMN === type: |
||||||
|
return 'grid-row: 1 / 3; img {max-height: none;}' |
||||||
|
case VIEW_ADS.ROW === type: |
||||||
|
return 'grid-column: 1 / 3' |
||||||
|
case VIEW_ADS.SQUARE === type: |
||||||
|
return 'grid-row: 1 / 3; grid-column: 1 / 3; img {max-height: none;}' |
||||||
|
case VIEW_ADS.SECOND_COLUMN === type: |
||||||
|
return 'grid-column: 2 / 3; grid-row: 1 / 1' |
||||||
|
case VIEW_ADS.SECOND_ROW === type: |
||||||
|
return 'grid-column: 1 / 2; grid-row: 2 / 3;' |
||||||
|
case MATCH_ADS.PLAYS_TOP === type: |
||||||
|
return 'margin-left: 14px; height: 48px' |
||||||
|
case MATCH_ADS.PLAYS_BOTTOM === type: |
||||||
|
return 'grid-row: 4; margin-bottom: 12px; height: 48px;' |
||||||
|
case PLAYER_ADS.LEFT_BOTTOM === type: |
||||||
|
return css` |
||||||
|
height: auto;
|
||||||
|
width: 42.4%;
|
||||||
|
bottom: 100px;
|
||||||
|
left: 20px;
|
||||||
|
` |
||||||
|
case PLAYER_ADS.CENTER_BOTTOM === type: |
||||||
|
return css` |
||||||
|
height: 18.3%;
|
||||||
|
width: 81.3%;
|
||||||
|
bottom: 100px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%); |
||||||
|
` |
||||||
|
case PLAYER_ADS.RIGHT === type: |
||||||
|
return css` |
||||||
|
height: 87.2%;
|
||||||
|
width: 18.3%;
|
||||||
|
bottom: 90px;
|
||||||
|
right: 18px; |
||||||
|
` |
||||||
|
case PLAYER_ADS.FULL_SCREEN === type: |
||||||
|
return 'bottom: 0; left: 0;' |
||||||
|
default: |
||||||
|
return '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const AdImg = styled.img<Props>` |
||||||
|
width: 100%; |
||||||
|
min-height: ${({ position }) => (!includes(header, position) && '100%')}; |
||||||
|
max-height: ${({ position }) => (includes(header, position) ? '13rem' : '100%')}; |
||||||
|
cursor: pointer; |
||||||
|
border-radius: 3px; |
||||||
|
` |
||||||
|
|
||||||
|
export const AdVideo = styled.video<Props>` |
||||||
|
object-fit: contain; |
||||||
|
width: 100%; |
||||||
|
cursor: pointer; |
||||||
|
max-height: ${({ position }) => (includes(header, position) ? '283px' : '100%')}; |
||||||
|
background-color: black; |
||||||
|
border-radius: 3px; |
||||||
|
` |
||||||
|
|
||||||
|
export const AdWrapper = styled.div<Props & {isOpenAd: boolean}>` |
||||||
|
position: ${({ position }) => (includes(PLAYER_ADS, position) ? 'absolute' : 'relative')}; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
z-index: 1; |
||||||
|
|
||||||
|
${({ position }) => chooseStyle(position)}; |
||||||
|
|
||||||
|
display: ${({ isOpenAd }) => (isOpenAd ? '' : 'none')}; |
||||||
|
` |
||||||
|
|
||||||
|
export const AdsCloseButton = styled(CloseButton)<Props>` |
||||||
|
position: absolute; |
||||||
|
right: ${({ position }) => (position === PLAYER_ADS.FULL_SCREEN ? '10px' : '0')}; |
||||||
|
top: ${({ position }) => (position === PLAYER_ADS.FULL_SCREEN ? '10px' : '0')}; |
||||||
|
background: none; |
||||||
|
border-radius: 0; |
||||||
|
z-index: 2; |
||||||
|
cursor: pointer; |
||||||
|
color: #9B9B9B; |
||||||
|
width: 28px; |
||||||
|
height: 28px; |
||||||
|
|
||||||
|
:hover { |
||||||
|
background: none; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const LinkWrapper = styled.a`` |
||||||
@ -0,0 +1,79 @@ |
|||||||
|
import type { MouseEvent } from 'react' |
||||||
|
|
||||||
|
import includes from 'lodash/includes' |
||||||
|
|
||||||
|
import type { AdType } from 'requests' |
||||||
|
|
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
|
||||||
|
import { useAd } from '../AdComponent/hooks' |
||||||
|
import { EventGA, PLAYER_MOBILE_FULL_SCREEN } from '../../types' |
||||||
|
|
||||||
|
import { |
||||||
|
AdsCloseButton, |
||||||
|
Img, |
||||||
|
MobileAdWrapper, |
||||||
|
Video, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type MobileAdTypes = { |
||||||
|
ad: AdType, |
||||||
|
} |
||||||
|
|
||||||
|
export const MobileAd = ({ ad }: MobileAdTypes) => { |
||||||
|
const { |
||||||
|
link, |
||||||
|
media, |
||||||
|
position, |
||||||
|
} = ad |
||||||
|
|
||||||
|
const { suffix } = useLexicsStore() |
||||||
|
|
||||||
|
const { |
||||||
|
handleClose, |
||||||
|
isNeedToShow, |
||||||
|
isOpenAd, |
||||||
|
isOpenCloseBtn, |
||||||
|
isVideo, |
||||||
|
sendBannerClickEvent, |
||||||
|
} = useAd({ ad }) |
||||||
|
|
||||||
|
const close = (e: MouseEvent<HTMLButtonElement>) => { |
||||||
|
e.stopPropagation() |
||||||
|
handleClose() |
||||||
|
} |
||||||
|
|
||||||
|
const onLinkClick = () => { |
||||||
|
if (link) { |
||||||
|
sendBannerClickEvent(EventGA.CLICK) |
||||||
|
window.open(link, '_blank') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
position && isOpenAd && isNeedToShow ? ( |
||||||
|
<MobileAdWrapper |
||||||
|
position={position.id} |
||||||
|
onClick={onLinkClick} |
||||||
|
> |
||||||
|
{isOpenCloseBtn |
||||||
|
&& ( |
||||||
|
<AdsCloseButton |
||||||
|
position={position.id} |
||||||
|
onClick={close} |
||||||
|
size={includes(PLAYER_MOBILE_FULL_SCREEN, position.id) ? 12 : 8} |
||||||
|
/> |
||||||
|
)} |
||||||
|
{isVideo |
||||||
|
? <Video position={position.id} src={media.url} /> |
||||||
|
: ( |
||||||
|
<Img |
||||||
|
position={position.id} |
||||||
|
src={media.url} |
||||||
|
alt={`name_${suffix}`} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</MobileAdWrapper> |
||||||
|
) : null |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,111 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import includes from 'lodash/includes' |
||||||
|
|
||||||
|
import { CloseButton } from 'features/PopupComponents' |
||||||
|
|
||||||
|
import { |
||||||
|
MATCH_ADS, |
||||||
|
PLAYER_MOBILE_FULL_SCREEN, |
||||||
|
PLAYER_MOBILE_ADS, |
||||||
|
MATCH_PAGE_MOBILE_ADS, |
||||||
|
} from '../../types' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
position: number, |
||||||
|
} |
||||||
|
|
||||||
|
const chooseStyle = (type: number) => { |
||||||
|
switch (type) { |
||||||
|
case MATCH_ADS.PLAYS_BOTTOM_MOBILE: |
||||||
|
return css` |
||||||
|
grid-row: 4;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
height: 48px; |
||||||
|
` |
||||||
|
case PLAYER_MOBILE_ADS: |
||||||
|
return css` |
||||||
|
position: absolute; |
||||||
|
width: 92%;
|
||||||
|
bottom: 50px;
|
||||||
|
left: 15px; |
||||||
|
` |
||||||
|
case PLAYER_MOBILE_FULL_SCREEN.VERTICAL_FULL_SCREEN: |
||||||
|
case PLAYER_MOBILE_FULL_SCREEN.HORIZONTAL_FULL_SCREEN: |
||||||
|
return css` |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
height: 100%; |
||||||
|
padding: 5px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7); |
||||||
|
` |
||||||
|
default: |
||||||
|
return '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const MobileAdWrapper = styled.div<Props>` |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
z-index: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '101' : '100')}; |
||||||
|
|
||||||
|
${({ position }) => chooseStyle(position)}; |
||||||
|
` |
||||||
|
|
||||||
|
export const AdsCloseButton = styled(CloseButton)<Props>` |
||||||
|
position: absolute; |
||||||
|
right: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '-5px' : '-10px')}; |
||||||
|
top: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '15px' : '10px')}; |
||||||
|
background: none; |
||||||
|
border-radius: 0; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
z-index: 2; |
||||||
|
color: #9B9B9B; |
||||||
|
` |
||||||
|
|
||||||
|
export const Img = styled.img<Props>` |
||||||
|
border-radius: 2px; |
||||||
|
width: 100%; |
||||||
|
|
||||||
|
object-fit: ${({ position }) => { |
||||||
|
switch (true) { |
||||||
|
case position === PLAYER_MOBILE_FULL_SCREEN.VERTICAL_FULL_SCREEN: |
||||||
|
return 'fill' |
||||||
|
case position === MATCH_ADS.PLAYS_TOP_MOBILE: |
||||||
|
return 'contain' |
||||||
|
default: |
||||||
|
return 'cover' |
||||||
|
} |
||||||
|
}}; |
||||||
|
|
||||||
|
height: ${({ position }) => { |
||||||
|
switch (true) { |
||||||
|
case position === 10: |
||||||
|
return '50px' |
||||||
|
case includes(MATCH_PAGE_MOBILE_ADS, position): |
||||||
|
return '100%' |
||||||
|
default: |
||||||
|
return '75px' |
||||||
|
} |
||||||
|
}} |
||||||
|
` |
||||||
|
|
||||||
|
export const Video = styled.video<Props>` |
||||||
|
max-height: 100%; |
||||||
|
object-fit: cover; |
||||||
|
min-width: 100%; |
||||||
|
height: ${({ position }) => (position === 10 ? '50px' : '75px')}; |
||||||
|
border-radius: 2px; |
||||||
|
|
||||||
|
height: ${({ position }) => { |
||||||
|
switch (true) { |
||||||
|
case position === 10: |
||||||
|
return '50px' |
||||||
|
case position === MATCH_ADS.PLAYS_TOP_MOBILE: |
||||||
|
return '48px' |
||||||
|
default: |
||||||
|
return '75px' |
||||||
|
} |
||||||
|
}} |
||||||
|
` |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
import type { AdResponse, AdsListType } from 'requests' |
||||||
|
|
||||||
|
export const calcMaxAdDurationAds = (advertisements: AdResponse) => { |
||||||
|
const allAds = Object.values(advertisements) |
||||||
|
|
||||||
|
const combineAds = allAds.reduce((result, currentAd) => { |
||||||
|
result.push(...currentAd) |
||||||
|
|
||||||
|
return result |
||||||
|
}, [] as AdsListType) |
||||||
|
|
||||||
|
const maxDuration = combineAds |
||||||
|
.reduce((result, { duration }) => Math.max(result, duration), 0) |
||||||
|
|
||||||
|
return maxDuration |
||||||
|
} |
||||||
@ -0,0 +1 @@ |
|||||||
|
export * from './isVideo' |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
const regexp = /^https?:\/\/\S+(?:mp4)$/ |
||||||
|
export const checkVideo = (url: string) => regexp.test(url) |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
import { useMemo } from 'react' |
||||||
|
|
||||||
|
import { useQuery } from 'react-query' |
||||||
|
|
||||||
|
import { useRecoilState } from 'recoil' |
||||||
|
|
||||||
|
import { isMobileDevice, querieKeys } from 'config' |
||||||
|
|
||||||
|
import { getAds } from 'requests' |
||||||
|
|
||||||
|
import { isMatchPage } from 'helpers/isMatchPage' |
||||||
|
import { useLang } from 'features/LexicsStore/hooks/useLang' |
||||||
|
import { useAuthStore } from 'features/AuthStore' |
||||||
|
|
||||||
|
import { |
||||||
|
DeviceType, |
||||||
|
PageType, |
||||||
|
} from './types' |
||||||
|
import { calcMaxAdDurationAds } from './helpers/calcMaxDurationAds' |
||||||
|
import { adsStore } from '../../pages/HighlightsPage/storeHighlightsAtoms' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
matchId?: number, |
||||||
|
sportType?: number, |
||||||
|
tournamentId?: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const useAds = ({ |
||||||
|
matchId, |
||||||
|
sportType, |
||||||
|
tournamentId, |
||||||
|
}: Props) => { |
||||||
|
const [ads, setAds] = useRecoilState(adsStore) |
||||||
|
const { lang } = useLang() |
||||||
|
const { user } = useAuthStore() |
||||||
|
|
||||||
|
useQuery({ |
||||||
|
enabled: isMatchPage() ? (!!user && !!tournamentId) : !!user, |
||||||
|
queryFn: async () => { |
||||||
|
const adsList = await getAds({ |
||||||
|
client_type: isMobileDevice ? DeviceType.MOBILE : DeviceType.WEB, |
||||||
|
language: lang, |
||||||
|
type_id: isMatchPage() ? PageType.MATCH : PageType.HOME, |
||||||
|
...isMatchPage() && { |
||||||
|
matches: [{ |
||||||
|
match_id: matchId, |
||||||
|
sport_id: sportType, |
||||||
|
}], |
||||||
|
tournaments: [{ |
||||||
|
sport_id: sportType, |
||||||
|
tournament_id: tournamentId, |
||||||
|
}], |
||||||
|
}, |
||||||
|
}) |
||||||
|
adsList && setAds(adsList) |
||||||
|
return adsList |
||||||
|
}, |
||||||
|
queryKey: [querieKeys.ads, matchId], |
||||||
|
staleTime: useMemo(() => Math.max(calcMaxAdDurationAds(ads), 60 * 1000), [ads]), |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
ads, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import type { AdType } from 'requests' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import type { AdsPropsType } from './types' |
||||||
|
import { AdComponent } from './components/AdComponent' |
||||||
|
import { MobileAd } from './components/MobileAd' |
||||||
|
|
||||||
|
import { |
||||||
|
HeaderWrapAd, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export const HeaderAds = ({ ads }: AdsPropsType) => ( |
||||||
|
ads?.length ? ( |
||||||
|
<HeaderWrapAd column={ads?.length}> |
||||||
|
{ads.map((ad: AdType) => ( |
||||||
|
!isMobileDevice ? ( |
||||||
|
<AdComponent |
||||||
|
ad={ad} |
||||||
|
key={ad.id} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<MobileAd |
||||||
|
ad={ad} |
||||||
|
key={ad.id} |
||||||
|
/> |
||||||
|
) |
||||||
|
))} |
||||||
|
</HeaderWrapAd> |
||||||
|
) : null |
||||||
|
) |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
export const HeaderWrapAd = styled.div<{column: number}>` |
||||||
|
width: 100%; |
||||||
|
margin-bottom: 0.7rem; |
||||||
|
display: grid; |
||||||
|
grid-column-gap: 0.9rem; |
||||||
|
grid-template-columns: ${({ column }) => (column > 1 ? `repeat(${column},${16.3 * 6 / column}%)` : 'repeat(1, 98.7%)')}; |
||||||
|
|
||||||
|
${isMobileDevice && css` |
||||||
|
padding: 0 0.71rem; |
||||||
|
grid-template-columns: none; |
||||||
|
`}}
|
||||||
|
` |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
import type { AdsListType } from 'requests' |
||||||
|
|
||||||
|
export enum PageType { |
||||||
|
HOME = 1, |
||||||
|
MATCH = 2, |
||||||
|
} |
||||||
|
|
||||||
|
export enum DeviceType { |
||||||
|
MOBILE = 'mobile', |
||||||
|
WEB = 'web' |
||||||
|
} |
||||||
|
|
||||||
|
export type ViewsType = Partial<Record<keyof typeof PageType, number>> |
||||||
|
|
||||||
|
export enum EventGA { |
||||||
|
CLICK = 'banner_click', |
||||||
|
CLOSE = 'banner_close', |
||||||
|
DISPLAY = 'banner_display' |
||||||
|
} |
||||||
|
|
||||||
|
export enum VIEW_ADS { |
||||||
|
ROW = 4, |
||||||
|
COLUMN = 5, |
||||||
|
SQUARE = 6, |
||||||
|
SECOND_COLUMN = 2, |
||||||
|
SECOND_ROW = 3, |
||||||
|
MOBILE_IN_COLLAPSE_HEADER = 12, |
||||||
|
MOBILE_IN_COLLAPSE_FOOTER = 25 |
||||||
|
} |
||||||
|
|
||||||
|
export const HEADER_MOBILE_ADS = [10, 11] |
||||||
|
|
||||||
|
export enum MATCH_ADS { |
||||||
|
WATCH_TOP = 13, |
||||||
|
PLAYS_TOP = 14, |
||||||
|
PLAYS_BOTTOM = 15, |
||||||
|
PLAYS_TOP_MOBILE = 16, |
||||||
|
PLAYS_BOTTOM_MOBILE = 17, |
||||||
|
} |
||||||
|
|
||||||
|
export enum PLAYER_ADS { |
||||||
|
LEFT_BOTTOM = 18, |
||||||
|
CENTER_BOTTOM = 19, |
||||||
|
RIGHT = 20, |
||||||
|
FULL_SCREEN = 21, |
||||||
|
} |
||||||
|
|
||||||
|
export const PLAYER_MOBILE_ADS = 22 |
||||||
|
|
||||||
|
export enum PLAYER_MOBILE_FULL_SCREEN { |
||||||
|
VERTICAL_FULL_SCREEN = 23, |
||||||
|
HORIZONTAL_FULL_SCREEN = 24, |
||||||
|
} |
||||||
|
|
||||||
|
export const MATCH_PAGE_MOBILE_ADS = [16, 17, 22, 23, 24] |
||||||
|
|
||||||
|
export type AdsPropsType = Record<'ads', AdsListType | undefined> |
||||||
|
|
||||||
|
export const adsViews = 'adsViews' |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
import type { Props, State } from '../types' |
||||||
|
import { |
||||||
|
createVariableSizingTransformationSet, |
||||||
|
createClones, |
||||||
|
createDefaultTransformationSet, |
||||||
|
getElementDimensions, |
||||||
|
getItemsCount, |
||||||
|
getItemsOffset, |
||||||
|
getTransitionProperty, |
||||||
|
getTranslate3dProperty, |
||||||
|
getItemsInSlide, |
||||||
|
} from './elements' |
||||||
|
import { getActiveIndex, getStartIndex } from './math' |
||||||
|
|
||||||
|
export const calculateInitialState = (props: Props, el: HTMLElement | null): State => { |
||||||
|
const { |
||||||
|
activeIndex: propsActiveIndex = 0, |
||||||
|
animationDuration = 0, |
||||||
|
infinite = false, |
||||||
|
variableSizing = false, |
||||||
|
} = props |
||||||
|
const clones = createClones(props) |
||||||
|
const transition = getTransitionProperty() |
||||||
|
const itemsCount = getItemsCount(props) |
||||||
|
const itemsOffset = getItemsOffset(props) |
||||||
|
const itemsInSlide = getItemsInSlide(itemsCount, props) |
||||||
|
const startIndex = getStartIndex(propsActiveIndex, itemsCount) |
||||||
|
const activeIndex = getActiveIndex({ |
||||||
|
infinite, |
||||||
|
itemsCount, |
||||||
|
startIndex, |
||||||
|
}) |
||||||
|
const { width: listWidth } = getElementDimensions(el) |
||||||
|
|
||||||
|
const transformationSet = variableSizing |
||||||
|
? createVariableSizingTransformationSet({ |
||||||
|
el, |
||||||
|
infinite, |
||||||
|
listWidth, |
||||||
|
props, |
||||||
|
}).coords |
||||||
|
: createDefaultTransformationSet({ |
||||||
|
children: clones, |
||||||
|
infinite, |
||||||
|
itemsInSlide, |
||||||
|
listWidth, |
||||||
|
props, |
||||||
|
}).coords |
||||||
|
|
||||||
|
const translate3d = getTranslate3dProperty(activeIndex, { |
||||||
|
infinite, |
||||||
|
itemsInSlide, |
||||||
|
itemsOffset, |
||||||
|
transformationSet, |
||||||
|
variableSizing, |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
activeIndex, |
||||||
|
animationDuration, |
||||||
|
clones, |
||||||
|
infinite, |
||||||
|
itemsCount, |
||||||
|
itemsInSlide, |
||||||
|
itemsOffset, |
||||||
|
listWidth, |
||||||
|
transformationSet, |
||||||
|
transition, |
||||||
|
translate3d, |
||||||
|
variableSizing, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,274 @@ |
|||||||
|
import type { CSSProperties, ReactNode } from 'react' |
||||||
|
import { Children } from 'react' |
||||||
|
|
||||||
|
import type { |
||||||
|
Transformations, |
||||||
|
ItemCoords, |
||||||
|
Props, |
||||||
|
State, |
||||||
|
Transition, |
||||||
|
} from '../types' |
||||||
|
import { mapPartialCoords, mapPositionCoords } from './mappers' |
||||||
|
import { getShiftIndex } from './math' |
||||||
|
|
||||||
|
export const getSlides = ({ children }: Props) => Children.toArray(children) |
||||||
|
|
||||||
|
export const getItemsCount = (props: Props) => getSlides(props).length |
||||||
|
|
||||||
|
export const getItemsOffset = ({ infinite }: Props) => (infinite ? 1 : 0) |
||||||
|
|
||||||
|
export const createClones = (props: Props) => { |
||||||
|
const slides = getSlides(props) |
||||||
|
|
||||||
|
if (!props.infinite) return slides |
||||||
|
|
||||||
|
const itemsCount = getItemsCount(props) |
||||||
|
const itemsOffset = getItemsOffset(props) |
||||||
|
const itemsInSlide = getItemsInSlide(itemsCount, props) |
||||||
|
const cursor = Math.min(itemsInSlide, itemsCount) + itemsOffset |
||||||
|
|
||||||
|
const clonesAfter = slides.slice(0, cursor) |
||||||
|
const clonesBefore = slides.slice(-cursor) |
||||||
|
|
||||||
|
if (itemsOffset && itemsInSlide === itemsCount) { |
||||||
|
const afterOffsetClone = slides[0] |
||||||
|
const [beforeOffsetClone] = slides.slice(-1) |
||||||
|
|
||||||
|
clonesBefore.unshift(beforeOffsetClone) |
||||||
|
clonesAfter.push(afterOffsetClone) |
||||||
|
} |
||||||
|
|
||||||
|
return clonesBefore.concat(slides, clonesAfter) |
||||||
|
} |
||||||
|
|
||||||
|
type CreateVariableSizingTransformationSetArgs = { |
||||||
|
el: HTMLElement | null, |
||||||
|
infinite: boolean, |
||||||
|
listWidth: number, |
||||||
|
props: Props, |
||||||
|
} |
||||||
|
|
||||||
|
export const createVariableSizingTransformationSet = ({ |
||||||
|
el, |
||||||
|
infinite, |
||||||
|
listWidth, |
||||||
|
props, |
||||||
|
}: CreateVariableSizingTransformationSetArgs) => { |
||||||
|
let content = 0 |
||||||
|
let partial = true |
||||||
|
let coords: Array<ItemCoords> = [] |
||||||
|
|
||||||
|
const { spaceBetween = 0 } = props |
||||||
|
|
||||||
|
const children: Array<HTMLElement | Element> = Array.from(el?.children || []) |
||||||
|
|
||||||
|
coords = children.reduce<Array<ItemCoords>>(( |
||||||
|
acc, |
||||||
|
child, |
||||||
|
i, |
||||||
|
) => { |
||||||
|
let position = 0 |
||||||
|
const previewsChildCursor = i - 1 |
||||||
|
const previewsChild = acc[previewsChildCursor] |
||||||
|
const { width = 0 } = getElementDimensions(child?.firstChild as HTMLElement) |
||||||
|
|
||||||
|
content += width + spaceBetween |
||||||
|
partial = listWidth >= content |
||||||
|
|
||||||
|
if (previewsChild) { |
||||||
|
position = previewsChildCursor === 0 |
||||||
|
? previewsChild.width + spaceBetween |
||||||
|
: previewsChild.width + previewsChild.position + spaceBetween |
||||||
|
} |
||||||
|
|
||||||
|
acc.push({ position, width }) |
||||||
|
return acc |
||||||
|
}, []) |
||||||
|
|
||||||
|
if (!infinite) { |
||||||
|
if (partial) { |
||||||
|
coords = mapPartialCoords(coords) |
||||||
|
} else { |
||||||
|
const position = content - listWidth |
||||||
|
coords = mapPositionCoords(coords, position) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
content, |
||||||
|
coords, |
||||||
|
partial, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type CreateDefaultTransformationSetArgs = { |
||||||
|
children: Array<ReactNode>, |
||||||
|
infinite: boolean, |
||||||
|
itemsInSlide: number, |
||||||
|
listWidth: number, |
||||||
|
props: Props, |
||||||
|
} |
||||||
|
|
||||||
|
export const createDefaultTransformationSet = ({ |
||||||
|
children, |
||||||
|
infinite, |
||||||
|
itemsInSlide, |
||||||
|
listWidth, |
||||||
|
props, |
||||||
|
}: CreateDefaultTransformationSetArgs): Transformations => { |
||||||
|
let content = 0 |
||||||
|
let partial = true |
||||||
|
let coords: Array<ItemCoords> = [] |
||||||
|
|
||||||
|
const { spaceBetween = 0 } = props |
||||||
|
|
||||||
|
const width = getItemWidth({ |
||||||
|
galleryWidth: listWidth, |
||||||
|
itemsInSlide, |
||||||
|
props, |
||||||
|
}) |
||||||
|
|
||||||
|
coords = children.reduce<Array<ItemCoords>>(( |
||||||
|
acc, |
||||||
|
_, |
||||||
|
i, |
||||||
|
) => { |
||||||
|
let position = 0 |
||||||
|
const previewsChild = acc[i - 1] |
||||||
|
|
||||||
|
content += width + spaceBetween |
||||||
|
partial = listWidth >= content |
||||||
|
|
||||||
|
if (previewsChild) { |
||||||
|
position = width + spaceBetween + previewsChild.position || 0 |
||||||
|
} |
||||||
|
|
||||||
|
acc.push({ position, width }) |
||||||
|
return acc |
||||||
|
}, []) |
||||||
|
|
||||||
|
if (!infinite) { |
||||||
|
if (partial) { |
||||||
|
coords = mapPartialCoords(coords) |
||||||
|
} else { |
||||||
|
const position = content - listWidth |
||||||
|
coords = mapPositionCoords(coords, position) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
content, |
||||||
|
coords, |
||||||
|
partial, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type GetItemWidthArgs = { |
||||||
|
galleryWidth: number, |
||||||
|
itemsInSlide: number, |
||||||
|
props: Props, |
||||||
|
} |
||||||
|
|
||||||
|
export const getItemWidth = ({ |
||||||
|
galleryWidth, |
||||||
|
itemsInSlide, |
||||||
|
props: { spaceBetween = 0 }, |
||||||
|
}: GetItemWidthArgs) => (itemsInSlide > 0 |
||||||
|
? (galleryWidth - spaceBetween * (itemsInSlide - 1)) / itemsInSlide |
||||||
|
: galleryWidth) |
||||||
|
|
||||||
|
export const getElementDimensions = (element: HTMLElement | null) => { |
||||||
|
const { height, width } = element?.getBoundingClientRect() || { height: 0, width: 0 } |
||||||
|
|
||||||
|
return { height, width } |
||||||
|
} |
||||||
|
|
||||||
|
export const getTransitionProperty = (options?: Transition): string => { |
||||||
|
const { animationDuration = 0, animationTimingFunction = 'ease' } = options || {} |
||||||
|
return `transform ${animationDuration}ms ${animationTimingFunction} 0ms` |
||||||
|
} |
||||||
|
|
||||||
|
export const getListElementStyles = ( |
||||||
|
{ translate3d }: Partial<State>, |
||||||
|
currentStyles: CSSProperties, |
||||||
|
): CSSProperties => { |
||||||
|
const transform = `translate3d(${-(translate3d || 0)}px, 0, 0)` |
||||||
|
|
||||||
|
return { ...currentStyles, transform } |
||||||
|
} |
||||||
|
|
||||||
|
export const getItemStyles = (index: number, state: State): CSSProperties => { |
||||||
|
const { transformationSet } = state |
||||||
|
const { width } = transformationSet[index] || {} |
||||||
|
|
||||||
|
return { |
||||||
|
width, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const getTranslate3dProperty = (nextIndex: number, state: Partial<State>) => { |
||||||
|
let cursor = nextIndex |
||||||
|
|
||||||
|
const { |
||||||
|
infinite, |
||||||
|
itemsInSlide = 0, |
||||||
|
itemsOffset = 0, |
||||||
|
transformationSet = [], |
||||||
|
} = state |
||||||
|
|
||||||
|
if (infinite) { |
||||||
|
cursor = nextIndex + getShiftIndex(itemsInSlide, itemsOffset) |
||||||
|
} |
||||||
|
|
||||||
|
return (transformationSet[cursor] || {}).position || 0 |
||||||
|
} |
||||||
|
|
||||||
|
export const isDisplayedItem = (i = 0, state: State) => { |
||||||
|
const { |
||||||
|
activeIndex, |
||||||
|
infinite, |
||||||
|
itemsInSlide, |
||||||
|
itemsOffset, |
||||||
|
variableSizing, |
||||||
|
} = state |
||||||
|
|
||||||
|
const shiftIndex = getShiftIndex(itemsInSlide, itemsOffset) |
||||||
|
|
||||||
|
if (variableSizing && infinite) { |
||||||
|
return i - shiftIndex === activeIndex + itemsOffset |
||||||
|
} |
||||||
|
|
||||||
|
const index = activeIndex + shiftIndex |
||||||
|
|
||||||
|
if (!infinite) { |
||||||
|
return i >= activeIndex && i < index |
||||||
|
} |
||||||
|
|
||||||
|
return i >= index && i < index + itemsInSlide |
||||||
|
} |
||||||
|
|
||||||
|
export const getItemsInSlide = (itemsCount: number, props: Props) => { |
||||||
|
let itemsInSlide = 1 |
||||||
|
|
||||||
|
const { |
||||||
|
breakpoints = {}, |
||||||
|
infinite, |
||||||
|
variableSizing, |
||||||
|
} = props |
||||||
|
|
||||||
|
if (variableSizing) { |
||||||
|
return infinite ? itemsCount : itemsInSlide |
||||||
|
} |
||||||
|
|
||||||
|
const configKeys = Object.keys(breakpoints) |
||||||
|
|
||||||
|
configKeys.forEach((key) => { |
||||||
|
if (Number(key) <= window.innerWidth) { |
||||||
|
const { items, itemsFit = 'fill' } = breakpoints[key] |
||||||
|
|
||||||
|
itemsInSlide = itemsFit === 'contain' ? items : Math.min(items, itemsCount) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return itemsInSlide || 1 |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export * from './common' |
||||||
|
export * from './elements' |
||||||
|
export * from './math' |
||||||
|
export * from './mappers' |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
import type { ItemCoords } from '../types' |
||||||
|
|
||||||
|
export const mapPartialCoords = (coords: Array<ItemCoords>) => ( |
||||||
|
coords.map(({ width }) => ({ position: 0, width })) |
||||||
|
) |
||||||
|
|
||||||
|
export const mapPositionCoords = (coords: Array<ItemCoords>, position = 0) => coords.map((item) => { |
||||||
|
if (item.position > position) { |
||||||
|
return { ...item, position } |
||||||
|
} |
||||||
|
return item |
||||||
|
}) |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
import type { ItemCoords } from '../types' |
||||||
|
|
||||||
|
export const getShiftIndex = (itemsInSlide = 0, itemsOffset = 0) => itemsInSlide + itemsOffset |
||||||
|
|
||||||
|
export const getStartIndex = (index = 0, itemsCount = 0) => { |
||||||
|
if (itemsCount) { |
||||||
|
if (index >= itemsCount) { |
||||||
|
return itemsCount - 1 |
||||||
|
} |
||||||
|
|
||||||
|
if (index > 0) { |
||||||
|
return index |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
export const getActiveIndex = ({ |
||||||
|
infinite = false, |
||||||
|
itemsCount = 0, |
||||||
|
startIndex = 0, |
||||||
|
}) => (infinite ? startIndex : getStartIndex(startIndex, itemsCount)) |
||||||
|
|
||||||
|
export const getUpdateSlidePositionIndex = (activeIndex: number, itemsCount: number) => { |
||||||
|
if (activeIndex < 0) return itemsCount - 1 |
||||||
|
if (activeIndex >= itemsCount) return 0 |
||||||
|
|
||||||
|
return activeIndex |
||||||
|
} |
||||||
|
|
||||||
|
export const shouldRecalculateSlideIndex = (activeIndex: number, itemsCount: number) => ( |
||||||
|
activeIndex < 0 || activeIndex >= itemsCount |
||||||
|
) |
||||||
|
|
||||||
|
export const shouldCancelSlideAnimation = (activeIndex: number, itemsCount: number) => ( |
||||||
|
activeIndex < 0 || activeIndex >= itemsCount |
||||||
|
) |
||||||
|
|
||||||
|
export const getTransformationItemIndex = ( |
||||||
|
transformationSet: Array<ItemCoords> = [], |
||||||
|
position = 0, |
||||||
|
) => transformationSet.findIndex((item) => item.position >= Math.abs(position)) |
||||||
|
|
||||||
@ -0,0 +1,160 @@ |
|||||||
|
import { |
||||||
|
useRef, |
||||||
|
useEffect, |
||||||
|
useLayoutEffect, |
||||||
|
type ReactNode, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import { KEYBOARD_KEYS } from 'config' |
||||||
|
|
||||||
|
import { useEventListener, useObjectState } from 'hooks' |
||||||
|
|
||||||
|
import type { State, Props } from './types' |
||||||
|
import * as Utils from './helpers' |
||||||
|
import { ListItem } from './styled' |
||||||
|
|
||||||
|
export const useCarousel = (props: Props) => { |
||||||
|
const [state, setState] = useObjectState<State>(Utils.calculateInitialState(props, null)) |
||||||
|
const isAnimationDisabledRef = useRef(false) |
||||||
|
const slideEndTimeoutIdRef = useRef<number | null>(null) |
||||||
|
const listElementRef = useRef<HTMLUListElement>(null) |
||||||
|
const rootElementRef = useRef<HTMLDivElement>(null) |
||||||
|
|
||||||
|
const { |
||||||
|
activeIndex, |
||||||
|
animationDuration, |
||||||
|
clones, |
||||||
|
itemsCount, |
||||||
|
itemsInSlide, |
||||||
|
transition, |
||||||
|
translate3d, |
||||||
|
} = state |
||||||
|
|
||||||
|
const { |
||||||
|
animationTimingFunction, |
||||||
|
infinite, |
||||||
|
onSlideChange, |
||||||
|
useKeyboardNavigation, |
||||||
|
} = props |
||||||
|
|
||||||
|
const listElementStyles = Utils.getListElementStyles({ translate3d }, { transition }) |
||||||
|
|
||||||
|
const clearSlideEndTimeout = () => { |
||||||
|
slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current) |
||||||
|
slideEndTimeoutIdRef.current = null |
||||||
|
} |
||||||
|
|
||||||
|
const slidePrev = () => { |
||||||
|
const newActiveIndex = activeIndex - 1 |
||||||
|
|
||||||
|
handleSlideTo(newActiveIndex) |
||||||
|
} |
||||||
|
|
||||||
|
const slideNext = () => { |
||||||
|
const newActiveIndex = activeIndex + 1 |
||||||
|
|
||||||
|
handleSlideTo(newActiveIndex) |
||||||
|
} |
||||||
|
|
||||||
|
const handleUpdateSlidePosition = (index: number) => { |
||||||
|
const newTranslate3d = Utils.getTranslate3dProperty(index, state) |
||||||
|
const newTransition = Utils.getTransitionProperty({ animationDuration: 0 }) |
||||||
|
|
||||||
|
setState({ |
||||||
|
activeIndex: index, |
||||||
|
transition: newTransition, |
||||||
|
translate3d: newTranslate3d, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleBeforeSlideEnd = (index: number) => { |
||||||
|
if (Utils.shouldRecalculateSlideIndex(index, itemsCount)) { |
||||||
|
const nextIndex = Utils.getUpdateSlidePositionIndex(index, itemsCount) |
||||||
|
handleUpdateSlidePosition(nextIndex) |
||||||
|
} |
||||||
|
|
||||||
|
isAnimationDisabledRef.current = false |
||||||
|
} |
||||||
|
|
||||||
|
const handleSlideTo = (newActiveIndex = 0) => { |
||||||
|
if ( |
||||||
|
isAnimationDisabledRef.current |
||||||
|
|| newActiveIndex === activeIndex |
||||||
|
|| (!infinite && Utils.shouldCancelSlideAnimation(newActiveIndex, itemsCount)) |
||||||
|
) return |
||||||
|
|
||||||
|
isAnimationDisabledRef.current = true |
||||||
|
clearSlideEndTimeout() |
||||||
|
|
||||||
|
const newTranslate3d = Utils.getTranslate3dProperty(newActiveIndex, state) |
||||||
|
|
||||||
|
const newTransition = Utils.getTransitionProperty({ |
||||||
|
animationDuration, |
||||||
|
animationTimingFunction, |
||||||
|
}) |
||||||
|
|
||||||
|
onSlideChange?.(newActiveIndex) |
||||||
|
|
||||||
|
setState({ |
||||||
|
activeIndex: newActiveIndex, |
||||||
|
transition: newTransition, |
||||||
|
translate3d: newTranslate3d, |
||||||
|
}) |
||||||
|
|
||||||
|
slideEndTimeoutIdRef.current = setTimeout( |
||||||
|
() => handleBeforeSlideEnd(newActiveIndex), |
||||||
|
animationDuration, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const renderItem = (item: ReactNode, index: number) => { |
||||||
|
const styles = Utils.getItemStyles(index, state) |
||||||
|
|
||||||
|
return ( |
||||||
|
<ListItem |
||||||
|
key={index} |
||||||
|
style={styles} |
||||||
|
aria-hidden={!Utils.isDisplayedItem(index, state)} |
||||||
|
> |
||||||
|
{item} |
||||||
|
</ListItem> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
useEventListener({ |
||||||
|
callback: (e) => { |
||||||
|
if (!useKeyboardNavigation) return |
||||||
|
if (e.key === KEYBOARD_KEYS.ArrowLeft) slidePrev() |
||||||
|
if (e.key === KEYBOARD_KEYS.ArrowRight) slideNext() |
||||||
|
}, |
||||||
|
event: 'keydown', |
||||||
|
}) |
||||||
|
|
||||||
|
useLayoutEffect(() => { |
||||||
|
const setInitialState = () => { |
||||||
|
const initialState = Utils.calculateInitialState(props, listElementRef.current) |
||||||
|
|
||||||
|
setState(initialState) |
||||||
|
} |
||||||
|
|
||||||
|
setInitialState() |
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [props.activeIndex]) |
||||||
|
|
||||||
|
useEffect(() => () => { |
||||||
|
slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current) |
||||||
|
}, []) |
||||||
|
|
||||||
|
return { |
||||||
|
activeIndex, |
||||||
|
clones, |
||||||
|
itemsCount, |
||||||
|
itemsInSlide, |
||||||
|
listElementRef, |
||||||
|
listElementStyles, |
||||||
|
renderItem, |
||||||
|
rootElementRef, |
||||||
|
slideNext, |
||||||
|
slidePrev, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
import { ArrowButton, Arrow } from 'features/HeaderFilters/components/DateFilter/styled' |
||||||
|
|
||||||
|
import type { Props } from './types' |
||||||
|
import { useCarousel } from './hooks' |
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
List, |
||||||
|
ButtonsWrapper, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export * from './types' |
||||||
|
|
||||||
|
type NavButtonProps = { |
||||||
|
direction: 'left' | 'right', |
||||||
|
disabled?: boolean, |
||||||
|
onClick: () => void, |
||||||
|
} |
||||||
|
|
||||||
|
const NavButton = ({ |
||||||
|
direction, |
||||||
|
disabled, |
||||||
|
onClick, |
||||||
|
}: NavButtonProps) => ( |
||||||
|
<ArrowButton |
||||||
|
aria-label={direction === 'left' ? 'Previous' : 'Next'} |
||||||
|
disabled={disabled} |
||||||
|
onClick={onClick} |
||||||
|
> |
||||||
|
<Arrow direction={direction} /> |
||||||
|
</ArrowButton> |
||||||
|
) |
||||||
|
|
||||||
|
export const Carousel = (props: Props) => { |
||||||
|
const { |
||||||
|
infinite, |
||||||
|
renderNextButton, |
||||||
|
renderPrevButton, |
||||||
|
} = props |
||||||
|
|
||||||
|
const { |
||||||
|
activeIndex, |
||||||
|
clones, |
||||||
|
itemsCount, |
||||||
|
itemsInSlide, |
||||||
|
listElementRef, |
||||||
|
listElementStyles, |
||||||
|
renderItem, |
||||||
|
rootElementRef, |
||||||
|
slideNext, |
||||||
|
slidePrev, |
||||||
|
} = useCarousel(props) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Wrapper ref={rootElementRef}> |
||||||
|
<List ref={listElementRef} style={listElementStyles}> |
||||||
|
{clones.map(renderItem)} |
||||||
|
</List> |
||||||
|
<ButtonsWrapper> |
||||||
|
{renderPrevButton |
||||||
|
? renderPrevButton({ |
||||||
|
disabled: !infinite && activeIndex === 0, |
||||||
|
onClick: slidePrev, |
||||||
|
}) |
||||||
|
: ( |
||||||
|
<NavButton |
||||||
|
disabled={!infinite && activeIndex === 0} |
||||||
|
onClick={slidePrev} |
||||||
|
direction='left' |
||||||
|
/> |
||||||
|
)} |
||||||
|
{renderNextButton |
||||||
|
? renderNextButton({ |
||||||
|
disabled: !infinite && itemsInSlide + activeIndex === itemsCount, |
||||||
|
onClick: slideNext, |
||||||
|
}) |
||||||
|
: ( |
||||||
|
<NavButton |
||||||
|
disabled={!infinite && itemsInSlide + activeIndex === itemsCount} |
||||||
|
onClick={slideNext} |
||||||
|
direction='right' |
||||||
|
/> |
||||||
|
)} |
||||||
|
</ButtonsWrapper> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
width: 100%; |
||||||
|
margin: auto; |
||||||
|
overflow: hidden; |
||||||
|
` |
||||||
|
|
||||||
|
export const List = styled.ul` |
||||||
|
display: flex; |
||||||
|
gap: 20px; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
` |
||||||
|
|
||||||
|
export const ListItem = styled.li` |
||||||
|
flex-shrink: 0; |
||||||
|
height: 100%; |
||||||
|
` |
||||||
|
|
||||||
|
export const ButtonsWrapper = styled.div` |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
` |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
import type { ReactNode } from 'react' |
||||||
|
|
||||||
|
type RenderButtonArgs = { |
||||||
|
disabled?: boolean, |
||||||
|
onClick: () => void, |
||||||
|
} |
||||||
|
|
||||||
|
export type Transition = { |
||||||
|
animationDuration?: number, |
||||||
|
animationTimingFunction?: string, |
||||||
|
} |
||||||
|
|
||||||
|
export type Breakpoints = { |
||||||
|
[key: string]: { |
||||||
|
// Число элементов в слайде
|
||||||
|
items: number, |
||||||
|
// Определяет, как элемент должен заполнять контейнер в соответствии с шириной слайда
|
||||||
|
itemsFit?: 'contain' | 'fill', |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export type Transformations = { |
||||||
|
content: number, |
||||||
|
coords: Array<ItemCoords>, |
||||||
|
partial: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export type ItemCoords = { |
||||||
|
position: number, |
||||||
|
width: number, |
||||||
|
} |
||||||
|
|
||||||
|
export type Props = { |
||||||
|
/** Текущая позиция */ |
||||||
|
activeIndex?: number, |
||||||
|
/** Длительность анимации */ |
||||||
|
animationDuration?: number, |
||||||
|
/** animation-timing-function */ |
||||||
|
animationTimingFunction?: string, |
||||||
|
/** Объект с брэкпойнтами */ |
||||||
|
breakpoints?: Breakpoints, |
||||||
|
/** Элементы карусели */ |
||||||
|
children: ReactNode, |
||||||
|
/** Бесконечный режим прокрутки */ |
||||||
|
infinite?: boolean, |
||||||
|
/** Колбэк при прокрутке */ |
||||||
|
onSlideChange?: (activeIndex: number) => void, |
||||||
|
/** Рендер-функция кнопки прокрутки вперед */ |
||||||
|
renderNextButton?: (args: RenderButtonArgs) => ReactNode, |
||||||
|
/** Рендер-функция кнопки прокрутки назад */ |
||||||
|
renderPrevButton?: (args: RenderButtonArgs) => ReactNode, |
||||||
|
/** Расстояние между элементами карусели */ |
||||||
|
spaceBetween?: number, |
||||||
|
/** Использование клавиатуры для навигации */ |
||||||
|
useKeyboardNavigation?: boolean, |
||||||
|
/** Использование произвольной ширины элементов карусели */ |
||||||
|
variableSizing?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export type State = { |
||||||
|
activeIndex: number, |
||||||
|
animationDuration?: number, |
||||||
|
clones: Array<ReactNode>, |
||||||
|
infinite?: boolean, |
||||||
|
itemsCount: number, |
||||||
|
itemsInSlide: number, |
||||||
|
itemsOffset: number, |
||||||
|
listWidth: number, |
||||||
|
transformationSet: Array<ItemCoords>, |
||||||
|
transition: string, |
||||||
|
translate3d: number, |
||||||
|
variableSizing: boolean, |
||||||
|
} |
||||||
@ -0,0 +1 @@ |
|||||||
|
export const COUNTRY = 'COUNTRY' |
||||||
@ -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) |
||||||
|
|||||||
@ -0,0 +1,94 @@ |
|||||||
|
import { useState, type MouseEvent } from 'react' |
||||||
|
|
||||||
|
import { useToggle } from 'hooks' |
||||||
|
|
||||||
|
import type { MatchPackage } from 'features/BuyMatchPopup/types' |
||||||
|
import { SubscriptionType } from 'features/BuyMatchPopup/types' |
||||||
|
import { Price } from 'features/Price' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store' |
||||||
|
import { ArrowLoader } from 'features/ArrowLoader' |
||||||
|
|
||||||
|
import { usePackage } from '../RegularPackage/usePackage' |
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
Header, |
||||||
|
Title, |
||||||
|
Button, |
||||||
|
Content, |
||||||
|
Description, |
||||||
|
Body, |
||||||
|
Border, |
||||||
|
SubscriptionTypeText, |
||||||
|
BestChoice, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
buttonId?: string, |
||||||
|
buttonLexic?: string, |
||||||
|
matchPackage: MatchPackage, |
||||||
|
onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void, |
||||||
|
} |
||||||
|
|
||||||
|
export const PackageMobile = ({ |
||||||
|
buttonId, |
||||||
|
buttonLexic, |
||||||
|
matchPackage, |
||||||
|
onButtonClick, |
||||||
|
}: Props) => { |
||||||
|
const { |
||||||
|
matchPackages, |
||||||
|
setSelectedPackage, |
||||||
|
} = useBuyMatchPopupStore() |
||||||
|
|
||||||
|
const isSinglePackage = matchPackages.length === 1 |
||||||
|
|
||||||
|
const { isOpen, toggle } = useToggle(isSinglePackage) |
||||||
|
const [loader, setLoader] = useState(false) |
||||||
|
|
||||||
|
const { firstDescription, priceTextTopLexic } = usePackage(matchPackage) |
||||||
|
|
||||||
|
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => { |
||||||
|
setSelectedPackage(matchPackage) |
||||||
|
onButtonClick?.(e, matchPackage) |
||||||
|
setLoader(true) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Wrapper> |
||||||
|
<Header |
||||||
|
isOpen={isOpen} |
||||||
|
onClick={isSinglePackage ? undefined : toggle} |
||||||
|
> |
||||||
|
<Title t={matchPackage.originalObject.sub.lexic1} /> |
||||||
|
<Price |
||||||
|
amount={matchPackage.originalObject.price} |
||||||
|
currency={matchPackage.currency} |
||||||
|
perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined} |
||||||
|
/> |
||||||
|
</Header> |
||||||
|
{isOpen && <Border />} |
||||||
|
<Content isOpen={isOpen}> |
||||||
|
<Body> |
||||||
|
<Description> |
||||||
|
{firstDescription} |
||||||
|
</Description> |
||||||
|
<Description> |
||||||
|
<T9n t={matchPackage.originalObject.sub.lexic3} /> |
||||||
|
</Description> |
||||||
|
{matchPackage.isMainPackage && ( |
||||||
|
<BestChoice> |
||||||
|
<T9n t='best_choice' /> |
||||||
|
</BestChoice> |
||||||
|
)} |
||||||
|
<SubscriptionTypeText t={priceTextTopLexic} /> |
||||||
|
</Body> |
||||||
|
<Button id={buttonId} onClick={handleButtonClick}> |
||||||
|
{loader |
||||||
|
? <ArrowLoader disabled /> |
||||||
|
: <T9n t={buttonLexic || ''} />} |
||||||
|
</Button> |
||||||
|
</Content> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,125 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { ButtonSolid } from 'features/Common' |
||||||
|
import { PriceDetails, PriceAmount } from 'features/Price/styled' |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
width: 100%; |
||||||
|
border-radius: 2px 2px 0px 0px; |
||||||
|
background-color: #414141; |
||||||
|
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); |
||||||
|
` |
||||||
|
|
||||||
|
type HeaderProps = { |
||||||
|
isOpen?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Header = styled.div<HeaderProps>` |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
height: 52px; |
||||||
|
align-items: center; |
||||||
|
padding: 0 15px 0 20px; |
||||||
|
|
||||||
|
${PriceAmount} { |
||||||
|
font-size: 24px; |
||||||
|
position: absolute; |
||||||
|
right: 46px; |
||||||
|
top: 50%; |
||||||
|
translate: 0 -50%; |
||||||
|
} |
||||||
|
|
||||||
|
${PriceDetails} { |
||||||
|
position: absolute; |
||||||
|
top: 12px; |
||||||
|
left: calc(100% - 48px); |
||||||
|
} |
||||||
|
|
||||||
|
${({ isOpen }) => (isOpen |
||||||
|
? '' |
||||||
|
: css` |
||||||
|
${PriceDetails}, ${PriceAmount}, ${Title} { |
||||||
|
color: #D9D9D9; |
||||||
|
} |
||||||
|
`)}
|
||||||
|
` |
||||||
|
|
||||||
|
export const Border = styled.div` |
||||||
|
height: 0.66px; |
||||||
|
background: linear-gradient(90deg, transparent 0%, #656565 50%, transparent 100%); |
||||||
|
` |
||||||
|
|
||||||
|
export const Title = styled(T9n)` |
||||||
|
max-width: calc(100% - 112px); |
||||||
|
font-weight: 600; |
||||||
|
font-size: 14px; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
overflow: hidden; |
||||||
|
` |
||||||
|
|
||||||
|
type ContentProps = { |
||||||
|
isOpen?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Content = styled.div<ContentProps>` |
||||||
|
height: ${({ isOpen }) => (isOpen ? 'auto' : 0)}; |
||||||
|
` |
||||||
|
|
||||||
|
export const Body = styled.div` |
||||||
|
position: relative; |
||||||
|
padding: 8px 24px 30px; |
||||||
|
background-color: #414141; |
||||||
|
` |
||||||
|
|
||||||
|
export const Description = styled.p` |
||||||
|
width: calc(100% - 90px); |
||||||
|
margin-bottom: 15px; |
||||||
|
font-weight: 500; |
||||||
|
font-size: 12px; |
||||||
|
line-height: 16px; |
||||||
|
color: #C7C7C7; |
||||||
|
|
||||||
|
:empty, :has(span:empty) { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
:last-of-type { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Button = styled(ButtonSolid)` |
||||||
|
width: 100%; |
||||||
|
height: 48px; |
||||||
|
` |
||||||
|
|
||||||
|
export const SubscriptionTypeText = styled(T9n)` |
||||||
|
position: absolute; |
||||||
|
right: 16px; |
||||||
|
bottom: 12px; |
||||||
|
font-size: 12px; |
||||||
|
font-weight: 600; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
` |
||||||
|
|
||||||
|
export const BestChoice = styled.div` |
||||||
|
position: absolute; |
||||||
|
bottom: 40px; |
||||||
|
right: 0; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: flex-end; |
||||||
|
width: 108px; |
||||||
|
height: 20px; |
||||||
|
padding-right: 16px; |
||||||
|
border-radius: 1px 0px 0px 1px; |
||||||
|
font-weight: 600; |
||||||
|
font-size: 12px; |
||||||
|
font-variant: all-small-caps; |
||||||
|
letter-spacing: 0.03em; |
||||||
|
color: #464646; |
||||||
|
background: linear-gradient(270deg, rgba(253, 253, 254, 0.8) 0%, rgba(253, 253, 254, 0) 97.42%); |
||||||
|
backdrop-filter: blur(3px); |
||||||
|
` |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import { Footer as FooterBase } from 'features/BuyMatchPopup/styled' |
||||||
|
|
||||||
|
export const ChooseSub = styled.div` |
||||||
|
font-weight: 700; |
||||||
|
font-size: ${isMobileDevice ? 16 : 24}px; |
||||||
|
margin: auto; |
||||||
|
color: ${({ theme }) => theme.colors.white}; |
||||||
|
` |
||||||
|
|
||||||
|
type FooterProps = { |
||||||
|
hasCard?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Footer = styled(FooterBase)<FooterProps>` |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
margin-top: ${({ hasCard }) => (hasCard ? 20 : 40)}px; |
||||||
|
` |
||||||
@ -1,41 +1,181 @@ |
|||||||
import styled from 'styled-components/macro' |
import type { MouseEvent } from 'react' |
||||||
|
import { useMemo } from 'react' |
||||||
|
|
||||||
import { PaymentPeriodTabs } from 'features/PaymentPeriodTabs' |
import reduce from 'lodash/reduce' |
||||||
|
import sortBy from 'lodash/sortBy' |
||||||
|
import without from 'lodash/without' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import type { MatchPackage } from 'features/BuyMatchPopup/types' |
||||||
|
import { useLexicsConfig } from 'features/LexicsStore' |
||||||
|
|
||||||
|
import { Carousel, type Breakpoints } from 'components/Carousel' |
||||||
|
|
||||||
import { useBuyMatchPopupStore } from '../../store' |
import { useBuyMatchPopupStore } from '../../store' |
||||||
import { PackagesList } from '../PackagesList' |
import { RegularPackage } from '../RegularPackage' |
||||||
|
import { PackageMobile } from '../PackageMobile' |
||||||
const Wrapper = styled.div` |
import { |
||||||
width: 100%; |
Wrapper, |
||||||
display: flex; |
List, |
||||||
flex-direction: column; |
PrevButton, |
||||||
align-items: center; |
NextButton, |
||||||
` |
Arrow, |
||||||
|
ListItem, |
||||||
export const Packages = () => { |
} from './styled' |
||||||
const { |
import { SinglePackage } from '../SinglePackage' |
||||||
onPackageSelect, |
|
||||||
onPeriodSelect, |
const breakpoints: Breakpoints = { |
||||||
selectedPackage, |
1000: { items: 3 }, |
||||||
selectedPeriod, |
1280: { items: 4 }, |
||||||
selectedSubscription, |
1400: { items: 5 }, |
||||||
subscriptions, |
} |
||||||
} = useBuyMatchPopupStore() |
|
||||||
|
type ButtonProps = { |
||||||
if (!selectedSubscription) return null |
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 ( |
return ( |
||||||
<Wrapper> |
<Wrapper> |
||||||
<PaymentPeriodTabs |
<List> |
||||||
onPeriodSelect={onPeriodSelect} |
{sortedPackages.map((matchPackage) => ( |
||||||
selectedPeriod={selectedPeriod} |
<ListItem key={matchPackage.id}> |
||||||
selectedSubscription={selectedSubscription} |
<PackageMobile |
||||||
/> |
matchPackage={matchPackage} |
||||||
<PackagesList |
onButtonClick={onButtonClick} |
||||||
packages={subscriptions} |
buttonId={buttonId} |
||||||
selectedPackage={selectedPackage} |
buttonLexic={buttonLexic} |
||||||
onSelect={onPackageSelect} |
|
||||||
/> |
/> |
||||||
|
</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 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> |
</Wrapper> |
||||||
) |
) |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,65 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { isMobileDevice } from 'config' |
||||||
|
|
||||||
|
import { ArrowButton, Arrow as ArrowBase } from 'features/HeaderFilters/components/DateFilter/styled' |
||||||
|
|
||||||
|
type WrapperProps = { |
||||||
|
width?: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const Wrapper = styled.div<WrapperProps>` |
||||||
|
width: ${({ width }) => (width ? `${width}px` : 'auto')}; |
||||||
|
margin: auto; |
||||||
|
` |
||||||
|
|
||||||
|
export const List = styled.ul` |
||||||
|
display: flex; |
||||||
|
gap: 20px; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
display: initial; |
||||||
|
max-height: calc(100vh - 140px); |
||||||
|
overflow-y: auto; |
||||||
|
` |
||||||
|
: ''} |
||||||
|
` |
||||||
|
|
||||||
|
export const ListItem = styled.li` |
||||||
|
width: 260px; |
||||||
|
|
||||||
|
${isMobileDevice |
||||||
|
? css` |
||||||
|
width: 100%; |
||||||
|
margin-bottom: 12px; |
||||||
|
border-radius: 2px; |
||||||
|
overflow: hidden; |
||||||
|
` |
||||||
|
: ''} |
||||||
|
` |
||||||
|
|
||||||
|
const NavButton = styled(ArrowButton)` |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
translate: 0 -50%; |
||||||
|
|
||||||
|
${({ disabled }) => (disabled |
||||||
|
? css` |
||||||
|
opacity: 0.5; |
||||||
|
` |
||||||
|
: '')} |
||||||
|
` |
||||||
|
|
||||||
|
export const PrevButton = styled(NavButton)` |
||||||
|
left: 30px; |
||||||
|
` |
||||||
|
|
||||||
|
export const NextButton = styled(NavButton)` |
||||||
|
right: 30px; |
||||||
|
` |
||||||
|
|
||||||
|
export const Arrow = styled(ArrowBase)` |
||||||
|
width: 20px; |
||||||
|
height: 20px; |
||||||
|
` |
||||||
@ -1,84 +0,0 @@ |
|||||||
import isNumber from 'lodash/isNumber' |
|
||||||
import map from 'lodash/map' |
|
||||||
|
|
||||||
import { T9n } from 'features/T9n' |
|
||||||
import { MatchPackage, SubscriptionType } from 'features/BuyMatchPopup/types' |
|
||||||
|
|
||||||
import { |
|
||||||
Pass, |
|
||||||
Name, |
|
||||||
Description, |
|
||||||
InfoWrapper, |
|
||||||
Item, |
|
||||||
List, |
|
||||||
Price, |
|
||||||
ScAutoRenewal, |
|
||||||
ScPriceContainer, |
|
||||||
} from './styled' |
|
||||||
|
|
||||||
type Props = { |
|
||||||
onSelect: (subscription: MatchPackage) => void, |
|
||||||
packages: Array<MatchPackage>, |
|
||||||
selectedPackage: MatchPackage | null, |
|
||||||
} |
|
||||||
|
|
||||||
export const PackagesList = ({ |
|
||||||
onSelect, |
|
||||||
packages, |
|
||||||
selectedPackage, |
|
||||||
}: Props) => ( |
|
||||||
<List> |
|
||||||
{ |
|
||||||
map( |
|
||||||
packages, |
|
||||||
(subPackage) => ( |
|
||||||
<Item |
|
||||||
key={subPackage.id} |
|
||||||
onClick={() => onSelect(subPackage)} |
|
||||||
active={subPackage === selectedPackage} |
|
||||||
> |
|
||||||
<InfoWrapper> |
|
||||||
<Pass> |
|
||||||
<T9n t={subPackage.pass} /> |
|
||||||
</Pass> |
|
||||||
<Name> |
|
||||||
{ |
|
||||||
isNumber(subPackage.nameLexic) |
|
||||||
? <T9n t={subPackage.nameLexic} /> |
|
||||||
: subPackage.name |
|
||||||
} |
|
||||||
</Name> |
|
||||||
<Description> |
|
||||||
<T9n |
|
||||||
t={subPackage.description.lexic} |
|
||||||
values={subPackage.description.values} |
|
||||||
/> |
|
||||||
</Description> |
|
||||||
</InfoWrapper> |
|
||||||
<ScPriceContainer> |
|
||||||
<Price |
|
||||||
amount={subPackage.price} |
|
||||||
currency={subPackage.currency} |
|
||||||
perPeriod={ |
|
||||||
subPackage.type !== SubscriptionType.Month |
|
||||||
? null |
|
||||||
: `per_${subPackage.type}` |
|
||||||
} |
|
||||||
/> |
|
||||||
{ |
|
||||||
subPackage.type === SubscriptionType.Month |
|
||||||
&& ( |
|
||||||
<ScAutoRenewal> |
|
||||||
<T9n |
|
||||||
t='auto_renewal' |
|
||||||
/> |
|
||||||
</ScAutoRenewal> |
|
||||||
) |
|
||||||
} |
|
||||||
</ScPriceContainer> |
|
||||||
</Item> |
|
||||||
), |
|
||||||
) |
|
||||||
} |
|
||||||
</List> |
|
||||||
) |
|
||||||
@ -1,230 +0,0 @@ |
|||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
|
|
||||||
import { popupScrollbarStyles } from 'features/PopupComponents' |
|
||||||
import { Price as BasePrice } from 'features/Price' |
|
||||||
import { |
|
||||||
PriceAmount, |
|
||||||
PriceDetails, |
|
||||||
Period, |
|
||||||
} from 'features/Price/styled' |
|
||||||
|
|
||||||
export const List = styled.ul` |
|
||||||
width: 100%; |
|
||||||
height: 460px; |
|
||||||
overflow-y: auto; |
|
||||||
margin-top: 25px; |
|
||||||
padding: 0 40px; |
|
||||||
|
|
||||||
${popupScrollbarStyles} |
|
||||||
|
|
||||||
@media (max-width: 1370px) { |
|
||||||
max-height: 415px; |
|
||||||
height: auto; |
|
||||||
} |
|
||||||
|
|
||||||
@media (max-height: 768px) { |
|
||||||
max-height: 280px; |
|
||||||
} |
|
||||||
|
|
||||||
@media (max-height: 500px) { |
|
||||||
max-height: 140px; |
|
||||||
} |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
padding: 0; |
|
||||||
margin-top: 19px; |
|
||||||
@media screen and (orientation: landscape){ |
|
||||||
margin-top: 10px; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
type ItemProps = { |
|
||||||
active?: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export const Item = styled.li.attrs(() => ({ |
|
||||||
tabIndex: 0, |
|
||||||
}))<ItemProps>` |
|
||||||
width: 100%; |
|
||||||
min-height: 140px; |
|
||||||
padding: 20px 30px 20px 20px; |
|
||||||
background: ${({ theme }) => theme.colors.packageBackground}; |
|
||||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); |
|
||||||
border-radius: 2px; |
|
||||||
|
|
||||||
display: flex; |
|
||||||
justify-content: space-between; |
|
||||||
align-items: center; |
|
||||||
cursor: pointer; |
|
||||||
transition: background-color 0.3s; |
|
||||||
|
|
||||||
:not(:last-child) { |
|
||||||
margin-bottom: 20px; |
|
||||||
} |
|
||||||
|
|
||||||
${({ active, theme: { colors } }) => ( |
|
||||||
active ? `background-color: ${colors.button}` : '' |
|
||||||
)}; |
|
||||||
|
|
||||||
@media (max-width: 1370px) { |
|
||||||
min-height: 125px; |
|
||||||
} |
|
||||||
|
|
||||||
@media (max-width: 750px) { |
|
||||||
height: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
padding: 5px 10px; |
|
||||||
|
|
||||||
@media (max-width: 750px) { |
|
||||||
min-height: 52.29px; |
|
||||||
height: auto; |
|
||||||
} |
|
||||||
|
|
||||||
:not(:last-child) { |
|
||||||
margin-bottom: 10px; |
|
||||||
|
|
||||||
@media screen and (orientation: landscape) { |
|
||||||
margin-bottom: 5.24px; |
|
||||||
} |
|
||||||
} |
|
||||||
` : ''};
|
|
||||||
|
|
||||||
@media (max-width: 850px) and (orientation: landscape){ |
|
||||||
min-height: 52.29px; |
|
||||||
height: auto; |
|
||||||
} |
|
||||||
` |
|
||||||
|
|
||||||
export const InfoWrapper = styled.div` |
|
||||||
width: 75%; |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
align-self: flex-start; |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
align-self: center; |
|
||||||
@media (max-width: 850px) and (orientation: landscape){ |
|
||||||
height: 100%; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
export const Name = styled.span` |
|
||||||
font-weight: 500; |
|
||||||
font-size: 20px; |
|
||||||
line-height: 23px; |
|
||||||
letter-spacing: 0.03em; |
|
||||||
|
|
||||||
@media (max-width: 1370px) { |
|
||||||
line-height: 20px; |
|
||||||
} |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
@media (max-width: 750px){ |
|
||||||
font-size: 12px; |
|
||||||
line-height: 10.04px; |
|
||||||
} |
|
||||||
@media screen and (orientation: landscape){ |
|
||||||
font-size: 14px; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
export const Pass = styled(Name)` |
|
||||||
font-weight: 600; |
|
||||||
text-transform: uppercase; |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
line-height: 12px; |
|
||||||
@media screen and (orientation: landscape){ |
|
||||||
line-height: 14px; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
export const Description = styled.span` |
|
||||||
width: 68%; |
|
||||||
margin-top: 13px; |
|
||||||
font-weight: 500; |
|
||||||
font-size: 15px; |
|
||||||
line-height: 20px; |
|
||||||
letter-spacing: 0.03em; |
|
||||||
|
|
||||||
@media (max-width: 1370px) { |
|
||||||
line-height: 18px; |
|
||||||
} |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
@media (max-width: 750px){ |
|
||||||
font-size: 8px; |
|
||||||
line-height: 8px; |
|
||||||
margin-top: 5px; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
@media (max-width: 850px) and (orientation: landscape){ |
|
||||||
margin-top: 0; |
|
||||||
line-height: 8px; |
|
||||||
font-size: 10px; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
export const Price = styled(BasePrice)` |
|
||||||
${PriceAmount} { |
|
||||||
font-size: 24px; |
|
||||||
line-height: 24px; |
|
||||||
font-weight: normal; |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
font-size: 14px; |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
} |
|
||||||
|
|
||||||
${PriceDetails} { |
|
||||||
font-weight: 500; |
|
||||||
font-size: 12px; |
|
||||||
line-height: 18px; |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
font-size: 8px; |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
} |
|
||||||
|
|
||||||
${Period} { |
|
||||||
text-transform: capitalize; |
|
||||||
} |
|
||||||
` |
|
||||||
|
|
||||||
export const ScAutoRenewal = styled(PriceDetails)` |
|
||||||
line-height: 21px; |
|
||||||
font-size: 12px; |
|
||||||
text-transform: none; |
|
||||||
margin: 0; |
|
||||||
color: ${({ theme: { colors } }) => colors.white70}; |
|
||||||
|
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
line-height: normal; |
|
||||||
font-size: 10px; |
|
||||||
text-align: center; |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
export const ScPriceContainer = styled.div` |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
` |
|
||||||
@ -0,0 +1,74 @@ |
|||||||
|
import type { MatchPackage } from 'features/BuyMatchPopup/types' |
||||||
|
import { SubscriptionType } from 'features/BuyMatchPopup/types' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { Price } from 'features/Price' |
||||||
|
|
||||||
|
import { usePackage } from './usePackage' |
||||||
|
|
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
InfoWrapper, |
||||||
|
Description, |
||||||
|
Header, |
||||||
|
HeaderTitle, |
||||||
|
BestChoice, |
||||||
|
PriceBlock, |
||||||
|
PriceTextWrapper, |
||||||
|
PriceTextTop, |
||||||
|
PriceTextBottom, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
matchPackage: MatchPackage, |
||||||
|
} |
||||||
|
|
||||||
|
export const RegularPackage = ({ matchPackage }: Props) => { |
||||||
|
const { |
||||||
|
firstDescription, |
||||||
|
handleClick, |
||||||
|
handleKeyPress, |
||||||
|
isActive, |
||||||
|
priceTextBottomLexic, |
||||||
|
priceTextTopLexic, |
||||||
|
} = usePackage(matchPackage) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Wrapper |
||||||
|
onClick={handleClick} |
||||||
|
onKeyDown={handleKeyPress} |
||||||
|
active={isActive} |
||||||
|
isMainPackage={matchPackage.isMainPackage} |
||||||
|
> |
||||||
|
<Header> |
||||||
|
<HeaderTitle |
||||||
|
width={matchPackage.id.startsWith('team_') ? 190 : undefined} |
||||||
|
t={matchPackage.originalObject.sub.lexic1} |
||||||
|
/> |
||||||
|
{matchPackage.isMainPackage && ( |
||||||
|
<BestChoice> |
||||||
|
<T9n t='best_choice' /> |
||||||
|
</BestChoice> |
||||||
|
)} |
||||||
|
</Header> |
||||||
|
<InfoWrapper> |
||||||
|
<Description> |
||||||
|
{firstDescription} |
||||||
|
</Description> |
||||||
|
<Description> |
||||||
|
<T9n t={matchPackage.originalObject.sub.lexic3} /> |
||||||
|
</Description> |
||||||
|
<PriceBlock isMonthSubscription={matchPackage.isMonthSubscription}> |
||||||
|
<PriceTextWrapper> |
||||||
|
<PriceTextTop t={priceTextTopLexic} /> |
||||||
|
<PriceTextBottom t={priceTextBottomLexic} /> |
||||||
|
</PriceTextWrapper> |
||||||
|
<Price |
||||||
|
amount={matchPackage.originalObject.price} |
||||||
|
currency={matchPackage.currency} |
||||||
|
perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined} |
||||||
|
/> |
||||||
|
</PriceBlock> |
||||||
|
</InfoWrapper> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,140 @@ |
|||||||
|
import styled, { css } from 'styled-components/macro' |
||||||
|
|
||||||
|
import { PriceDetails } from 'features/Price/styled' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
export const Description = styled.p` |
||||||
|
margin-bottom: 20px; |
||||||
|
font-weight: 400; |
||||||
|
font-size: 12px; |
||||||
|
line-height: 20px; |
||||||
|
white-space: initial; |
||||||
|
|
||||||
|
:empty, :has(span:empty) { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
:last-of-type { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
type PriceBlockProps = { |
||||||
|
isMonthSubscription?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const PriceBlock = styled.div<PriceBlockProps>` |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
position: absolute; |
||||||
|
top: 218px; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
padding-left: 30px; |
||||||
|
padding-right: ${({ isMonthSubscription }) => (isMonthSubscription ? 10 : 20)}px; |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceTextWrapper = styled.div` |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
white-space: initial; |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceTextTop = styled(T9n)` |
||||||
|
margin-bottom: 2px; |
||||||
|
font-weight: 600; |
||||||
|
font-size: 10px; |
||||||
|
font-style: italic; |
||||||
|
letter-spacing: 0.03em; |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceTextBottom = styled(T9n)` |
||||||
|
font-weight: 400; |
||||||
|
font-size: 8px; |
||||||
|
font-style: italic; |
||||||
|
letter-spacing: 0.03em; |
||||||
|
` |
||||||
|
|
||||||
|
type WrapperProps = { |
||||||
|
active?: boolean, |
||||||
|
isMainPackage?: boolean, |
||||||
|
} |
||||||
|
|
||||||
|
export const Wrapper = styled.div.attrs({ |
||||||
|
tabIndex: 0, |
||||||
|
})<WrapperProps>` |
||||||
|
width: 100%; |
||||||
|
height: 380px; |
||||||
|
border-radius: 2px; |
||||||
|
background: ${({ theme }) => theme.colors.packageBackground}; |
||||||
|
box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1); |
||||||
|
transition: background-color 0.3s; |
||||||
|
cursor: pointer; |
||||||
|
|
||||||
|
${({ active, theme: { colors } }) => (active |
||||||
|
? css` |
||||||
|
background-color: ${colors.button}; |
||||||
|
` |
||||||
|
: css` |
||||||
|
:hover { |
||||||
|
background-color: #2F3E6D; |
||||||
|
}` |
||||||
|
)} |
||||||
|
|
||||||
|
${({ isMainPackage, theme: { colors } }) => (isMainPackage |
||||||
|
? css` |
||||||
|
border: 2px solid ${colors.white}; |
||||||
|
` |
||||||
|
: '' |
||||||
|
)} |
||||||
|
` |
||||||
|
|
||||||
|
export const Header = styled.header` |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
height: 100px; |
||||||
|
background: rgba(255, 255, 255, 0.2); |
||||||
|
` |
||||||
|
|
||||||
|
type HeaderTitleProps = { |
||||||
|
width?: number, |
||||||
|
} |
||||||
|
|
||||||
|
export const HeaderTitle = styled(T9n)<HeaderTitleProps>` |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
width: ${({ width }) => (width ? `${width}px` : 'auto')}; |
||||||
|
padding-left: 30px; |
||||||
|
white-space: initial; |
||||||
|
font-size: 16px; |
||||||
|
font-weight: 600; |
||||||
|
` |
||||||
|
|
||||||
|
export const BestChoice = styled.div` |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
height: 22px; |
||||||
|
margin-top: auto; |
||||||
|
padding-left: 32px; |
||||||
|
font-size: 12px; |
||||||
|
font-weight: 600; |
||||||
|
letter-spacing: 0.03em; |
||||||
|
font-variant: all-small-caps; |
||||||
|
color: #464646; |
||||||
|
background-color: #FDFDFE; |
||||||
|
` |
||||||
|
|
||||||
|
export const InfoWrapper = styled.div` |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
height: calc(100% - 100px); |
||||||
|
padding: 20px 30px 30px; |
||||||
|
|
||||||
|
${PriceDetails} { |
||||||
|
margin-top: 5px; |
||||||
|
} |
||||||
|
` |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
import type { KeyboardEvent } from 'react' |
||||||
|
|
||||||
|
import { KEYBOARD_KEYS } from 'config' |
||||||
|
|
||||||
|
import type { MatchPackage } from 'features/BuyMatchPopup/types' |
||||||
|
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup' |
||||||
|
import { useLexicsStore } from 'features/LexicsStore' |
||||||
|
|
||||||
|
export const usePackage = (matchPackage: MatchPackage) => { |
||||||
|
const { |
||||||
|
onPackageSelect, |
||||||
|
selectedPackage, |
||||||
|
} = useBuyMatchPopupStore() |
||||||
|
|
||||||
|
const { suffix, translate } = useLexicsStore() |
||||||
|
|
||||||
|
const isActive = matchPackage.id === selectedPackage?.id |
||||||
|
|
||||||
|
const priceTextTopLexic = matchPackage.isMonthSubscription ? 'subscription' : 'purchase' |
||||||
|
const priceTextBottomLexic = matchPackage.isMonthSubscription ? 'cancel_anytime' : 'one_off_payment' |
||||||
|
|
||||||
|
const getFirstDescription = () => { |
||||||
|
const tournament = matchPackage.match.tournament[`name_${suffix}`] |
||||||
|
const season = typeof matchPackage.description.values.season === 'string' |
||||||
|
? `${matchPackage.description.values.season.slice(0, 4)}/${matchPackage.description.values.season.slice(-2)}` |
||||||
|
: '' |
||||||
|
|
||||||
|
switch (true) { |
||||||
|
case matchPackage.originalObject.sub.lexic2 !== null: |
||||||
|
return translate(matchPackage.originalObject.sub.lexic2!) |
||||||
|
|
||||||
|
case matchPackage.id === '0': |
||||||
|
return matchPackage.name |
||||||
|
|
||||||
|
case matchPackage.id.startsWith('team1'): |
||||||
|
case matchPackage.id.startsWith('team2'): |
||||||
|
return `${translate('all_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}` |
||||||
|
|
||||||
|
case matchPackage.id.startsWith('team_home'): |
||||||
|
return `${translate('all_home_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}` |
||||||
|
|
||||||
|
case matchPackage.id.startsWith('team_away'): |
||||||
|
return `${translate('all_away_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}` |
||||||
|
|
||||||
|
case matchPackage.id.startsWith('all'): |
||||||
|
return `${tournament} ${translate('in_season')} ${season}` |
||||||
|
|
||||||
|
default: return '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleClick = () => { |
||||||
|
onPackageSelect(matchPackage) |
||||||
|
} |
||||||
|
|
||||||
|
const handleKeyPress = (e: KeyboardEvent) => { |
||||||
|
if (e.key === KEYBOARD_KEYS.Enter) { |
||||||
|
onPackageSelect(matchPackage) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
firstDescription: getFirstDescription(), |
||||||
|
handleClick, |
||||||
|
handleKeyPress, |
||||||
|
isActive, |
||||||
|
priceTextBottomLexic, |
||||||
|
priceTextTopLexic, |
||||||
|
} |
||||||
|
} |
||||||
@ -1,115 +0,0 @@ |
|||||||
import { useCallback } from 'react' |
|
||||||
|
|
||||||
import map from 'lodash/map' |
|
||||||
|
|
||||||
import { MDASH } from 'config' |
|
||||||
|
|
||||||
import { isSubscribePopup } from 'helpers' |
|
||||||
|
|
||||||
import { Name as Names } from 'features/Name' |
|
||||||
import { T9n } from 'features/T9n' |
|
||||||
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store' |
|
||||||
import { CloseButton, HeaderActions } from 'features/PopupComponents' |
|
||||||
import { |
|
||||||
Body, |
|
||||||
Button, |
|
||||||
Footer, |
|
||||||
Header, |
|
||||||
HeaderTitle, |
|
||||||
Wrapper, |
|
||||||
} from 'features/BuyMatchPopup/styled' |
|
||||||
|
|
||||||
import { MatchPackage, SubscriptionType } from '../../types' |
|
||||||
|
|
||||||
import { |
|
||||||
Description, |
|
||||||
InfoWrapper, |
|
||||||
Name, |
|
||||||
Pass, |
|
||||||
} from '../PackagesList/styled' |
|
||||||
|
|
||||||
import { |
|
||||||
Price, |
|
||||||
ChooseSub, |
|
||||||
ChooseSubItem, |
|
||||||
ChooseSubList, |
|
||||||
} from './styled' |
|
||||||
|
|
||||||
export const SelectSubscriptionStep = () => { |
|
||||||
const { |
|
||||||
close, |
|
||||||
match, |
|
||||||
matchSubscriptions, |
|
||||||
onNext, |
|
||||||
onSubscriptionSelect, |
|
||||||
selectedSubscription, |
|
||||||
} = useBuyMatchPopupStore() |
|
||||||
|
|
||||||
const getPackagesCurrency = useCallback(( |
|
||||||
packages: Record<SubscriptionType, Array<MatchPackage>>, |
|
||||||
) => { |
|
||||||
const packageWithValue = Object.entries(packages).find(([key, value]) => value.length)?.[1][0] |
|
||||||
return packageWithValue ? packageWithValue.currency : 'RUB' |
|
||||||
}, []) |
|
||||||
|
|
||||||
if (!match || !matchSubscriptions) return null |
|
||||||
|
|
||||||
return ( |
|
||||||
<Wrapper> |
|
||||||
<Header> |
|
||||||
{!isSubscribePopup() |
|
||||||
&& ( |
|
||||||
<HeaderTitle> |
|
||||||
<Names nameObj={match.team1} /> |
|
||||||
{` ${MDASH} `} |
|
||||||
<Names nameObj={match.team2} /> |
|
||||||
<ChooseSub> |
|
||||||
<T9n t='choose_subscription' /> |
|
||||||
</ChooseSub> |
|
||||||
</HeaderTitle> |
|
||||||
)} |
|
||||||
<HeaderActions position='right'> |
|
||||||
<CloseButton onClick={close} /> |
|
||||||
</HeaderActions> |
|
||||||
</Header> |
|
||||||
<Body marginTop={15}> |
|
||||||
<ChooseSubList> |
|
||||||
{map(matchSubscriptions, (subscription) => ( |
|
||||||
<ChooseSubItem |
|
||||||
key={subscription.id} |
|
||||||
onClick={() => onSubscriptionSelect(subscription)} |
|
||||||
active={subscription === selectedSubscription} |
|
||||||
> |
|
||||||
<InfoWrapper> |
|
||||||
<Pass> |
|
||||||
<T9n t={subscription.lexic} /> |
|
||||||
</Pass> |
|
||||||
<Name> |
|
||||||
<T9n t={subscription.lexic2} /> |
|
||||||
</Name> |
|
||||||
<Description> |
|
||||||
<T9n t={subscription.lexic3} /> |
|
||||||
</Description> |
|
||||||
</InfoWrapper> |
|
||||||
|
|
||||||
<Price |
|
||||||
amount={subscription.min_price || 0} |
|
||||||
currency={getPackagesCurrency(subscription.packages)} |
|
||||||
isFrom={Boolean(subscription.min_price)} |
|
||||||
/> |
|
||||||
</ChooseSubItem> |
|
||||||
))} |
|
||||||
</ChooseSubList> |
|
||||||
</Body> |
|
||||||
<Footer> |
|
||||||
<Button |
|
||||||
disabled={!selectedSubscription} |
|
||||||
onClick={onNext} |
|
||||||
id='purchase_next' |
|
||||||
> |
|
||||||
<T9n t='next_choose' /> |
|
||||||
</Button> |
|
||||||
</Footer> |
|
||||||
</Wrapper> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,33 +0,0 @@ |
|||||||
import styled from 'styled-components/macro' |
|
||||||
import { Price as BasePrice } from 'features/Price' |
|
||||||
import { PriceAmount, PriceDetails } from 'features/Price/styled' |
|
||||||
|
|
||||||
import { |
|
||||||
Item, |
|
||||||
List, |
|
||||||
} from '../PackagesList/styled' |
|
||||||
|
|
||||||
export const Price = styled(BasePrice)` |
|
||||||
${PriceAmount} { |
|
||||||
font-size: 24px; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
${PriceDetails} { |
|
||||||
padding-top: 5px; |
|
||||||
font-size: 12px; |
|
||||||
} |
|
||||||
` |
|
||||||
|
|
||||||
export const ChooseSub = styled.div` |
|
||||||
font-weight: 600; |
|
||||||
font-size: 16px; |
|
||||||
margin: 35px 0 17px; |
|
||||||
` |
|
||||||
|
|
||||||
export const ChooseSubItem = styled(Item)` |
|
||||||
min-height: auto; |
|
||||||
` |
|
||||||
|
|
||||||
export const ChooseSubList = styled(List)` |
|
||||||
height: auto; |
|
||||||
` |
|
||||||
@ -0,0 +1,52 @@ |
|||||||
|
import type { MatchPackage } from 'features/BuyMatchPopup/types' |
||||||
|
import { SubscriptionType } from 'features/BuyMatchPopup/types' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { Price } from 'features/Price' |
||||||
|
|
||||||
|
import { usePackage } from '../RegularPackage/usePackage' |
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
Description, |
||||||
|
PriceBlock, |
||||||
|
PriceTextWrapper, |
||||||
|
PriceTextTop, |
||||||
|
PriceTextBottom, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
matchPackage: MatchPackage, |
||||||
|
} |
||||||
|
|
||||||
|
export const SinglePackage = ({ matchPackage }: Props) => { |
||||||
|
const { |
||||||
|
firstDescription, |
||||||
|
priceTextBottomLexic, |
||||||
|
priceTextTopLexic, |
||||||
|
} = usePackage(matchPackage) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Wrapper> |
||||||
|
<Description> |
||||||
|
<T9n t={matchPackage.originalObject.sub.lexic1} /> |
||||||
|
</Description> |
||||||
|
<Description> |
||||||
|
{firstDescription} |
||||||
|
</Description> |
||||||
|
<Description> |
||||||
|
<T9n t={matchPackage.originalObject.sub.lexic3} /> |
||||||
|
</Description> |
||||||
|
<PriceBlock> |
||||||
|
<PriceTextWrapper> |
||||||
|
<PriceTextTop t={priceTextTopLexic} /> |
||||||
|
<PriceTextBottom t={priceTextBottomLexic} /> |
||||||
|
</PriceTextWrapper> |
||||||
|
<Price |
||||||
|
amount={matchPackage.originalObject.price} |
||||||
|
currency={matchPackage.currency} |
||||||
|
perPeriod={`per_${SubscriptionType.Month}`} |
||||||
|
/> |
||||||
|
</PriceBlock> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,100 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { |
||||||
|
PriceAmount, |
||||||
|
PriceDetails, |
||||||
|
Period, |
||||||
|
Divider, |
||||||
|
Currency, |
||||||
|
PerPeriod, |
||||||
|
} from 'features/Price/styled' |
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
|
||||||
|
export const Description = styled.p` |
||||||
|
margin-bottom: 20px; |
||||||
|
line-height: 20px; |
||||||
|
font-size: 18px; |
||||||
|
font-weight: 400; |
||||||
|
|
||||||
|
:empty, :has(span:empty) { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
:last-of-type { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
:first-child { |
||||||
|
font-size: 20px; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceBlock = styled.div` |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
position: relative; |
||||||
|
margin-top: 60px; |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceTextWrapper = styled.div` |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
white-space: initial; |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceTextTop = styled(T9n)` |
||||||
|
margin-bottom: 2px; |
||||||
|
font-weight: 600; |
||||||
|
font-style: italic; |
||||||
|
font-size: 16px; |
||||||
|
letter-spacing: 0.03em; |
||||||
|
` |
||||||
|
|
||||||
|
export const PriceTextBottom = styled(T9n)` |
||||||
|
font-weight: 400; |
||||||
|
font-size: 16px; |
||||||
|
font-style: italic; |
||||||
|
letter-spacing: 0.03em; |
||||||
|
` |
||||||
|
|
||||||
|
export const Wrapper = styled.div` |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
height: calc(100% - 100px); |
||||||
|
|
||||||
|
${PriceDetails} { |
||||||
|
font-size: 50px; |
||||||
|
} |
||||||
|
|
||||||
|
${PriceAmount} { |
||||||
|
font-size: 50px; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
${Divider} { |
||||||
|
width: 5px; |
||||||
|
height: 45px; |
||||||
|
margin-right: 3px; |
||||||
|
translate: 0 7px; |
||||||
|
} |
||||||
|
|
||||||
|
${Currency} { |
||||||
|
margin-right: 10px; |
||||||
|
translate: none; |
||||||
|
font-size: 50px; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
${Period} { |
||||||
|
font-size: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
${PerPeriod} { |
||||||
|
display: inline-block; |
||||||
|
translate: 0 4px; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
@ -1,31 +0,0 @@ |
|||||||
import omitBy from 'lodash/omitBy' |
|
||||||
import isEmpty from 'lodash/isEmpty' |
|
||||||
import map from 'lodash/map' |
|
||||||
|
|
||||||
import type { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types' |
|
||||||
|
|
||||||
type PaymentTab = { |
|
||||||
tabLexic: string, |
|
||||||
type: SubscriptionType, |
|
||||||
} |
|
||||||
|
|
||||||
const getTabLexic = (type: string) => { |
|
||||||
switch (type) { |
|
||||||
case 'month': |
|
||||||
return 'for_month' |
|
||||||
case 'year': |
|
||||||
return 'for_year' |
|
||||||
case 'pay_per_view': |
|
||||||
return 'for_view' |
|
||||||
default: |
|
||||||
return '' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const getCorrectPaymentTabs = (matchSubscriptions: MatchSubscription) => { |
|
||||||
const matchSubscriptionsWithValues = omitBy(matchSubscriptions.packages, isEmpty) |
|
||||||
return map(matchSubscriptionsWithValues, (matchSubscription, key) => ({ |
|
||||||
tabLexic: getTabLexic(key), |
|
||||||
type: key, |
|
||||||
})) as Array<PaymentTab> |
|
||||||
} |
|
||||||
@ -1,115 +0,0 @@ |
|||||||
import { useMemo } from 'react' |
|
||||||
|
|
||||||
import map from 'lodash/map' |
|
||||||
import size from 'lodash/size' |
|
||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
|
|
||||||
import { isMobileDevice } from 'config/userAgent' |
|
||||||
|
|
||||||
import type { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types' |
|
||||||
import { T9n } from 'features/T9n' |
|
||||||
|
|
||||||
import { getCorrectPaymentTabs } from './helpers' |
|
||||||
|
|
||||||
type ListProps = { |
|
||||||
countSubscriptions: number, |
|
||||||
} |
|
||||||
const List = styled.ul<ListProps>` |
|
||||||
display: flex; |
|
||||||
min-width: 395px; |
|
||||||
justify-content: ${({ countSubscriptions }) => (countSubscriptions === 1 |
|
||||||
? 'center' |
|
||||||
: 'space-between')}; |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
min-width: 90%; |
|
||||||
width: 90%; |
|
||||||
position: relative; |
|
||||||
justify-content: center; |
|
||||||
@media screen and (orientation: landscape){ |
|
||||||
margin-top: -5px; |
|
||||||
width: 50%; |
|
||||||
min-width: 50%; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
type ItemProps = { |
|
||||||
active?: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
const Item = styled.li<ItemProps>` |
|
||||||
position: relative; |
|
||||||
font-weight: 600; |
|
||||||
font-size: 16px; |
|
||||||
line-height: 47px; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
color: rgba(255, 255, 255, 0.5); |
|
||||||
cursor: pointer; |
|
||||||
transition: color 0.3s; |
|
||||||
|
|
||||||
::after { |
|
||||||
transition: background-color 0.3s; |
|
||||||
position: absolute; |
|
||||||
content: ''; |
|
||||||
bottom: 0px; |
|
||||||
width: 130px; |
|
||||||
height: 3px; |
|
||||||
} |
|
||||||
|
|
||||||
${({ active }) => ( |
|
||||||
active |
|
||||||
? css` |
|
||||||
color: #fff; |
|
||||||
::after { |
|
||||||
background-color: #fff; |
|
||||||
} |
|
||||||
` |
|
||||||
: '' |
|
||||||
)} |
|
||||||
${isMobileDevice |
|
||||||
? css` |
|
||||||
font-size: 10px; |
|
||||||
width: 33%; |
|
||||||
white-space: nowrap; |
|
||||||
::after { |
|
||||||
height: 2px; |
|
||||||
} |
|
||||||
` |
|
||||||
: ''}; |
|
||||||
` |
|
||||||
|
|
||||||
type Props = { |
|
||||||
className?: string, |
|
||||||
onPeriodSelect: (period: SubscriptionType) => void, |
|
||||||
selectedPeriod: SubscriptionType, |
|
||||||
selectedSubscription: MatchSubscription, |
|
||||||
} |
|
||||||
|
|
||||||
export const PaymentPeriodTabs = ({ |
|
||||||
className, |
|
||||||
onPeriodSelect, |
|
||||||
selectedPeriod, |
|
||||||
selectedSubscription, |
|
||||||
}: Props) => { |
|
||||||
const matchSubscriptionsWithValues = useMemo(() => ( |
|
||||||
getCorrectPaymentTabs(selectedSubscription) |
|
||||||
), [selectedSubscription]) |
|
||||||
|
|
||||||
return ( |
|
||||||
<List className={className} countSubscriptions={size(matchSubscriptionsWithValues)}> |
|
||||||
{map(matchSubscriptionsWithValues, ({ tabLexic, type }) => ( |
|
||||||
<Item |
|
||||||
key={type} |
|
||||||
active={selectedPeriod === type} |
|
||||||
onClick={() => onPeriodSelect(type)} |
|
||||||
> |
|
||||||
<T9n t={tabLexic} /> |
|
||||||
</Item> |
|
||||||
))} |
|
||||||
</List> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
import { currencySymbols } from 'config' |
|
||||||
|
|
||||||
import { T9n } from 'features/T9n' |
|
||||||
|
|
||||||
import { |
|
||||||
Prefix, |
|
||||||
PriceAmount, |
|
||||||
PriceDetails, |
|
||||||
PriceWrapper, |
|
||||||
Currency, |
|
||||||
Period, |
|
||||||
} from '../../styled' |
|
||||||
|
|
||||||
type Props = { |
|
||||||
amount: number, |
|
||||||
className?: string, |
|
||||||
currency?: string, |
|
||||||
isFrom?: boolean, |
|
||||||
perPeriod?: string | null, |
|
||||||
} |
|
||||||
|
|
||||||
export const BasePrice = ({ |
|
||||||
amount, |
|
||||||
className, |
|
||||||
currency = currencySymbols.RUB, |
|
||||||
isFrom, |
|
||||||
perPeriod, |
|
||||||
}: Props) => ( |
|
||||||
<PriceWrapper className={className}> |
|
||||||
{ |
|
||||||
isFrom |
|
||||||
? ( |
|
||||||
<Prefix> |
|
||||||
<T9n t='from_price' /> |
|
||||||
</Prefix> |
|
||||||
) |
|
||||||
: '' |
|
||||||
} |
|
||||||
<PriceAmount>{amount}</PriceAmount> |
|
||||||
<PriceDetails> |
|
||||||
<Currency>{currency}</Currency> |
|
||||||
{ |
|
||||||
perPeriod && ( |
|
||||||
<Period> |
|
||||||
/ <T9n t={perPeriod} /> |
|
||||||
</Period> |
|
||||||
) |
|
||||||
} |
|
||||||
</PriceDetails> |
|
||||||
</PriceWrapper> |
|
||||||
) |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
import { currencySymbols } from 'config' |
|
||||||
|
|
||||||
import { T9n } from 'features/T9n' |
|
||||||
|
|
||||||
import { |
|
||||||
Prefix, |
|
||||||
PriceDetails, |
|
||||||
PriceWrapper, |
|
||||||
Currency, |
|
||||||
Period, |
|
||||||
} from '../../styled' |
|
||||||
import { PriceAmount } from './styled' |
|
||||||
|
|
||||||
type Props = { |
|
||||||
amount: number, |
|
||||||
className?: string, |
|
||||||
currency?: string, |
|
||||||
isFrom?: boolean, |
|
||||||
perPeriod?: string | null, |
|
||||||
} |
|
||||||
|
|
||||||
export const BrazilianPrice = ({ |
|
||||||
amount, |
|
||||||
className, |
|
||||||
currency = currencySymbols.BRL, |
|
||||||
isFrom, |
|
||||||
perPeriod, |
|
||||||
}: Props) => ( |
|
||||||
<PriceWrapper className={className}> |
|
||||||
{ |
|
||||||
isFrom |
|
||||||
? ( |
|
||||||
<Prefix> |
|
||||||
<T9n t='from_price' /> |
|
||||||
</Prefix> |
|
||||||
) |
|
||||||
: '' |
|
||||||
} |
|
||||||
<PriceDetails> |
|
||||||
<Currency>{currency}</Currency> |
|
||||||
<PriceAmount>{amount.toFixed(2).replace('.', ',')}</PriceAmount> |
|
||||||
{ |
|
||||||
perPeriod && ( |
|
||||||
<Period> |
|
||||||
/ <T9n t={perPeriod} /> |
|
||||||
</Period> |
|
||||||
) |
|
||||||
} |
|
||||||
</PriceDetails> |
|
||||||
</PriceWrapper> |
|
||||||
) |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
import styled from 'styled-components' |
|
||||||
import { PriceAmount as BasePriceAmount } from '../../styled' |
|
||||||
|
|
||||||
export const PriceAmount = styled(BasePriceAmount)` |
|
||||||
margin-right: 4px; |
|
||||||
` |
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue