Compare commits
No commits in common. 'develop' and 'fqtv.insports.tv' have entirely different histories.
develop
...
fqtv.inspo
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 802 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
@ -1,12 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<browserconfig> |
|
||||||
<msapplication> |
|
||||||
<tile> |
|
||||||
<square70x70logo src="/mstile-70x70.png"/> |
|
||||||
<square144x144logo src="/mstile-144x144.png"/> |
|
||||||
<square150x150logo src="/mstile-150x150.png"/> |
|
||||||
<square310x310logo src="/mstile-310x310.png"/> |
|
||||||
<TileColor>#da532c</TileColor> |
|
||||||
</tile> |
|
||||||
</msapplication> |
|
||||||
</browserconfig> |
|
||||||
|
Before Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 15 KiB |
@ -1,19 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "", |
|
||||||
"short_name": "", |
|
||||||
"icons": [ |
|
||||||
{ |
|
||||||
"src": "/android-chrome-192x192.png", |
|
||||||
"sizes": "192x192", |
|
||||||
"type": "image/png" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"src": "/android-chrome-512x512.png", |
|
||||||
"sizes": "512x512", |
|
||||||
"type": "image/png" |
|
||||||
} |
|
||||||
], |
|
||||||
"theme_color": "#ffffff", |
|
||||||
"background_color": "#ffffff", |
|
||||||
"display": "standalone" |
|
||||||
} |
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 598 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
@ -1,28 +1,15 @@ |
|||||||
<!DOCTYPE html> |
<!DOCTYPE html> |
||||||
<html lang="en"> |
<html lang="en"> |
||||||
|
|
||||||
<head> |
<head> |
||||||
<title></title> |
<title></title> |
||||||
</head> |
</head> |
||||||
|
|
||||||
<body> |
<body> |
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.11.5/oidc-client.min.js" |
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.11.5/oidc-client.min.js" integrity="sha512-pGtU1n/6GJ8fu6bjYVGIOT9Dphaw5IWPwVlqkpvVgqBxFkvdNbytUh0H8AP15NYF777P4D3XEeA/uDWFCpSQ1g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
||||||
integrity="sha512-pGtU1n/6GJ8fu6bjYVGIOT9Dphaw5IWPwVlqkpvVgqBxFkvdNbytUh0H8AP15NYF777P4D3XEeA/uDWFCpSQ1g==" |
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
|
||||||
<script> |
<script> |
||||||
new Oidc.UserManager().signinSilentCallback() |
new Oidc.UserManager().signinSilentCallback() |
||||||
// обновляем рефреш токен в локалсторадже |
.catch((err) => { |
||||||
// так как safari не дает доступ к кукам |
console.error('OIDC: silent refresh callback error', err); |
||||||
.then(() => { |
}); |
||||||
const refreshToken = localStorage.getItem('refresh_token'); |
|
||||||
if (refreshToken) { |
|
||||||
localStorage.setItem('refresh_token', new URLSearchParams(document.location.search).get('refresh_token')); |
|
||||||
} |
|
||||||
}) |
|
||||||
.catch((err) => { |
|
||||||
console.error('OIDC: silent refresh callback error', err); |
|
||||||
}); |
|
||||||
</script> |
</script> |
||||||
</body> |
</body> |
||||||
|
</html> |
||||||
</html> |
|
||||||
|
|||||||
@ -1,163 +0,0 @@ |
|||||||
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 isMatch = isMatchPage() |
|
||||||
|
|
||||||
const [isOpenAd, setIsOpenAd] = useState(isMatch) |
|
||||||
const [isNeedToShow, setIsNeedToShow] = useState(isMatch) |
|
||||||
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) |
|
||||||
isMatch && 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 (isMatch) { |
|
||||||
const interval = setInterval(() => { |
|
||||||
setIsNeedToShow(true) |
|
||||||
setIsOpenAd(true) |
|
||||||
}, frequency * 1000) |
|
||||||
|
|
||||||
return () => clearInterval(interval) |
|
||||||
} |
|
||||||
|
|
||||||
setIsNeedToShow(isNeedBanner) |
|
||||||
return setIsOpenAd(isNeedBanner) |
|
||||||
}, [ |
|
||||||
frequency, |
|
||||||
isNeedToShow, |
|
||||||
views?.HOME, |
|
||||||
isNeedBanner, |
|
||||||
isMatch, |
|
||||||
]) |
|
||||||
|
|
||||||
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 || (!isMatch && !isNeedBanner)) return |
|
||||||
|
|
||||||
(async () => { |
|
||||||
await updateAdsView({ adv_id: id }) |
|
||||||
})() |
|
||||||
sendBannerClickEvent(EventGA.DISPLAY) |
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [ |
|
||||||
id, |
|
||||||
isNeedToShow, |
|
||||||
isMatch, |
|
||||||
]) |
|
||||||
|
|
||||||
return { |
|
||||||
handleClose, |
|
||||||
isNeedToShow, |
|
||||||
isOpenAd, |
|
||||||
isOpenCloseBtn, |
|
||||||
isVideo, |
|
||||||
sendBannerClickEvent, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,91 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
||||||
}) |
|
||||||
@ -1,109 +0,0 @@ |
|||||||
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`` |
|
||||||
@ -1,79 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,115 +0,0 @@ |
|||||||
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, |
|
||||||
VIEW_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); |
|
||||||
` |
|
||||||
case VIEW_ADS.MOBILE_IN_COLLAPSE_FOOTER: |
|
||||||
case VIEW_ADS.MOBILE_IN_COLLAPSE_HEADER: |
|
||||||
return 'margin-top: 6px' |
|
||||||
default: |
|
||||||
return '' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const MobileAdWrapper = styled.div<Props>` |
|
||||||
position: relative; |
|
||||||
width: 100%; |
|
||||||
z-index: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '101' : '4')}; |
|
||||||
|
|
||||||
${({ 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' |
|
||||||
} |
|
||||||
}} |
|
||||||
` |
|
||||||
@ -1,16 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
@ -1 +0,0 @@ |
|||||||
export * from './isVideo' |
|
||||||
@ -1,2 +0,0 @@ |
|||||||
const regexp = /^https?:\/\/\S+(?:mp4)$/ |
|
||||||
export const checkVideo = (url: string) => regexp.test(url) |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,31 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
||||||
@ -1,16 +0,0 @@ |
|||||||
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; |
|
||||||
`}}
|
|
||||||
` |
|
||||||
@ -1,59 +0,0 @@ |
|||||||
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' |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,274 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
@ -1,4 +0,0 @@ |
|||||||
export * from './common' |
|
||||||
export * from './elements' |
|
||||||
export * from './math' |
|
||||||
export * from './mappers' |
|
||||||
@ -1,12 +0,0 @@ |
|||||||
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 |
|
||||||
}) |
|
||||||
@ -1,44 +0,0 @@ |
|||||||
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)) |
|
||||||
|
|
||||||
@ -1,160 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,86 +0,0 @@ |
|||||||
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> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,25 +0,0 @@ |
|||||||
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; |
|
||||||
` |
|
||||||
@ -1,73 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
@ -1 +0,0 @@ |
|||||||
export const COUNTRY = 'COUNTRY' |
|
||||||
@ -1,7 +1,5 @@ |
|||||||
export const device = navigator.userAgent |
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) |
||||||
|
|
||||||
export const isIOS = /iPad|iPhone|iPod/.test(device) |
export const isAndroid = /Android/.test(navigator.userAgent) |
||||||
|
|
||||||
export const isAndroid = /Android/.test(device) |
export const isMobileDevice = /iPhone|Android/.test(navigator.userAgent) |
||||||
|
|
||||||
export const isMobileDevice = /iPhone|Android/.test(device) |
|
||||||
|
|||||||
@ -1,94 +0,0 @@ |
|||||||
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> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,125 +0,0 @@ |
|||||||
import styled, { css } from 'styled-components/macro' |
|
||||||
|
|
||||||
import { T9n } from 'features/T9n' |
|
||||||
import { ButtonSolid } from 'features/Common' |
|
||||||
import { PriceDetails, PriceAmount } from 'features/Price/styled' |
|
||||||
|
|
||||||
export const Wrapper = styled.div` |
|
||||||
width: 100%; |
|
||||||
border-radius: 2px 2px 0px 0px; |
|
||||||
background-color: #414141; |
|
||||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); |
|
||||||
` |
|
||||||
|
|
||||||
type HeaderProps = { |
|
||||||
isOpen?: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export const Header = styled.div<HeaderProps>` |
|
||||||
position: relative; |
|
||||||
display: flex; |
|
||||||
justify-content: space-between; |
|
||||||
height: 52px; |
|
||||||
align-items: center; |
|
||||||
padding: 0 15px 0 20px; |
|
||||||
|
|
||||||
${PriceAmount} { |
|
||||||
font-size: 24px; |
|
||||||
position: absolute; |
|
||||||
right: 46px; |
|
||||||
top: 50%; |
|
||||||
translate: 0 -50%; |
|
||||||
} |
|
||||||
|
|
||||||
${PriceDetails} { |
|
||||||
position: absolute; |
|
||||||
top: 12px; |
|
||||||
left: calc(100% - 48px); |
|
||||||
} |
|
||||||
|
|
||||||
${({ isOpen }) => (isOpen |
|
||||||
? '' |
|
||||||
: css` |
|
||||||
${PriceDetails}, ${PriceAmount}, ${Title} { |
|
||||||
color: #D9D9D9; |
|
||||||
} |
|
||||||
`)}
|
|
||||||
` |
|
||||||
|
|
||||||
export const Border = styled.div` |
|
||||||
height: 0.66px; |
|
||||||
background: linear-gradient(90deg, transparent 0%, #656565 50%, transparent 100%); |
|
||||||
` |
|
||||||
|
|
||||||
export const Title = styled(T9n)` |
|
||||||
max-width: calc(100% - 112px); |
|
||||||
font-weight: 600; |
|
||||||
font-size: 14px; |
|
||||||
color: ${({ theme }) => theme.colors.white}; |
|
||||||
overflow: hidden; |
|
||||||
` |
|
||||||
|
|
||||||
type ContentProps = { |
|
||||||
isOpen?: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export const Content = styled.div<ContentProps>` |
|
||||||
height: ${({ isOpen }) => (isOpen ? 'auto' : 0)}; |
|
||||||
` |
|
||||||
|
|
||||||
export const Body = styled.div` |
|
||||||
position: relative; |
|
||||||
padding: 8px 24px 30px; |
|
||||||
background-color: #414141; |
|
||||||
` |
|
||||||
|
|
||||||
export const Description = styled.p` |
|
||||||
width: calc(100% - 90px); |
|
||||||
margin-bottom: 15px; |
|
||||||
font-weight: 500; |
|
||||||
font-size: 12px; |
|
||||||
line-height: 16px; |
|
||||||
color: #C7C7C7; |
|
||||||
|
|
||||||
:empty, :has(span:empty) { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
|
|
||||||
:last-of-type { |
|
||||||
margin-bottom: 0; |
|
||||||
} |
|
||||||
` |
|
||||||
|
|
||||||
export const Button = styled(ButtonSolid)` |
|
||||||
width: 100%; |
|
||||||
height: 48px; |
|
||||||
` |
|
||||||
|
|
||||||
export const SubscriptionTypeText = styled(T9n)` |
|
||||||
position: absolute; |
|
||||||
right: 16px; |
|
||||||
bottom: 12px; |
|
||||||
font-size: 12px; |
|
||||||
font-weight: 600; |
|
||||||
color: ${({ theme }) => theme.colors.white}; |
|
||||||
` |
|
||||||
|
|
||||||
export const BestChoice = styled.div` |
|
||||||
position: absolute; |
|
||||||
bottom: 40px; |
|
||||||
right: 0; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: flex-end; |
|
||||||
width: 108px; |
|
||||||
height: 20px; |
|
||||||
padding-right: 16px; |
|
||||||
border-radius: 1px 0px 0px 1px; |
|
||||||
font-weight: 600; |
|
||||||
font-size: 12px; |
|
||||||
font-variant: all-small-caps; |
|
||||||
letter-spacing: 0.03em; |
|
||||||
color: #464646; |
|
||||||
background: linear-gradient(270deg, rgba(253, 253, 254, 0.8) 0%, rgba(253, 253, 254, 0) 97.42%); |
|
||||||
backdrop-filter: blur(3px); |
|
||||||
` |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
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,181 +1,41 @@ |
|||||||
import type { MouseEvent } from 'react' |
import styled from 'styled-components/macro' |
||||||
import { useMemo } from 'react' |
|
||||||
|
|
||||||
import reduce from 'lodash/reduce' |
import { PaymentPeriodTabs } from 'features/PaymentPeriodTabs' |
||||||
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 { RegularPackage } from '../RegularPackage' |
import { PackagesList } from '../PackagesList' |
||||||
import { PackageMobile } from '../PackageMobile' |
|
||||||
import { |
const Wrapper = styled.div` |
||||||
Wrapper, |
width: 100%; |
||||||
List, |
display: flex; |
||||||
PrevButton, |
flex-direction: column; |
||||||
NextButton, |
align-items: center; |
||||||
Arrow, |
` |
||||||
ListItem, |
|
||||||
} from './styled' |
export const Packages = () => { |
||||||
import { SinglePackage } from '../SinglePackage' |
const { |
||||||
|
onPackageSelect, |
||||||
const breakpoints: Breakpoints = { |
onPeriodSelect, |
||||||
1000: { items: 3 }, |
selectedPackage, |
||||||
1280: { items: 4 }, |
selectedPeriod, |
||||||
1400: { items: 5 }, |
selectedSubscription, |
||||||
} |
subscriptions, |
||||||
|
} = useBuyMatchPopupStore() |
||||||
type ButtonProps = { |
|
||||||
disabled?: boolean, |
if (!selectedSubscription) return null |
||||||
onClick: () => void, |
|
||||||
} |
|
||||||
|
|
||||||
const renderPrevButton = ({ disabled, onClick }: ButtonProps) => ( |
|
||||||
<PrevButton |
|
||||||
aria-label='Previous' |
|
||||||
disabled={disabled} |
|
||||||
onClick={onClick} |
|
||||||
> |
|
||||||
<Arrow direction='left' /> |
|
||||||
</PrevButton> |
|
||||||
) |
|
||||||
|
|
||||||
const renderNextButton = ({ disabled, onClick }: ButtonProps) => ( |
|
||||||
<NextButton |
|
||||||
aria-label='Next' |
|
||||||
disabled={disabled} |
|
||||||
onClick={onClick} |
|
||||||
> |
|
||||||
<Arrow direction='right' /> |
|
||||||
</NextButton> |
|
||||||
) |
|
||||||
|
|
||||||
type Props = { |
|
||||||
buttonId?: string, |
|
||||||
buttonLexic?: string, |
|
||||||
onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void, |
|
||||||
} |
|
||||||
|
|
||||||
export const Packages = ({ |
|
||||||
buttonId, |
|
||||||
buttonLexic, |
|
||||||
onButtonClick, |
|
||||||
}: Props) => { |
|
||||||
const { matchPackages } = useBuyMatchPopupStore() |
|
||||||
|
|
||||||
const hasOnlyOneSubscription = matchPackages.length === 1 |
|
||||||
const hasMoreThanFiveSubscriptions = matchPackages.length > 5 |
|
||||||
|
|
||||||
const getSortedPackages = () => { |
|
||||||
let temp = sortBy(matchPackages, 'order') |
|
||||||
const mainPackage = matchPackages.find(({ isMainPackage }) => isMainPackage) |
|
||||||
|
|
||||||
if (mainPackage && matchPackages.length > 1) { |
|
||||||
const index = matchPackages.length >= 2 && matchPackages.length < 4 ? 1 : 2 |
|
||||||
|
|
||||||
temp = without(temp, mainPackage) |
|
||||||
temp.splice( |
|
||||||
index, |
|
||||||
0, |
|
||||||
mainPackage, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return temp |
|
||||||
} |
|
||||||
|
|
||||||
const sortedPackages = getSortedPackages() |
|
||||||
|
|
||||||
const lexicsIds = useMemo( |
|
||||||
() => [...reduce<MatchPackage, Set<number>>( |
|
||||||
matchPackages, |
|
||||||
(acc, { originalObject: { sub } }) => { |
|
||||||
acc.add(sub.lexic1) |
|
||||||
sub.lexic2 && acc.add(sub.lexic2) |
|
||||||
acc.add(sub.lexic3) |
|
||||||
|
|
||||||
return acc |
|
||||||
}, |
|
||||||
new Set(), |
|
||||||
)], |
|
||||||
[matchPackages], |
|
||||||
) |
|
||||||
|
|
||||||
useLexicsConfig(lexicsIds) |
|
||||||
|
|
||||||
if (isMobileDevice) { |
|
||||||
return ( |
|
||||||
<Wrapper> |
|
||||||
<List> |
|
||||||
{sortedPackages.map((matchPackage) => ( |
|
||||||
<ListItem key={matchPackage.id}> |
|
||||||
<PackageMobile |
|
||||||
matchPackage={matchPackage} |
|
||||||
onButtonClick={onButtonClick} |
|
||||||
buttonId={buttonId} |
|
||||||
buttonLexic={buttonLexic} |
|
||||||
/> |
|
||||||
</ListItem> |
|
||||||
))} |
|
||||||
</List> |
|
||||||
</Wrapper> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
if (hasOnlyOneSubscription) { |
|
||||||
return ( |
|
||||||
<SinglePackage matchPackage={matchPackages[0]} /> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
const getWrapperWidth = () => { |
|
||||||
switch (true) { |
|
||||||
case !hasMoreThanFiveSubscriptions: |
|
||||||
return undefined |
|
||||||
|
|
||||||
case window.innerWidth < 1500: |
|
||||||
return window.innerWidth - 160 |
|
||||||
|
|
||||||
default: |
|
||||||
return 1380 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const wrapperWidth = getWrapperWidth() |
|
||||||
|
|
||||||
return ( |
return ( |
||||||
<Wrapper width={wrapperWidth}> |
<Wrapper> |
||||||
{hasMoreThanFiveSubscriptions |
<PaymentPeriodTabs |
||||||
? ( |
onPeriodSelect={onPeriodSelect} |
||||||
<Carousel |
selectedPeriod={selectedPeriod} |
||||||
animationDuration={400} |
selectedSubscription={selectedSubscription} |
||||||
infinite |
/> |
||||||
useKeyboardNavigation |
<PackagesList |
||||||
breakpoints={breakpoints} |
packages={subscriptions} |
||||||
spaceBetween={20} |
selectedPackage={selectedPackage} |
||||||
renderPrevButton={renderPrevButton} |
onSelect={onPackageSelect} |
||||||
renderNextButton={renderNextButton} |
/> |
||||||
> |
|
||||||
{sortedPackages.map((matchPackage) => ( |
|
||||||
<RegularPackage matchPackage={matchPackage} /> |
|
||||||
))} |
|
||||||
</Carousel> |
|
||||||
) |
|
||||||
: ( |
|
||||||
<List> |
|
||||||
{sortedPackages.map((matchPackage) => ( |
|
||||||
<ListItem key={matchPackage.id}> |
|
||||||
<RegularPackage matchPackage={matchPackage} /> |
|
||||||
</ListItem> |
|
||||||
))} |
|
||||||
</List> |
|
||||||
)} |
|
||||||
</Wrapper> |
</Wrapper> |
||||||
) |
) |
||||||
} |
} |
||||||
|
|||||||
@ -1,65 +0,0 @@ |
|||||||
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; |
|
||||||
` |
|
||||||
@ -0,0 +1,84 @@ |
|||||||
|
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> |
||||||
|
) |
||||||
@ -0,0 +1,230 @@ |
|||||||
|
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; |
||||||
|
` |
||||||
@ -1,74 +0,0 @@ |
|||||||
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> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -1,140 +0,0 @@ |
|||||||
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; |
|
||||||
} |
|
||||||
` |
|
||||||