feat(in-621): subscriptions popup redesign

pull/258/head
Ruslan Khayrullin 3 years ago
parent 18cc2b31eb
commit 05dcd7ff58
  1. 72
      src/components/Carousel/helpers/common.tsx
  2. 274
      src/components/Carousel/helpers/elements.tsx
  3. 4
      src/components/Carousel/helpers/index.tsx
  4. 12
      src/components/Carousel/helpers/mappers.tsx
  5. 44
      src/components/Carousel/helpers/math.tsx
  6. 160
      src/components/Carousel/hooks.tsx
  7. 86
      src/components/Carousel/index.tsx
  8. 25
      src/components/Carousel/styled.tsx
  9. 73
      src/components/Carousel/types.tsx
  10. 1
      src/config/index.tsx
  11. 15
      src/config/lexics/indexLexics.tsx
  12. 1
      src/config/pages.tsx
  13. 18
      src/features/App/AuthenticatedApp.tsx
  14. 2
      src/features/BuyMatchPopup/components/CardStep/index.tsx
  15. 13
      src/features/BuyMatchPopup/components/ErrorStep/index.tsx
  16. 94
      src/features/BuyMatchPopup/components/PackageMobile/index.tsx
  17. 125
      src/features/BuyMatchPopup/components/PackageMobile/styled.tsx
  18. 136
      src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx
  19. 22
      src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx
  20. 206
      src/features/BuyMatchPopup/components/Packages/index.tsx
  21. 65
      src/features/BuyMatchPopup/components/Packages/styled.tsx
  22. 84
      src/features/BuyMatchPopup/components/PackagesList/index.tsx
  23. 230
      src/features/BuyMatchPopup/components/PackagesList/styled.tsx
  24. 74
      src/features/BuyMatchPopup/components/RegularPackage/index.tsx
  25. 140
      src/features/BuyMatchPopup/components/RegularPackage/styled.tsx
  26. 70
      src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx
  27. 115
      src/features/BuyMatchPopup/components/SelectSubscription/index.tsx
  28. 33
      src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx
  29. 58
      src/features/BuyMatchPopup/components/SelectedCard/index.tsx
  30. 52
      src/features/BuyMatchPopup/components/SinglePackage/index.tsx
  31. 100
      src/features/BuyMatchPopup/components/SinglePackage/styled.tsx
  32. 22
      src/features/BuyMatchPopup/index.tsx
  33. 47
      src/features/BuyMatchPopup/store/helpers.tsx
  34. 61
      src/features/BuyMatchPopup/store/hooks/index.tsx
  35. 9
      src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx
  36. 18
      src/features/BuyMatchPopup/styled.tsx
  37. 5
      src/features/BuyMatchPopup/types.tsx
  38. 7
      src/features/HomePage/index.tsx
  39. 10
      src/features/MatchCard/hooks.tsx
  40. 4
      src/features/MatchPage/components/SubscriptionGuard/index.tsx
  41. 2
      src/features/MatchPage/index.tsx
  42. 31
      src/features/PaymentPeriodTabs/helpers.tsx
  43. 115
      src/features/PaymentPeriodTabs/index.tsx
  44. 6
      src/features/PlayerPage/index.tsx
  45. 51
      src/features/Price/components/BasePrice/index.tsx
  46. 51
      src/features/Price/components/BrazilianPrice/index.tsx
  47. 6
      src/features/Price/components/BrazilianPrice/styled.tsx
  48. 61
      src/features/Price/index.tsx
  49. 56
      src/features/Price/styled.tsx
  50. 6
      src/features/TeamPage/index.tsx
  51. 6
      src/features/TournamentPage/index.tsx
  52. 62
      src/features/UserAccount/components/UserSubscriptionsList/index.tsx
  53. 159
      src/features/UserAccount/components/UserSubscriptionsList/styled.tsx
  54. 1
      src/helpers/index.tsx
  55. 16
      src/pages/SubscriptionsPage/index.tsx
  56. 31
      src/pages/SubscriptionsPage/styled.tsx
  57. 3
      src/requests/getSubscriptions.tsx

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

@ -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,
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,7 +1,6 @@
import { PAGES } from 'config'
import { isMobileDevice } from 'config/userAgent'
import { PAGES, isMobileDevice } from 'config'
import { usePageLogger } from 'hooks/usePageLogger'
import { usePageLogger } from 'hooks'
import { ConfirmPopup } from 'features/AuthServiceApp/components/ConfirmPopup'
import { Matches } from 'features/Matches'
@ -14,6 +13,7 @@ import {
Content,
} from 'features/PageLayout'
import { UserFavorites } from 'features/UserFavorites'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { HEADER_MOBILE_ADS } from 'components/Ads/types'
import { HeaderAds } from 'components/Ads'
@ -74,6 +74,7 @@ const Home = () => {
const HomePage = () => (
<HeaderFiltersStore>
<Home />
{!isMobileDevice && <BuyMatchPopup />}
</HeaderFiltersStore>
)

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

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

@ -14,6 +14,7 @@ import {
PageWrapper,
Main,
} from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { FavoritesActions } from 'requests'
@ -152,6 +153,7 @@ const MatchPage = () => (
<MatchPageStore>
<TourProvider>
<MatchPageComponent />
{!isMobileDevice && <BuyMatchPopup />}
</TourProvider>
</MatchPageStore>
)

@ -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,4 +1,6 @@
import { usePageLogger } from 'hooks/usePageLogger'
import { isMobileDevice } from 'config'
import { usePageLogger } from 'hooks'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
@ -9,6 +11,7 @@ import {
Main,
Content,
} from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { usePlayerPage } from './hooks'
@ -31,6 +34,7 @@ const PlayerPage = () => {
<Matches fetch={fetchMatches} />
</Content>
</Main>
{!isMobileDevice && <BuyMatchPopup />}
</PageWrapper>
)
}

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

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

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

@ -1,48 +1,39 @@
import { currencySymbols } from 'config'
import { useMemo } from 'react'
import { BasePrice } from './components/BasePrice'
import { BrazilianPrice } from './components/BrazilianPrice'
import {
PriceAmount,
PriceDetails,
PriceWrapper,
Currency,
Period,
Divider,
PerPeriod,
} from './styled'
export type PriceProps = {
type Props = {
amount: number,
className?: string,
currency?: string,
isFrom?: boolean,
perPeriod?: string | null,
}
export const Price: React.FC<PriceProps> = ({
export const Price = ({
amount,
className,
currency,
isFrom,
currency = currencySymbols.RUB,
perPeriod,
}) => {
const priceContent = useMemo(() => {
switch (currency) {
case currencySymbols.BRL:
return (
<BrazilianPrice
amount={amount}
className={className}
currency={currency}
isFrom={isFrom}
perPeriod={perPeriod}
/>
}: Props) => (
<PriceWrapper className={className}>
<PriceAmount>{amount}</PriceAmount>
<PriceDetails>
<Currency>{currency}</Currency>
{
perPeriod && (
<Period>
<Divider /><PerPeriod t={perPeriod} />
</Period>
)
default:
return (
<BasePrice
amount={amount}
className={className}
currency={currency}
isFrom={isFrom}
perPeriod={perPeriod}
/>
)
}
}, [amount, className, currency, isFrom, perPeriod])
return priceContent
}
}
</PriceDetails>
</PriceWrapper>
)

@ -1,62 +1,48 @@
import styled, { css } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { devices } from 'config/devices'
import { isMobileDevice } from 'config'
import { T9n } from 'features/T9n'
export const PriceWrapper = styled.div`
display: flex;
align-items: flex-start;
@media ${devices.tablet} {
justify-content: center;
}
${isMobileDevice
? css`
min-width: 80px;
`
: ''};
`
export const PriceAmount = styled.span`
font-style: normal;
font-weight: 600;
font-size: 48px;
line-height: 40px;
font-weight: 400;
font-size: 32px;
color: ${({ theme: { colors } }) => colors.white};
@media ${devices.tablet} {
font-size: 36px;
}
`
export const PriceDetails = styled.span`
display: flex;
margin-left: 5px;
font-style: normal;
font-weight: normal;
font-size: 18px;
line-height: 21px;
font-size: 30px;
color: ${({ theme: { colors } }) => colors.white};
text-transform: uppercase;
@media ${devices.tablet} {
font-size: 11px;
}
`
export const Currency = styled.span`
text-transform: uppercase;
margin-right: 4px;
font-size: ${isMobileDevice ? 8 : 10}px;
`
export const Period = styled.span`
text-transform: uppercase;
font-style: italic;
font-weight: 400;
font-size: ${isMobileDevice ? 8 : 10}px;
`
export const Prefix = styled.span`
padding-top: 5px;
text-transform: lowercase;
line-height: 40px;
font-size: 12px;
font-weight: normal;
margin-right: 5px;
export const Divider = styled.div`
display: inline-block;
width: 0.5px;
height: 8px;
margin: 0 2px -1px 4px;
transform: skew(-20deg, 0);
background-color: ${({ theme: { colors } }) => colors.white};
`
export const PerPeriod = styled(T9n)``

@ -1,4 +1,6 @@
import { usePageLogger } from 'hooks/usePageLogger'
import { isMobileDevice } from 'config'
import { usePageLogger } from 'hooks'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
@ -9,6 +11,7 @@ import {
Main,
Content,
} from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { useTeamPage } from './hooks'
@ -35,6 +38,7 @@ const TeamPage = () => {
<Matches fetch={fetchMatches} />
</Content>
</Main>
{!isMobileDevice && <BuyMatchPopup />}
</PageWrapper>
)
}

@ -1,4 +1,6 @@
import { usePageLogger } from 'hooks/usePageLogger'
import { isMobileDevice } from 'config'
import { usePageLogger } from 'hooks'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
@ -9,6 +11,7 @@ import {
Main,
Content,
} from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { useTournamentPage } from './hooks'
@ -39,6 +42,7 @@ const TournamentPage = () => {
</Content>
</Main>
{user && (tournamentId === 131 || tournamentId === 2032) && <FavouriteTeamPopup />}
{!isMobileDevice && <BuyMatchPopup />}
</PageWrapper>
)
}

@ -1,62 +0,0 @@
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import type { MatchSubscriptions } from '../PageSubscriptions'
import { InlineButton } from '../../styled'
import {
Wrapper,
SportName,
List,
Item,
Subscription,
InfoWrapper,
Header,
Description,
Price,
SubscriptionEnd,
} from './styled'
type Props = {
list: MatchSubscriptions,
sport: number,
}
export const UserSubscriptionsList = ({ list, sport }: Props) => {
if (isEmpty(list)) return null
return (
<Wrapper>
<SportName sport={sport} />
<List>
{
map(list, ({
description,
header,
isActive,
price,
subscription_id,
type,
}) => (
<Item key={subscription_id}>
<Subscription>
<InfoWrapper>
<Header>
{header}
</Header>
<Description>
{description}
</Description>
</InfoWrapper>
<Price amount={price} perPeriod={`per_${type}`} />
<InlineButton color={isActive ? '#eb5757' : '#294FC3'}>
{isActive ? 'Удалить' : 'Восстановить'}
</InlineButton>
</Subscription>
<SubscriptionEnd>Следующее списание 31.02.2020</SubscriptionEnd>
</Item>
))
}
</List>
</Wrapper>
)
}

@ -1,159 +0,0 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { SportName as SportNameBase } from 'features/Common/SportName'
import { Price as BasePrice } from 'features/Price'
import { PriceAmount, PriceDetails } from 'features/Price/styled'
import { InlineButton } from '../../styled'
export const Wrapper = styled.div`
:not(:first-child) {
margin-top: 24px;
}
`
export const SportName = styled(SportNameBase)`
font-style: normal;
font-weight: 500;
font-size: 18px;
line-height: 22px;
color: rgba(255, 255, 255, 0.6);
${isMobileDevice
? css`
font-size: 14px;
`
: ''}
`
export const List = styled.ul`
margin-top: 6px;
`
export const Item = styled.li`
display: flex;
align-items: center;
${isMobileDevice
? css`
display: block;
`
: ''}
`
export const Price = styled(BasePrice)`
margin-right: 20px;
${PriceAmount} {
font-size: 24px;
line-height: 24px;
font-weight: normal;
${isMobileDevice
? css`
font-size: 14px;
`
: ''}
}
${PriceDetails} {
font-weight: 500;
font-size: 12px;
line-height: 18px;
${isMobileDevice
? css`
font-size: 7px;
`
: ''}
}
`
export const Subscription = styled.div.attrs(
() => ({
tabIndex: 0,
}),
)`
position: relative;
width: 800px;
height: 70px;
display: flex;
margin: 5px 0;
align-items: center;
justify-content: space-between;
background-color: #3F3F3F;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
border-radius: 2px;
overflow: hidden;
${InlineButton} {
width: 133px;
}
:focus-within, :hover {
${InlineButton} {
transform: translateX(0);
}
}
${isMobileDevice
? css`
height: 86px;
width: 100%;
`
: ''}
`
export const InfoWrapper = styled.div`
width: 68%;
display: flex;
padding: 14px 0 14px 20px;
flex-direction: column;
color: #fff;
${isMobileDevice
? css`
padding: 10px 0 10px 15px;
`
: ''}
`
export const Header = styled.span`
font-style: normal;
font-weight: 600;
font-size: 16px;
line-height: 23px;
text-transform: uppercase;
${isMobileDevice
? css`
font-size: 14px;
line-height: 20px;
`
: ''}
`
export const Description = styled.p`
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 20px;
text-transform: capitalize;
${isMobileDevice
? css`
font-size: 10px;
line-height: 12px;
`
: ''}
`
export const SubscriptionEnd = styled.span`
margin-left: 24px;
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
color: rgba(255, 255, 255, 0.3);
${isMobileDevice
? css`
font-size: 10px;
margin-left: 0;
`
: ''}
`

@ -16,3 +16,4 @@ export * from './isMatchPage'
export * from './languageUrlParam'
export * from './bodyScrollLock'
export * from './getLocalStorage'
export * from './checkPage'

@ -0,0 +1,16 @@
import { ProfileHeader } from 'features/ProfileHeader'
import { Main } from 'features/PageLayout'
import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { Wrapper } from './styled'
const SubscriptionsPage = () => (
<Wrapper>
<ProfileHeader />
<Main>
<BuyMatchPopup />
</Main>
</Wrapper>
)
export default SubscriptionsPage

@ -0,0 +1,31 @@
import styled from 'styled-components/macro'
import { PageWrapper } from 'features/PageLayout'
import { Form } from 'features/Search/styled'
import { MenuList } from 'features/Menu/styled'
import { HeaderStyled } from 'features/ProfileHeader/styled'
import { Logo } from 'features/Logo'
import { Body, Wrapper as ContentWrapper } from 'features/BuyMatchPopup/styled'
export const Wrapper = styled(PageWrapper)`
${Form}, ${MenuList} {
display: none;
}
${HeaderStyled} {
height: 60px;
}
${Logo} {
top: 7.5px;
}
${Body} {
margin-top: 18px;
padding: 0;
}
${ContentWrapper} {
padding: 20px 16px;
}
`

@ -50,6 +50,9 @@ type Sub = {
active_to: string,
currency_id: number,
id: number,
lexic1: number,
lexic2: number | null,
lexic3: number,
match_id?: number,
option: number,
price: number,

Loading…
Cancel
Save