diff --git a/src/components/Carousel/helpers/common.tsx b/src/components/Carousel/helpers/common.tsx new file mode 100644 index 00000000..fa4913ad --- /dev/null +++ b/src/components/Carousel/helpers/common.tsx @@ -0,0 +1,72 @@ +import type { Props, State } from '../types' +import { + createCustomWidthTransformationSet, + 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, + customWidth = false, + infinite = 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 = customWidth + ? createCustomWidthTransformationSet({ + el, + infinite, + listWidth, + props, + }).coords + : createDefaultTransformationSet({ + children: clones, + infinite, + itemsInSlide, + listWidth, + props, + }).coords + + const translate3d = getTranslate3dProperty(activeIndex, { + customWidth, + infinite, + itemsInSlide, + itemsOffset, + transformationSet, + }) + + return { + activeIndex, + animationDuration, + clones, + customWidth, + infinite, + itemsCount, + itemsInSlide, + itemsOffset, + listWidth, + transformationSet, + transition, + translate3d, + } +} diff --git a/src/components/Carousel/helpers/elements.tsx b/src/components/Carousel/helpers/elements.tsx new file mode 100644 index 00000000..02540738 --- /dev/null +++ b/src/components/Carousel/helpers/elements.tsx @@ -0,0 +1,282 @@ +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 CreateCustomWidthTransformationSetArgs = { + el: HTMLElement | null, + infinite: boolean, + listWidth: number, + props: Props, +} + +export const createCustomWidthTransformationSet = ({ + el, + infinite, + listWidth, + props, +}: CreateCustomWidthTransformationSetArgs) => { + let content = 0 + let partial = true + let coords: Array = [] + + const { spaceBetween = 0 } = props + + const children: Array = Array.from(el?.children || []) + + coords = children.reduce>(( + acc, + child, + i, + ) => { + let position = 0 + const previewsChildCursor = i - 1 + const previewsChild = acc[previewsChildCursor] + const { width = 0 } = getElementDimensions(child?.firstChild as HTMLElement) + + content += width + spaceBetween + partial = listWidth >= content + + if (previewsChild) { + position = previewsChildCursor === 0 + ? previewsChild.width + spaceBetween + : previewsChild.width + previewsChild.position + spaceBetween + } + + acc.push({ position, width }) + return acc + }, []) + + if (!infinite) { + if (partial) { + coords = mapPartialCoords(coords) + } else { + const position = content - listWidth + coords = mapPositionCoords(coords, position) + } + } + + return { + content, + coords, + partial, + } +} + +type CreateDefaultTransformationSetArgs = { + children: Array, + infinite: boolean, + itemsInSlide: number, + listWidth: number, + props: Props, +} + +export const createDefaultTransformationSet = ({ + children, + infinite, + itemsInSlide, + listWidth, + props, +}: CreateDefaultTransformationSetArgs): Transformations => { + let content = 0 + let partial = true + let coords: Array = [] + + const { spaceBetween = 0 } = props + + const width = getItemWidth({ + galleryWidth: listWidth, + itemsInSlide, + props, + }) + + coords = children.reduce>(( + acc, + _, + i, + ) => { + let position = 0 + const previewsChild = acc[i - 1] + + content += width + spaceBetween + partial = listWidth >= content + + if (previewsChild) { + position = width + spaceBetween + previewsChild.position || 0 + } + + acc.push({ position, width }) + return acc + }, []) + + if (!infinite) { + if (partial) { + coords = mapPartialCoords(coords) + } else { + const position = content - listWidth + coords = mapPositionCoords(coords, position) + } + } + + return { + content, + coords, + partial, + } +} + +type GetItemWidthArgs = { + galleryWidth: number, + itemsInSlide: number, + props: Props, +} + +export const getItemWidth = ({ + galleryWidth, + itemsInSlide, + props: { spaceBetween = 0 }, +}: GetItemWidthArgs) => (itemsInSlide > 0 + ? (galleryWidth - spaceBetween * (itemsInSlide - 1)) / itemsInSlide + : galleryWidth) + +export const getElementDimensions = (element: HTMLElement | null) => { + const { height, width } = element?.getBoundingClientRect() || { height: 0, width: 0 } + + return { height, width } +} + +export const getTransitionProperty = (options?: Transition): string => { + const { animationDuration = 0, animationTimingFunction = 'ease' } = options || {} + return `transform ${animationDuration}ms ${animationTimingFunction} 0ms` +} + +export const getListElementStyles = ( + { translate3d }: Partial, + currentStyles: CSSProperties, +): CSSProperties => { + const transform = `translate3d(${-(translate3d || 0)}px, 0, 0)` + + return { ...currentStyles, transform } +} + +export const getItemStyles = (index: number, state: State): CSSProperties => { + const { transformationSet } = state + const { width } = transformationSet[index] || {} + + return { + width, + } +} + +export const getTranslate3dProperty = (nextIndex: number, state: Partial) => { + let cursor = nextIndex + + const { + infinite, + itemsInSlide = 0, + itemsOffset = 0, + transformationSet = [], + } = state + + if (infinite) { + cursor = nextIndex + getShiftIndex(itemsInSlide, itemsOffset) + } + + return (transformationSet[cursor] || {}).position || 0 +} + +export const isDisplayedItem = (i = 0, state: State) => { + const { + activeIndex, + customWidth, + infinite, + itemsInSlide, + itemsOffset, + } = state + + const shiftIndex = getShiftIndex(itemsInSlide, itemsOffset) + + if (customWidth && 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, + customWidth, + infinite, + } = props + + if (customWidth) { + return infinite ? itemsCount : itemsInSlide + } + + if (breakpoints) { + const configKeys = Object.keys(breakpoints) + + if (configKeys.length) { + configKeys.forEach((key) => { + if (Number(key) <= window.innerWidth) { + const { items, itemsFit = 'fill' } = breakpoints[key] + + if (itemsFit === 'contain') { + itemsInSlide = items + } else { + itemsInSlide = Math.min(items, itemsCount) + } + } + }) + } + } + + return itemsInSlide || 1 +} diff --git a/src/components/Carousel/helpers/index.tsx b/src/components/Carousel/helpers/index.tsx new file mode 100644 index 00000000..412e9262 --- /dev/null +++ b/src/components/Carousel/helpers/index.tsx @@ -0,0 +1,4 @@ +export * from './common' +export * from './elements' +export * from './math' +export * from './mappers' diff --git a/src/components/Carousel/helpers/mappers.tsx b/src/components/Carousel/helpers/mappers.tsx new file mode 100644 index 00000000..85e3249c --- /dev/null +++ b/src/components/Carousel/helpers/mappers.tsx @@ -0,0 +1,12 @@ +import type { ItemCoords } from '../types' + +export const mapPartialCoords = (coords: Array) => ( + coords.map(({ width }) => ({ position: 0, width })) +) + +export const mapPositionCoords = (coords: Array, position = 0) => coords.map((item) => { + if (item.position > position) { + return { ...item, position } + } + return item +}) diff --git a/src/components/Carousel/helpers/math.tsx b/src/components/Carousel/helpers/math.tsx new file mode 100644 index 00000000..dadf3054 --- /dev/null +++ b/src/components/Carousel/helpers/math.tsx @@ -0,0 +1,44 @@ +import type { ItemCoords } from '../types' + +export const getShiftIndex = (itemsInSlide = 0, itemsOffset = 0) => itemsInSlide + itemsOffset + +export const getStartIndex = (index = 0, itemsCount = 0) => { + if (itemsCount) { + if (index >= itemsCount) { + return itemsCount - 1 + } + + if (index > 0) { + return index + } + } + + return 0 +} + +export const getActiveIndex = ({ + infinite = false, + itemsCount = 0, + startIndex = 0, +}) => (infinite ? startIndex : getStartIndex(startIndex, itemsCount)) + +export const getUpdateSlidePositionIndex = (activeIndex: number, itemsCount: number) => { + if (activeIndex < 0) return itemsCount - 1 + if (activeIndex >= itemsCount) return 0 + + return activeIndex +} + +export const shouldRecalculateSlideIndex = (activeIndex: number, itemsCount: number) => ( + activeIndex < 0 || activeIndex >= itemsCount +) + +export const shouldCancelSlideAnimation = (activeIndex: number, itemsCount: number) => ( + activeIndex < 0 || activeIndex >= itemsCount +) + +export const getTransformationItemIndex = ( + transformationSet: Array = [], + position = 0, +) => transformationSet.findIndex((item) => item.position >= Math.abs(position)) + diff --git a/src/components/Carousel/hooks.tsx b/src/components/Carousel/hooks.tsx new file mode 100644 index 00000000..05ebb207 --- /dev/null +++ b/src/components/Carousel/hooks.tsx @@ -0,0 +1,160 @@ +import { + useRef, + useEffect, + useLayoutEffect, + type ReactNode, +} from 'react' + +import { KEYBOARD_KEYS } from 'config' + +import { useEventListener, useObjectState } from 'hooks' + +import type { State, Props } from './types' +import * as Utils from './helpers' +import { ListItem } from './styled' + +export const useCarousel = (props: Props) => { + const [state, setState] = useObjectState(Utils.calculateInitialState(props, null)) + const isAnimationDisabledRef = useRef(false) + const slideEndTimeoutIdRef = useRef(null) + const listElementRef = useRef(null) + const rootElementRef = useRef(null) + + const { + activeIndex, + animationDuration, + clones, + itemsCount, + itemsInSlide, + transition, + translate3d, + } = state + + const { + animationTimingFunction, + infinite, + onSlideChange, + useKeyboardNavigation, + } = props + + const listElementStyles = Utils.getListElementStyles({ translate3d }, { transition }) + + const clearSlideEndTimeout = () => { + slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current) + slideEndTimeoutIdRef.current = null + } + + const slidePrev = () => { + const newActiveIndex = activeIndex - 1 + + handleSlideTo(newActiveIndex) + } + + const slideNext = () => { + const newActiveIndex = activeIndex + 1 + + handleSlideTo(newActiveIndex) + } + + const handleUpdateSlidePosition = (index: number) => { + const newTranslate3d = Utils.getTranslate3dProperty(index, state) + const newTransition = Utils.getTransitionProperty({ animationDuration: 0 }) + + setState({ + activeIndex: index, + transition: newTransition, + translate3d: newTranslate3d, + }) + } + + const handleBeforeSlideEnd = (index: number) => { + if (Utils.shouldRecalculateSlideIndex(index, itemsCount)) { + const nextIndex = Utils.getUpdateSlidePositionIndex(index, itemsCount) + handleUpdateSlidePosition(nextIndex) + } + + isAnimationDisabledRef.current = false + } + + const handleSlideTo = (newActiveIndex = 0) => { + if ( + isAnimationDisabledRef.current + || newActiveIndex === activeIndex + || (!infinite && Utils.shouldCancelSlideAnimation(newActiveIndex, itemsCount)) + ) return + + isAnimationDisabledRef.current = true + clearSlideEndTimeout() + + const newTranslate3d = Utils.getTranslate3dProperty(newActiveIndex, state) + + const newTransition = Utils.getTransitionProperty({ + animationDuration, + animationTimingFunction, + }) + + onSlideChange?.(newActiveIndex) + + setState({ + activeIndex: newActiveIndex, + transition: newTransition, + translate3d: newTranslate3d, + }) + + slideEndTimeoutIdRef.current = setTimeout( + () => handleBeforeSlideEnd(newActiveIndex), + animationDuration, + ) + } + + const renderItem = (item: ReactNode, index: number) => { + const styles = Utils.getItemStyles(index, state) + + return ( + + {item} + + ) + } + + useEventListener({ + callback: (e) => { + if (!useKeyboardNavigation) return + if (e.key === KEYBOARD_KEYS.ArrowLeft) slidePrev() + if (e.key === KEYBOARD_KEYS.ArrowRight) slideNext() + }, + event: 'keydown', + }) + + useLayoutEffect(() => { + const setInitialState = () => { + const initialState = Utils.calculateInitialState(props, listElementRef.current) + + setState(initialState) + } + + setInitialState() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.activeIndex]) + + useEffect(() => () => { + slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current) + }, []) + + return { + activeIndex, + clones, + itemsCount, + itemsInSlide, + listElementRef, + listElementStyles, + renderItem, + rootElementRef, + slideNext, + slidePrev, + } +} diff --git a/src/components/Carousel/index.tsx b/src/components/Carousel/index.tsx new file mode 100644 index 00000000..61521613 --- /dev/null +++ b/src/components/Carousel/index.tsx @@ -0,0 +1,86 @@ +import { ArrowButton, Arrow } from 'features/HeaderFilters/components/DateFilter/styled' + +import type { Props } from './types' +import { useCarousel } from './hooks' +import { + Wrapper, + List, + ButtonsWrapper, +} from './styled' + +export * from './types' + +type NavButtonProps = { + direction: 'left' | 'right', + disabled?: boolean, + onClick: () => void, +} + +const NavButton = ({ + direction, + disabled, + onClick, +}: NavButtonProps) => ( + + + +) + +export const Carousel = (props: Props) => { + const { + infinite, + renderNextButton, + renderPrevButton, + } = props + + const { + activeIndex, + clones, + itemsCount, + itemsInSlide, + listElementRef, + listElementStyles, + renderItem, + rootElementRef, + slideNext, + slidePrev, + } = useCarousel(props) + + return ( + + + {clones.map(renderItem)} + + + {renderPrevButton + ? renderPrevButton({ + disabled: !infinite && activeIndex === 0, + onClick: slidePrev, + }) + : ( + + )} + {renderNextButton + ? renderNextButton({ + disabled: !infinite && itemsInSlide + activeIndex === itemsCount, + onClick: slideNext, + }) + : ( + + )} + + + ) +} diff --git a/src/components/Carousel/styled.tsx b/src/components/Carousel/styled.tsx new file mode 100644 index 00000000..bf8b3e76 --- /dev/null +++ b/src/components/Carousel/styled.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + width: 100%; + margin: auto; + overflow: hidden; +` + +export const List = styled.ul` + display: flex; + gap: 20px; + width: 100%; + height: 100%; +` + +export const ListItem = styled.li` + flex-shrink: 0; + height: 100%; +` + +export const ButtonsWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +` diff --git a/src/components/Carousel/types.tsx b/src/components/Carousel/types.tsx new file mode 100644 index 00000000..7abc997f --- /dev/null +++ b/src/components/Carousel/types.tsx @@ -0,0 +1,73 @@ +import type { ReactNode } from 'react' + +type RenderButtonArgs = { + disabled?: boolean, + onClick: () => void, +} + +export type Transition = { + animationDuration?: number, + animationTimingFunction?: string, +} + +export type Breakpoints = { + [key: string]: { + // Число элементов в слайде + items: number, + // Определяет, как элемент должен заполнять контейнер в соответствии с шириной слайда + itemsFit?: 'contain' | 'fill', + }, +} + +export type Transformations = { + content: number, + coords: Array, + partial: boolean, +} + +export type ItemCoords = { + position: number, + width: number, +} + +export type Props = { + /** Текущая позиция */ + activeIndex?: number, + /** Длительность анимации */ + animationDuration?: number, + /** animation-timing-function */ + animationTimingFunction?: string, + /** Объект с брэкпойнтами */ + breakpoints?: Breakpoints, + /** Элементы карусели */ + children: ReactNode, + /** Свойство позволяет задать свою ширину элементов карусели */ + customWidth?: boolean, + /** Бесконечный режим прокрутки */ + infinite?: boolean, + /** Колбэк при прокрутке */ + onSlideChange?: (activeIndex: number) => void, + /** Рендер-функция кнопки прокрутки вперед */ + renderNextButton?: (args: RenderButtonArgs) => ReactNode, + /** Рендер-функция кнопки прокрутки назад */ + renderPrevButton?: (args: RenderButtonArgs) => ReactNode, + /** Расстояние между элементами карусели */ + spaceBetween?: number, + /** Использование клавиатуры для навигации */ + useKeyboardNavigation?: boolean, +} + +export type State = { + activeIndex: number, + animationDuration?: number, + clones: Array, + customWidth: boolean, + infinite?: boolean, + itemsCount: number, + itemsInSlide: number, + itemsOffset: number, + listWidth: number, + transformationSet: Array, + transition: string, + translate3d: number, +} diff --git a/src/config/index.tsx b/src/config/index.tsx index cf9cf018..792f6aeb 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -13,3 +13,4 @@ export * from './queries' export * from './keyboardKeys' export * from './clients' export * from './localStorageKeys' +export * from './payments' diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index a4c9227f..0c3b5835 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -90,11 +90,16 @@ const confirmPopup = { const buyMatchPopupLexics = { add: 15075, adding_card: 15074, + all_away_games_of: 20186, + all_games_of: 20182, + all_home_games_of: 20185, auto_renewal: 16624, + best_choice: 20175, buy_for: 14095, buy_subscription: 13565, + cancel_anytime: 20171, change_card: 13564, - choose_subscription: 13563, + choose_subscription: 20174, completed: 14072, description_all_season_matches: 15069, description_all_team_matches: 15070, @@ -105,7 +110,11 @@ const buyMatchPopupLexics = { for_month: 13561, for_view: 15064, for_year: 13562, + how_to_watch: 20199, + in: 20183, + in_season: 20184, next_choose: 15156, + one_off_payment: 20172, pass_league: 15065, pass_match_access: 15067, pass_team: 15066, @@ -114,8 +123,10 @@ const buyMatchPopupLexics = { pay: 15073, payment: 14096, payment_confirmation: 14094, - per_month: 13573, + per_month: 20173, per_year: 13574, + purchase: 12623, + subscription: 15604, subscription_done: 2668, success_subscription: 14097, } diff --git a/src/config/pages.tsx b/src/config/pages.tsx index a3ca9614..410e3e23 100644 --- a/src/config/pages.tsx +++ b/src/config/pages.tsx @@ -7,6 +7,7 @@ export const PAGES = { mailings: '/useraccount/mailings', match: '/matches', player: '/players', + subscriptions: '/subscriptions', team: '/teams', thanksForSubscribe: '/thanks-for-subscription', tournament: '/tournaments', diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx index cddfc571..7cb4c33a 100644 --- a/src/features/App/AuthenticatedApp.tsx +++ b/src/features/App/AuthenticatedApp.tsx @@ -9,9 +9,12 @@ import { import { RecoilRoot } from 'recoil' import { indexLexics } from 'config/lexics/indexLexics' -import { isProduction } from 'config/env' -import { PAGES } from 'config/pages' -import { client } from 'config/clients' +import { + client, + PAGES, + isProduction, + isMobileDevice, +} from 'config' import { StripeElements } from 'features/StripeElements' import { useLexicsConfig } from 'features/LexicsStore' @@ -19,7 +22,7 @@ import { ExtendedSearchStore } from 'features/ExtendedSearchPage' import { MatchSwitchesStore } from 'features/MatchSwitches' import { UserFavoritesStore } from 'features/UserFavorites/store' import { MatchPopup, MatchPopupStore } from 'features/MatchPopup' -import { BuyMatchPopup, BuyMatchPopupStore } from 'features/BuyMatchPopup' +import { BuyMatchPopupStore } from 'features/BuyMatchPopup' import { PreferencesPopup, PreferencesPopupStore } from 'features/PreferencesPopup' import { TournamentsPopup } from 'features/TournamentsPopup' import { TournamentPopupStore } from 'features/TournamentsPopup/store' @@ -41,6 +44,7 @@ const HighlightsPage = lazy(() => import('pages/HighlightsPage')) const ThanksPage = lazy(() => import('pages/ThanksPage')) const Mailings = lazy(() => import('pages/Mailings')) const FailedPaymeePage = lazy(() => import('pages/FailedPaymeePage')) +const SubscriptionsPage = lazy(() => import('pages/SubscriptionsPage')) export const AuthenticatedApp = () => { useSportList() @@ -59,7 +63,6 @@ export const AuthenticatedApp = () => { - { client.name === 'facr' ? : } {/* в Switch как прямой children @@ -98,6 +101,11 @@ export const AuthenticatedApp = () => { + {isMobileDevice && ( + + + + )} {!isProduction && } diff --git a/src/features/BuyMatchPopup/components/CardStep/index.tsx b/src/features/BuyMatchPopup/components/CardStep/index.tsx index a0519f15..03eb1406 100644 --- a/src/features/BuyMatchPopup/components/CardStep/index.tsx +++ b/src/features/BuyMatchPopup/components/CardStep/index.tsx @@ -51,7 +51,7 @@ export const CardStep = ({ return (
- + {isHighlightsPage ? '' : } diff --git a/src/features/BuyMatchPopup/components/ErrorStep/index.tsx b/src/features/BuyMatchPopup/components/ErrorStep/index.tsx index 461d99c0..abfc90a3 100644 --- a/src/features/BuyMatchPopup/components/ErrorStep/index.tsx +++ b/src/features/BuyMatchPopup/components/ErrorStep/index.tsx @@ -1,3 +1,7 @@ +import { useHistory } from 'react-router-dom' + +import { isMobileDevice } from 'config' + import { T9n } from 'features/T9n' import { Header, HeaderTitle } from 'features/PopupComponents' @@ -16,6 +20,13 @@ export const ErrorStep = () => { error: paymentError, } = useBuyMatchPopupStore() + const history = useHistory() + + const handleButtonclick = () => { + close() + isMobileDevice && history.goBack() + } + return (
@@ -29,7 +40,7 @@ export const ErrorStep = () => {
- + Ок
diff --git a/src/features/BuyMatchPopup/components/PackageMobile/index.tsx b/src/features/BuyMatchPopup/components/PackageMobile/index.tsx new file mode 100644 index 00000000..55993ef9 --- /dev/null +++ b/src/features/BuyMatchPopup/components/PackageMobile/index.tsx @@ -0,0 +1,94 @@ +import { useState, type MouseEvent } from 'react' + +import { useToggle } from 'hooks' + +import type { MatchPackage } from 'features/BuyMatchPopup/types' +import { SubscriptionType } from 'features/BuyMatchPopup/types' +import { Price } from 'features/Price' +import { T9n } from 'features/T9n' +import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store' +import { ArrowLoader } from 'features/ArrowLoader' + +import { usePackage } from '../RegularPackage/usePackage' +import { + Wrapper, + Header, + Title, + Button, + Content, + Description, + Body, + Border, + SubscriptionTypeText, + BestChoice, +} from './styled' + +type Props = { + buttonId?: string, + buttonLexic?: string, + matchPackage: MatchPackage, + onButtonClick?: (e: MouseEvent, matchPackage?: MatchPackage) => void, +} + +export const PackageMobile = ({ + buttonId, + buttonLexic, + matchPackage, + onButtonClick, +}: Props) => { + const { + matchPackages, + setSelectedPackage, + } = useBuyMatchPopupStore() + + const isSinglePackage = matchPackages.length === 1 + + const { isOpen, toggle } = useToggle(isSinglePackage) + const [loader, setLoader] = useState(false) + + const { firstDescription, priceTextTopLexic } = usePackage(matchPackage) + + const handleButtonClick = (e: MouseEvent) => { + setSelectedPackage(matchPackage) + onButtonClick?.(e, matchPackage) + setLoader(true) + } + + return ( + +
+ + <Price + amount={matchPackage.originalObject.price} + currency={matchPackage.currency} + perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined} + /> + </Header> + {isOpen && <Border />} + <Content isOpen={isOpen}> + <Body> + <Description> + {firstDescription} + </Description> + <Description> + <T9n t={matchPackage.originalObject.sub.lexic3} /> + </Description> + {matchPackage.isMainPackage && ( + <BestChoice> + <T9n t='best_choice' /> + </BestChoice> + )} + <SubscriptionTypeText t={priceTextTopLexic} /> + </Body> + <Button id={buttonId} onClick={handleButtonClick}> + {loader + ? <ArrowLoader disabled /> + : <T9n t={buttonLexic || ''} />} + </Button> + </Content> + </Wrapper> + ) +} diff --git a/src/features/BuyMatchPopup/components/PackageMobile/styled.tsx b/src/features/BuyMatchPopup/components/PackageMobile/styled.tsx new file mode 100644 index 00000000..9c99c7d9 --- /dev/null +++ b/src/features/BuyMatchPopup/components/PackageMobile/styled.tsx @@ -0,0 +1,125 @@ +import styled, { css } from 'styled-components/macro' + +import { T9n } from 'features/T9n' +import { ButtonSolid } from 'features/Common' +import { PriceDetails, PriceAmount } from 'features/Price/styled' + +export const Wrapper = styled.div` + width: 100%; + border-radius: 2px 2px 0px 0px; + background-color: #414141; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); +` + +type HeaderProps = { + isOpen?: boolean, +} + +export const Header = styled.div<HeaderProps>` + position: relative; + display: flex; + justify-content: space-between; + height: 52px; + align-items: center; + padding: 0 15px 0 20px; + + ${PriceAmount} { + font-size: 24px; + position: absolute; + right: 46px; + top: 50%; + translate: 0 -50%; + } + + ${PriceDetails} { + position: absolute; + top: 12px; + left: calc(100% - 48px); +} + + ${({ isOpen }) => (isOpen + ? '' + : css` + ${PriceDetails}, ${PriceAmount}, ${Title} { + color: #D9D9D9; + } + `)} +` + +export const Border = styled.div` + height: 0.66px; + background: linear-gradient(90deg, transparent 0%, #656565 50%, transparent 100%); +` + +export const Title = styled(T9n)` + max-width: calc(100% - 112px); + font-weight: 600; + font-size: 14px; + color: ${({ theme }) => theme.colors.white}; + overflow: hidden; +` + +type ContentProps = { + isOpen?: boolean, +} + +export const Content = styled.div<ContentProps>` + height: ${({ isOpen }) => (isOpen ? 'auto' : 0)}; +` + +export const Body = styled.div` + position: relative; + padding: 8px 24px 30px; + background-color: #414141; +` + +export const Description = styled.p` + width: calc(100% - 90px); + margin-bottom: 15px; + font-weight: 500; + font-size: 12px; + line-height: 16px; + color: #C7C7C7; + + :empty, :has(span:empty) { + display: none; + } + + :last-of-type { + margin-bottom: 0; + } +` + +export const Button = styled(ButtonSolid)` + width: 100%; + height: 48px; +` + +export const SubscriptionTypeText = styled(T9n)` + position: absolute; + right: 16px; + bottom: 12px; + font-size: 12px; + font-weight: 600; + color: ${({ theme }) => theme.colors.white}; +` + +export const BestChoice = styled.div` + position: absolute; + bottom: 40px; + right: 0; + display: flex; + align-items: center; + justify-content: flex-end; + width: 108px; + height: 20px; + padding-right: 16px; + border-radius: 1px 0px 0px 1px; + font-weight: 600; + font-size: 12px; + font-variant: all-small-caps; + letter-spacing: 0.03em; + color: #464646; + background: linear-gradient(270deg, rgba(253, 253, 254, 0.8) 0%, rgba(253, 253, 254, 0) 97.42%); + backdrop-filter: blur(3px); +` diff --git a/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx b/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx index 226e38df..f65af167 100644 --- a/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx +++ b/src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx @@ -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> diff --git a/src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx b/src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx new file mode 100644 index 00000000..76fe3def --- /dev/null +++ b/src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components/macro' + +import { isMobileDevice } from 'config' + +import { Footer as FooterBase } from 'features/BuyMatchPopup/styled' + +export const ChooseSub = styled.div` + font-weight: 700; + font-size: ${isMobileDevice ? 16 : 24}px; + margin: auto; + color: ${({ theme }) => theme.colors.white}; +` + +type FooterProps = { + hasCard?: boolean, +} + +export const Footer = styled(FooterBase)<FooterProps>` + flex-direction: column; + align-items: center; + margin-top: ${({ hasCard }) => (hasCard ? 20 : 40)}px; +` diff --git a/src/features/BuyMatchPopup/components/Packages/index.tsx b/src/features/BuyMatchPopup/components/Packages/index.tsx index 749f674a..626af78c 100644 --- a/src/features/BuyMatchPopup/components/Packages/index.tsx +++ b/src/features/BuyMatchPopup/components/Packages/index.tsx @@ -1,41 +1,181 @@ -import styled from 'styled-components/macro' +import type { MouseEvent } from 'react' +import { useMemo } from 'react' -import { PaymentPeriodTabs } from 'features/PaymentPeriodTabs' +import reduce from 'lodash/reduce' +import sortBy from 'lodash/sortBy' +import without from 'lodash/without' + +import { isMobileDevice } from 'config' + +import type { MatchPackage } from 'features/BuyMatchPopup/types' +import { useLexicsConfig } from 'features/LexicsStore' + +import { Carousel, type Breakpoints } from 'components/Carousel' import { useBuyMatchPopupStore } from '../../store' -import { PackagesList } from '../PackagesList' - -const Wrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -` - -export const Packages = () => { - const { - onPackageSelect, - onPeriodSelect, - selectedPackage, - selectedPeriod, - selectedSubscription, - subscriptions, - } = useBuyMatchPopupStore() - - if (!selectedSubscription) return null +import { RegularPackage } from '../RegularPackage' +import { PackageMobile } from '../PackageMobile' +import { + Wrapper, + List, + PrevButton, + NextButton, + Arrow, + ListItem, +} from './styled' +import { SinglePackage } from '../SinglePackage' + +const breakpoints: Breakpoints = { + 1000: { items: 3 }, + 1280: { items: 4 }, + 1400: { items: 5 }, +} + +type ButtonProps = { + disabled?: boolean, + onClick: () => void, +} + +const renderPrevButton = ({ disabled, onClick }: ButtonProps) => ( + <PrevButton + aria-label='Previous' + disabled={disabled} + onClick={onClick} + > + <Arrow direction='left' /> + </PrevButton> +) + +const renderNextButton = ({ disabled, onClick }: ButtonProps) => ( + <NextButton + aria-label='Next' + disabled={disabled} + onClick={onClick} + > + <Arrow direction='right' /> + </NextButton> +) + +type Props = { + buttonId?: string, + buttonLexic?: string, + onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void, +} + +export const Packages = ({ + buttonId, + buttonLexic, + onButtonClick, +}: Props) => { + const { matchPackages } = useBuyMatchPopupStore() + + const hasOnlyOneSubscription = matchPackages.length === 1 + const hasMoreThanFiveSubscriptions = matchPackages.length > 5 + + const getSortedPackages = () => { + let temp = sortBy(matchPackages, 'order') + const mainPackage = matchPackages.find(({ isMainPackage }) => isMainPackage) + + if (mainPackage && matchPackages.length > 1) { + const index = matchPackages.length >= 2 && matchPackages.length < 4 ? 1 : 2 + + temp = without(temp, mainPackage) + temp.splice( + index, + 0, + mainPackage, + ) + } + + return temp + } + + const sortedPackages = getSortedPackages() + + const lexicsIds = useMemo( + () => [...reduce<MatchPackage, Set<number>>( + matchPackages, + (acc, { originalObject: { sub } }) => { + acc.add(sub.lexic1) + sub.lexic2 && acc.add(sub.lexic2) + acc.add(sub.lexic3) + + return acc + }, + new Set(), + )], + [matchPackages], + ) + + useLexicsConfig(lexicsIds) + + if (isMobileDevice) { + return ( + <Wrapper> + <List> + {sortedPackages.map((matchPackage) => ( + <ListItem key={matchPackage.id}> + <PackageMobile + matchPackage={matchPackage} + onButtonClick={onButtonClick} + buttonId={buttonId} + buttonLexic={buttonLexic} + /> + </ListItem> + ))} + </List> + </Wrapper> + ) + } + + if (hasOnlyOneSubscription) { + return ( + <SinglePackage matchPackage={matchPackages[0]} /> + ) + } + + const getWrapperWidth = () => { + switch (true) { + case !hasMoreThanFiveSubscriptions: + return undefined + + case window.innerWidth < 1500: + return window.innerWidth - 160 + + default: + return 1380 + } + } + + const wrapperWidth = getWrapperWidth() return ( - <Wrapper> - <PaymentPeriodTabs - onPeriodSelect={onPeriodSelect} - selectedPeriod={selectedPeriod} - selectedSubscription={selectedSubscription} - /> - <PackagesList - packages={subscriptions} - selectedPackage={selectedPackage} - onSelect={onPackageSelect} - /> + <Wrapper width={wrapperWidth}> + {hasMoreThanFiveSubscriptions + ? ( + <Carousel + animationDuration={400} + infinite + useKeyboardNavigation + breakpoints={breakpoints} + spaceBetween={20} + renderPrevButton={renderPrevButton} + renderNextButton={renderNextButton} + > + {sortedPackages.map((matchPackage) => ( + <RegularPackage matchPackage={matchPackage} /> + ))} + </Carousel> + ) + : ( + <List> + {sortedPackages.map((matchPackage) => ( + <ListItem key={matchPackage.id}> + <RegularPackage matchPackage={matchPackage} /> + </ListItem> + ))} + </List> + )} </Wrapper> ) } diff --git a/src/features/BuyMatchPopup/components/Packages/styled.tsx b/src/features/BuyMatchPopup/components/Packages/styled.tsx new file mode 100644 index 00000000..4e8e2e53 --- /dev/null +++ b/src/features/BuyMatchPopup/components/Packages/styled.tsx @@ -0,0 +1,65 @@ +import styled, { css } from 'styled-components/macro' + +import { isMobileDevice } from 'config' + +import { ArrowButton, Arrow as ArrowBase } from 'features/HeaderFilters/components/DateFilter/styled' + +type WrapperProps = { + width?: number, +} + +export const Wrapper = styled.div<WrapperProps>` + width: ${({ width }) => (width ? `${width}px` : 'auto')}; + margin: auto; +` + +export const List = styled.ul` + display: flex; + gap: 20px; + + ${isMobileDevice + ? css` + display: initial; + max-height: calc(100vh - 140px); + overflow-y: auto; + ` + : ''} +` + +export const ListItem = styled.li` + width: 260px; + + ${isMobileDevice + ? css` + width: 100%; + margin-bottom: 12px; + border-radius: 2px; + overflow: hidden; + ` + : ''} +` + +const NavButton = styled(ArrowButton)` + position: absolute; + top: 50%; + translate: 0 -50%; + + ${({ disabled }) => (disabled + ? css` + opacity: 0.5; + ` + : '')} +` + +export const PrevButton = styled(NavButton)` + left: 30px; +` + +export const NextButton = styled(NavButton)` + right: 30px; +` + +export const Arrow = styled(ArrowBase)` + width: 20px; + height: 20px; +` diff --git a/src/features/BuyMatchPopup/components/PackagesList/index.tsx b/src/features/BuyMatchPopup/components/PackagesList/index.tsx deleted file mode 100644 index a92415c2..00000000 --- a/src/features/BuyMatchPopup/components/PackagesList/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import isNumber from 'lodash/isNumber' -import map from 'lodash/map' - -import { T9n } from 'features/T9n' -import { MatchPackage, SubscriptionType } from 'features/BuyMatchPopup/types' - -import { - Pass, - Name, - Description, - InfoWrapper, - Item, - List, - Price, - ScAutoRenewal, - ScPriceContainer, -} from './styled' - -type Props = { - onSelect: (subscription: MatchPackage) => void, - packages: Array<MatchPackage>, - selectedPackage: MatchPackage | null, -} - -export const PackagesList = ({ - onSelect, - packages, - selectedPackage, -}: Props) => ( - <List> - { - map( - packages, - (subPackage) => ( - <Item - key={subPackage.id} - onClick={() => onSelect(subPackage)} - active={subPackage === selectedPackage} - > - <InfoWrapper> - <Pass> - <T9n t={subPackage.pass} /> - </Pass> - <Name> - { - isNumber(subPackage.nameLexic) - ? <T9n t={subPackage.nameLexic} /> - : subPackage.name - } - </Name> - <Description> - <T9n - t={subPackage.description.lexic} - values={subPackage.description.values} - /> - </Description> - </InfoWrapper> - <ScPriceContainer> - <Price - amount={subPackage.price} - currency={subPackage.currency} - perPeriod={ - subPackage.type !== SubscriptionType.Month - ? null - : `per_${subPackage.type}` - } - /> - { - subPackage.type === SubscriptionType.Month - && ( - <ScAutoRenewal> - <T9n - t='auto_renewal' - /> - </ScAutoRenewal> - ) - } - </ScPriceContainer> - </Item> - ), - ) - } - </List> -) diff --git a/src/features/BuyMatchPopup/components/PackagesList/styled.tsx b/src/features/BuyMatchPopup/components/PackagesList/styled.tsx deleted file mode 100644 index ba648f67..00000000 --- a/src/features/BuyMatchPopup/components/PackagesList/styled.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import styled, { css } from 'styled-components/macro' -import { isMobileDevice } from 'config/userAgent' - -import { popupScrollbarStyles } from 'features/PopupComponents' -import { Price as BasePrice } from 'features/Price' -import { - PriceAmount, - PriceDetails, - Period, -} from 'features/Price/styled' - -export const List = styled.ul` - width: 100%; - height: 460px; - overflow-y: auto; - margin-top: 25px; - padding: 0 40px; - - ${popupScrollbarStyles} - - @media (max-width: 1370px) { - max-height: 415px; - height: auto; - } - - @media (max-height: 768px) { - max-height: 280px; - } - - @media (max-height: 500px) { - max-height: 140px; - } - - ${isMobileDevice - ? css` - padding: 0; - margin-top: 19px; - @media screen and (orientation: landscape){ - margin-top: 10px; - } - ` - : ''}; -` - -type ItemProps = { - active?: boolean, -} - -export const Item = styled.li.attrs(() => ({ - tabIndex: 0, -}))<ItemProps>` - width: 100%; - min-height: 140px; - padding: 20px 30px 20px 20px; - background: ${({ theme }) => theme.colors.packageBackground}; - box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); - border-radius: 2px; - - display: flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - transition: background-color 0.3s; - - :not(:last-child) { - margin-bottom: 20px; - } - - ${({ active, theme: { colors } }) => ( - active ? `background-color: ${colors.button}` : '' - )}; - - @media (max-width: 1370px) { - min-height: 125px; - } - - @media (max-width: 750px) { - height: 100%; - } - - ${isMobileDevice - ? css` - padding: 5px 10px; - - @media (max-width: 750px) { - min-height: 52.29px; - height: auto; - } - - :not(:last-child) { - margin-bottom: 10px; - - @media screen and (orientation: landscape) { - margin-bottom: 5.24px; - } - } - ` : ''}; - - @media (max-width: 850px) and (orientation: landscape){ - min-height: 52.29px; - height: auto; - } -` - -export const InfoWrapper = styled.div` - width: 75%; - display: flex; - flex-direction: column; - align-self: flex-start; - ${isMobileDevice - ? css` - align-self: center; - @media (max-width: 850px) and (orientation: landscape){ - height: 100%; - } - ` - : ''}; -` - -export const Name = styled.span` - font-weight: 500; - font-size: 20px; - line-height: 23px; - letter-spacing: 0.03em; - - @media (max-width: 1370px) { - line-height: 20px; - } - ${isMobileDevice - ? css` - @media (max-width: 750px){ - font-size: 12px; - line-height: 10.04px; - } - @media screen and (orientation: landscape){ - font-size: 14px; - } - ` - : ''}; -` - -export const Pass = styled(Name)` - font-weight: 600; - text-transform: uppercase; - ${isMobileDevice - ? css` - line-height: 12px; - @media screen and (orientation: landscape){ - line-height: 14px; - } - ` - : ''}; -` - -export const Description = styled.span` - width: 68%; - margin-top: 13px; - font-weight: 500; - font-size: 15px; - line-height: 20px; - letter-spacing: 0.03em; - - @media (max-width: 1370px) { - line-height: 18px; - } - ${isMobileDevice - ? css` - @media (max-width: 750px){ - font-size: 8px; - line-height: 8px; - margin-top: 5px; - width: 100%; - } - @media (max-width: 850px) and (orientation: landscape){ - margin-top: 0; - line-height: 8px; - font-size: 10px; - } - ` - : ''}; -` - -export const Price = styled(BasePrice)` - ${PriceAmount} { - font-size: 24px; - line-height: 24px; - font-weight: normal; - ${isMobileDevice - ? css` - font-size: 14px; - ` - : ''}; - } - - ${PriceDetails} { - font-weight: 500; - font-size: 12px; - line-height: 18px; - ${isMobileDevice - ? css` - font-size: 8px; - ` - : ''}; - } - - ${Period} { - text-transform: capitalize; - } -` - -export const ScAutoRenewal = styled(PriceDetails)` - line-height: 21px; - font-size: 12px; - text-transform: none; - margin: 0; - color: ${({ theme: { colors } }) => colors.white70}; - - ${isMobileDevice - ? css` - line-height: normal; - font-size: 10px; - text-align: center; - ` - : ''}; -` - -export const ScPriceContainer = styled.div` - display: flex; - flex-direction: column; -` diff --git a/src/features/BuyMatchPopup/components/RegularPackage/index.tsx b/src/features/BuyMatchPopup/components/RegularPackage/index.tsx new file mode 100644 index 00000000..3fbceeab --- /dev/null +++ b/src/features/BuyMatchPopup/components/RegularPackage/index.tsx @@ -0,0 +1,74 @@ +import type { MatchPackage } from 'features/BuyMatchPopup/types' +import { SubscriptionType } from 'features/BuyMatchPopup/types' +import { T9n } from 'features/T9n' +import { Price } from 'features/Price' + +import { usePackage } from './usePackage' + +import { + Wrapper, + InfoWrapper, + Description, + Header, + HeaderTitle, + BestChoice, + PriceBlock, + PriceTextWrapper, + PriceTextTop, + PriceTextBottom, +} from './styled' + +type Props = { + matchPackage: MatchPackage, +} + +export const RegularPackage = ({ matchPackage }: Props) => { + const { + firstDescription, + handleClick, + handleKeyPress, + isActive, + priceTextBottomLexic, + priceTextTopLexic, + } = usePackage(matchPackage) + + return ( + <Wrapper + onClick={handleClick} + onKeyDown={handleKeyPress} + active={isActive} + isMainPackage={matchPackage.isMainPackage} + > + <Header> + <HeaderTitle + width={matchPackage.id.startsWith('team_') ? 190 : undefined} + t={matchPackage.originalObject.sub.lexic1} + /> + {matchPackage.isMainPackage && ( + <BestChoice> + <T9n t='best_choice' /> + </BestChoice> + )} + </Header> + <InfoWrapper> + <Description> + {firstDescription} + </Description> + <Description> + <T9n t={matchPackage.originalObject.sub.lexic3} /> + </Description> + <PriceBlock isMonthSubscription={matchPackage.isMonthSubscription}> + <PriceTextWrapper> + <PriceTextTop t={priceTextTopLexic} /> + <PriceTextBottom t={priceTextBottomLexic} /> + </PriceTextWrapper> + <Price + amount={matchPackage.originalObject.price} + currency={matchPackage.currency} + perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined} + /> + </PriceBlock> + </InfoWrapper> + </Wrapper> + ) +} diff --git a/src/features/BuyMatchPopup/components/RegularPackage/styled.tsx b/src/features/BuyMatchPopup/components/RegularPackage/styled.tsx new file mode 100644 index 00000000..c8137834 --- /dev/null +++ b/src/features/BuyMatchPopup/components/RegularPackage/styled.tsx @@ -0,0 +1,140 @@ +import styled, { css } from 'styled-components/macro' + +import { PriceDetails } from 'features/Price/styled' +import { T9n } from 'features/T9n' + +export const Description = styled.p` + margin-bottom: 20px; + font-weight: 400; + font-size: 12px; + line-height: 20px; + white-space: initial; + + :empty, :has(span:empty) { + display: none; + } + + :last-of-type { + margin-bottom: 0; + } +` + +type PriceBlockProps = { + isMonthSubscription?: boolean, +} + +export const PriceBlock = styled.div<PriceBlockProps>` + display: flex; + justify-content: space-between; + align-items: center; + position: absolute; + top: 218px; + left: 0; + right: 0; + padding-left: 30px; + padding-right: ${({ isMonthSubscription }) => (isMonthSubscription ? 10 : 20)}px; +` + +export const PriceTextWrapper = styled.div` + display: flex; + flex-direction: column; + white-space: initial; +` + +export const PriceTextTop = styled(T9n)` + margin-bottom: 2px; + font-weight: 600; + font-size: 10px; + font-style: italic; + letter-spacing: 0.03em; +` + +export const PriceTextBottom = styled(T9n)` + font-weight: 400; + font-size: 8px; + font-style: italic; + letter-spacing: 0.03em; +` + +type WrapperProps = { + active?: boolean, + isMainPackage?: boolean, +} + +export const Wrapper = styled.div.attrs({ + tabIndex: 0, +})<WrapperProps>` + width: 100%; + height: 380px; + border-radius: 2px; + background: ${({ theme }) => theme.colors.packageBackground}; + box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s; + cursor: pointer; + + ${({ active, theme: { colors } }) => (active + ? css` + background-color: ${colors.button}; + ` + : css` + :hover { + background-color: #2F3E6D; + }` + )} + + ${({ isMainPackage, theme: { colors } }) => (isMainPackage + ? css` + border: 2px solid ${colors.white}; + ` + : '' + )} +` + +export const Header = styled.header` + display: flex; + flex-direction: column; + justify-content: center; + height: 100px; + background: rgba(255, 255, 255, 0.2); +` + +type HeaderTitleProps = { + width?: number, +} + +export const HeaderTitle = styled(T9n)<HeaderTitleProps>` + flex: 1; + display: flex; + align-items: center; + width: ${({ width }) => (width ? `${width}px` : 'auto')}; + padding-left: 30px; + white-space: initial; + font-size: 16px; + font-weight: 600; +` + +export const BestChoice = styled.div` + display: flex; + align-items: center; + height: 22px; + margin-top: auto; + padding-left: 32px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.03em; + font-variant: all-small-caps; + color: #464646; + background-color: #FDFDFE; +` + +export const InfoWrapper = styled.div` + position: relative; + display: flex; + flex-direction: column; + height: calc(100% - 100px); + padding: 20px 30px 30px; + + ${PriceDetails} { + margin-top: 5px; + } +` diff --git a/src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx b/src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx new file mode 100644 index 00000000..175bad64 --- /dev/null +++ b/src/features/BuyMatchPopup/components/RegularPackage/usePackage.tsx @@ -0,0 +1,70 @@ +import type { KeyboardEvent } from 'react' + +import { KEYBOARD_KEYS } from 'config' + +import type { MatchPackage } from 'features/BuyMatchPopup/types' +import { useBuyMatchPopupStore } from 'features/BuyMatchPopup' +import { useLexicsStore } from 'features/LexicsStore' + +export const usePackage = (matchPackage: MatchPackage) => { + const { + onPackageSelect, + selectedPackage, + } = useBuyMatchPopupStore() + + const { suffix, translate } = useLexicsStore() + + const isActive = matchPackage.id === selectedPackage?.id + + const priceTextTopLexic = matchPackage.isMonthSubscription ? 'subscription' : 'purchase' + const priceTextBottomLexic = matchPackage.isMonthSubscription ? 'cancel_anytime' : 'one_off_payment' + + const getFirstDescription = () => { + const tournament = matchPackage.match.tournament[`name_${suffix}`] + const season = typeof matchPackage.description.values.season === 'string' + ? `${matchPackage.description.values.season.slice(0, 4)}/${matchPackage.description.values.season.slice(-2)}` + : '' + + switch (true) { + case matchPackage.originalObject.sub.lexic2 !== null: + return translate(matchPackage.originalObject.sub.lexic2!) + + case matchPackage.id === '0': + return matchPackage.name + + case matchPackage.id.startsWith('team1'): + case matchPackage.id.startsWith('team2'): + return `${translate('all_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}` + + case matchPackage.id.startsWith('team_home'): + return `${translate('all_home_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}` + + case matchPackage.id.startsWith('team_away'): + return `${translate('all_away_games_of')} ${matchPackage.name} ${translate('in')} ${tournament} ${translate('in_season')} ${season}` + + case matchPackage.id.startsWith('all'): + return `${tournament} ${translate('in_season')} ${season}` + + default: return '' + } + } + + const handleClick = () => { + onPackageSelect(matchPackage) + } + + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === KEYBOARD_KEYS.Enter) { + onPackageSelect(matchPackage) + } + } + + return { + firstDescription: getFirstDescription(), + handleClick, + handleKeyPress, + isActive, + priceTextBottomLexic, + priceTextTopLexic, + } +} diff --git a/src/features/BuyMatchPopup/components/SelectSubscription/index.tsx b/src/features/BuyMatchPopup/components/SelectSubscription/index.tsx deleted file mode 100644 index be9f9513..00000000 --- a/src/features/BuyMatchPopup/components/SelectSubscription/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useCallback } from 'react' - -import map from 'lodash/map' - -import { MDASH } from 'config' - -import { isSubscribePopup } from 'helpers' - -import { Name as Names } from 'features/Name' -import { T9n } from 'features/T9n' -import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store' -import { CloseButton, HeaderActions } from 'features/PopupComponents' -import { - Body, - Button, - Footer, - Header, - HeaderTitle, - Wrapper, -} from 'features/BuyMatchPopup/styled' - -import { MatchPackage, SubscriptionType } from '../../types' - -import { - Description, - InfoWrapper, - Name, - Pass, -} from '../PackagesList/styled' - -import { - Price, - ChooseSub, - ChooseSubItem, - ChooseSubList, -} from './styled' - -export const SelectSubscriptionStep = () => { - const { - close, - match, - matchSubscriptions, - onNext, - onSubscriptionSelect, - selectedSubscription, - } = useBuyMatchPopupStore() - - const getPackagesCurrency = useCallback(( - packages: Record<SubscriptionType, Array<MatchPackage>>, - ) => { - const packageWithValue = Object.entries(packages).find(([key, value]) => value.length)?.[1][0] - return packageWithValue ? packageWithValue.currency : 'RUB' - }, []) - - if (!match || !matchSubscriptions) return null - - return ( - <Wrapper> - <Header> - {!isSubscribePopup() - && ( - <HeaderTitle> - <Names nameObj={match.team1} /> - {` ${MDASH} `} - <Names nameObj={match.team2} /> - <ChooseSub> - <T9n t='choose_subscription' /> - </ChooseSub> - </HeaderTitle> - )} - <HeaderActions position='right'> - <CloseButton onClick={close} /> - </HeaderActions> - </Header> - <Body marginTop={15}> - <ChooseSubList> - {map(matchSubscriptions, (subscription) => ( - <ChooseSubItem - key={subscription.id} - onClick={() => onSubscriptionSelect(subscription)} - active={subscription === selectedSubscription} - > - <InfoWrapper> - <Pass> - <T9n t={subscription.lexic} /> - </Pass> - <Name> - <T9n t={subscription.lexic2} /> - </Name> - <Description> - <T9n t={subscription.lexic3} /> - </Description> - </InfoWrapper> - - <Price - amount={subscription.min_price || 0} - currency={getPackagesCurrency(subscription.packages)} - isFrom={Boolean(subscription.min_price)} - /> - </ChooseSubItem> - ))} - </ChooseSubList> - </Body> - <Footer> - <Button - disabled={!selectedSubscription} - onClick={onNext} - id='purchase_next' - > - <T9n t='next_choose' /> - </Button> - </Footer> - </Wrapper> - ) -} diff --git a/src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx b/src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx deleted file mode 100644 index eab2b6aa..00000000 --- a/src/features/BuyMatchPopup/components/SelectSubscription/styled.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import styled from 'styled-components/macro' -import { Price as BasePrice } from 'features/Price' -import { PriceAmount, PriceDetails } from 'features/Price/styled' - -import { - Item, - List, -} from '../PackagesList/styled' - -export const Price = styled(BasePrice)` - ${PriceAmount} { - font-size: 24px; - font-weight: normal; - } - ${PriceDetails} { - padding-top: 5px; - font-size: 12px; - } -` - -export const ChooseSub = styled.div` - font-weight: 600; - font-size: 16px; - margin: 35px 0 17px; -` - -export const ChooseSubItem = styled(Item)` - min-height: auto; -` - -export const ChooseSubList = styled(List)` - height: auto; -` diff --git a/src/features/BuyMatchPopup/components/SelectedCard/index.tsx b/src/features/BuyMatchPopup/components/SelectedCard/index.tsx index a7ccfc80..add44b85 100644 --- a/src/features/BuyMatchPopup/components/SelectedCard/index.tsx +++ b/src/features/BuyMatchPopup/components/SelectedCard/index.tsx @@ -1,7 +1,4 @@ -import styled, { css } from 'styled-components/macro' - -import { isMobileDevice } from 'config/userAgent' -import capitalize from 'lodash/capitalize' +import styled from 'styled-components/macro' import { useCardsStore } from 'features/CardsStore' import { ButtonOutline } from 'features/Common' @@ -9,61 +6,40 @@ import { T9n } from 'features/T9n' import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store' import { Steps } from 'features/BuyMatchPopup/types' -const Wrapper = styled.div` - display: flex; - margin-top: 25px; - padding: 0 40px; - ${isMobileDevice - ? css` - padding: 0; - justify-content: center; - align-items: center; - ` - : ''}; +type WrapperProps = { + hasOnlyOneSubscription: boolean, +} + +const Wrapper = styled.div<WrapperProps>` + margin: ${({ hasOnlyOneSubscription }) => (hasOnlyOneSubscription ? '0 0 37px auto' : '0 auto 20px 30px')}; + letter-spacing: 0.03em; ` const CardInfo = styled.span` - font-weight: 500; - font-size: 18px; - line-height: 20px; - color: rgba(255, 255, 255, 0.7); - ${isMobileDevice - ? css` - font-size: 14px; - ` - : ''}; + font-size: 16px; + font-weight: 600; + font-style: italic; ` const ChangeCardButton = styled(ButtonOutline)` border: none; width: auto; height: auto; - padding: 0 10px; - margin-left: 10px; - line-height: 20px; - font-size: 14px; - color: rgba(255, 255, 255, 0.5); + margin-left: 5px; + font-size: 13px; + font-style: italic; cursor: pointer; - - :hover { - color: rgba(255, 255, 255); - } - ${isMobileDevice - ? css` - font-size: 12px; - ` - : ''}; ` export const SelectedCard = () => { - const { goTo } = useBuyMatchPopupStore() + const { goTo, matchPackages } = useBuyMatchPopupStore() const { defaultCard } = useCardsStore() if (!defaultCard) return null return ( - <Wrapper> - <CardInfo>{capitalize(defaultCard?.brand)} •••• {defaultCard?.last4}</CardInfo> + <Wrapper hasOnlyOneSubscription={matchPackages.length === 1}> + <CardInfo>{defaultCard.brand} •••• {defaultCard.last4}</CardInfo> <ChangeCardButton onClick={(e) => goTo(Steps.CardSelection, e)}> <T9n t='change_card' /> </ChangeCardButton> diff --git a/src/features/BuyMatchPopup/components/SinglePackage/index.tsx b/src/features/BuyMatchPopup/components/SinglePackage/index.tsx new file mode 100644 index 00000000..8161a12b --- /dev/null +++ b/src/features/BuyMatchPopup/components/SinglePackage/index.tsx @@ -0,0 +1,52 @@ +import type { MatchPackage } from 'features/BuyMatchPopup/types' +import { SubscriptionType } from 'features/BuyMatchPopup/types' + +import { T9n } from 'features/T9n' +import { Price } from 'features/Price' + +import { usePackage } from '../RegularPackage/usePackage' +import { + Wrapper, + Description, + PriceBlock, + PriceTextWrapper, + PriceTextTop, + PriceTextBottom, +} from './styled' + +type Props = { + matchPackage: MatchPackage, +} + +export const SinglePackage = ({ matchPackage }: Props) => { + const { + firstDescription, + priceTextBottomLexic, + priceTextTopLexic, + } = usePackage(matchPackage) + + return ( + <Wrapper> + <Description> + <T9n t={matchPackage.originalObject.sub.lexic1} /> + </Description> + <Description> + {firstDescription} + </Description> + <Description> + <T9n t={matchPackage.originalObject.sub.lexic3} /> + </Description> + <PriceBlock> + <PriceTextWrapper> + <PriceTextTop t={priceTextTopLexic} /> + <PriceTextBottom t={priceTextBottomLexic} /> + </PriceTextWrapper> + <Price + amount={matchPackage.originalObject.price} + currency={matchPackage.currency} + perPeriod={`per_${SubscriptionType.Month}`} + /> + </PriceBlock> + </Wrapper> + ) +} diff --git a/src/features/BuyMatchPopup/components/SinglePackage/styled.tsx b/src/features/BuyMatchPopup/components/SinglePackage/styled.tsx new file mode 100644 index 00000000..cebb3bc1 --- /dev/null +++ b/src/features/BuyMatchPopup/components/SinglePackage/styled.tsx @@ -0,0 +1,100 @@ +import styled from 'styled-components/macro' + +import { + PriceAmount, + PriceDetails, + Period, + Divider, + Currency, + PerPeriod, +} from 'features/Price/styled' +import { T9n } from 'features/T9n' + +export const Description = styled.p` + margin-bottom: 20px; + line-height: 20px; + font-size: 18px; + font-weight: 400; + + :empty, :has(span:empty) { + display: none; + } + + :last-of-type { + margin-bottom: 0; + } + + :first-child { + font-size: 20px; + font-weight: 700; + } +` + +export const PriceBlock = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + margin-top: 60px; +` + +export const PriceTextWrapper = styled.div` + display: flex; + flex-direction: column; + white-space: initial; +` + +export const PriceTextTop = styled(T9n)` + margin-bottom: 2px; + font-weight: 600; + font-style: italic; + font-size: 16px; + letter-spacing: 0.03em; +` + +export const PriceTextBottom = styled(T9n)` + font-weight: 400; + font-size: 16px; + font-style: italic; + letter-spacing: 0.03em; +` + +export const Wrapper = styled.div` + position: relative; + display: flex; + flex-direction: column; + height: calc(100% - 100px); + + ${PriceDetails} { + font-size: 50px; + } + + ${PriceAmount} { + font-size: 50px; + font-weight: 600; + } + + ${Divider} { + width: 5px; + height: 45px; + margin-right: 3px; + translate: 0 7px; + } + + ${Currency} { + margin-right: 10px; + translate: none; + font-size: 50px; + font-weight: 600; + } + + ${Period} { + font-size: 16px; + } + + ${PerPeriod} { + display: inline-block; + translate: 0 4px; + } +` + diff --git a/src/features/BuyMatchPopup/index.tsx b/src/features/BuyMatchPopup/index.tsx index a0932bc6..8d473cd9 100644 --- a/src/features/BuyMatchPopup/index.tsx +++ b/src/features/BuyMatchPopup/index.tsx @@ -1,8 +1,11 @@ +import { useHistory } from 'react-router-dom' + +import { isMobileDevice } from 'config' + import { CardStep } from './components/CardStep' import { ErrorStep } from './components/ErrorStep' import { SuccessStep } from './components/SuccessStep' import { PackageSelectionStep } from './components/PackageSelectionStep' -import { SelectSubscriptionStep } from './components/SelectSubscription' import { Steps } from './types' import { Modal } from './styled' @@ -16,7 +19,6 @@ const components = { [Steps.CardSelection]: CardStep, [Steps.Success]: SuccessStep, [Steps.Error]: ErrorStep, - [Steps.SelectSubscription]: SelectSubscriptionStep, } export const BuyMatchPopup = () => { @@ -24,9 +26,12 @@ export const BuyMatchPopup = () => { close, currentStep, isPopupOpen, + matchPackages, } = useBuyMatchPopupStore() - if (!isPopupOpen || !currentStep) return null + const history = useHistory() + + if (!isPopupOpen || !currentStep || matchPackages.length === 0) return null const Step = components[currentStep] @@ -37,10 +42,17 @@ export const BuyMatchPopup = () => { } } + if (isMobileDevice && currentStep === Steps.SelectPackage) return <Step /> + + const handleClose = () => { + close() + isMobileDevice && currentStep !== Steps.Success && history.goBack() + } + return ( <Modal - isOpen={Boolean(1)} - close={close} + isOpen + close={handleClose} withCloseButton={false} > <Step /> diff --git a/src/features/BuyMatchPopup/store/helpers.tsx b/src/features/BuyMatchPopup/store/helpers.tsx index 33480d7d..2b7419ca 100644 --- a/src/features/BuyMatchPopup/store/helpers.tsx +++ b/src/features/BuyMatchPopup/store/helpers.tsx @@ -34,6 +34,23 @@ type SubscriptionArgs = { suffix: string, } +const getOrder = (id: string, packageId: number) => { + const ordersMap = { + [`all ${packageId} month`]: 2, + [`all ${packageId} year`]: 3, + [`team1 ${packageId} month`]: 4, + [`team2 ${packageId} month`]: 5, + [`team1 ${packageId} year`]: 6, + [`team2 ${packageId} year`]: 7, + [`team_home ${packageId} year`]: 8, + [`team_home ${packageId} month`]: 9, + [`team_away ${packageId} year`]: 10, + [`team_away ${packageId} month`]: 11, + } + + return ordersMap[id] +} + const transformPackage = ({ match, season, @@ -49,25 +66,27 @@ const transformPackage = ({ nameObj: match[passNameKeys[key]], suffix, }) - const description: Desciption = isLeaguePass - ? { - lexic: subscription.lexic3, - values: {}, - } - : { - lexic: descriptionLexics[key], - values: { - season: season.name, - team: teamName, - }, - } + const description: Desciption = { + lexic: descriptionLexics[key], + values: { + season: season.name, + team: teamName, + }, + } const nameLexic = isLeaguePass ? subscription.lexic2 : null + const subscriptionPackageId = subscriptionPackage.sub.id + const id = `${key} ${subscriptionPackageId} ${type}` + const order = getOrder(id, subscriptionPackageId) return { currency: currencySymbols[subscriptionPackage.currency_iso], description, - id: `${key} ${subscriptionPackage.sub.id}`, + id, + isMainPackage: subscriptionPackage.sub.id === 21, + isMonthSubscription: type === 'month', + match, name: teamName, nameLexic, + order, originalObject: subscriptionPackage, pass: passLexics[key], price: subscriptionPackage.price, @@ -111,7 +130,9 @@ const transformPackages = ({ values: {}, }, id: '0', + match, name: `${team1Name} - ${team2Name}`, + order: 1, originalObject: payPerView, pass: 'pass_match_access', price: payPerView.price, diff --git a/src/features/BuyMatchPopup/store/hooks/index.tsx b/src/features/BuyMatchPopup/store/hooks/index.tsx index 2f16a9d7..a385d21c 100644 --- a/src/features/BuyMatchPopup/store/hooks/index.tsx +++ b/src/features/BuyMatchPopup/store/hooks/index.tsx @@ -3,6 +3,7 @@ import { useCallback, useState, useEffect, + useRef, } from 'react' import { useHistory } from 'react-router-dom' @@ -27,11 +28,8 @@ import type { OnFailedPaymentActionData } from 'requests/buySubscription' import { dataForPayHighlights } from 'pages/HighlightsPage/storeHighlightsAtoms' import { useCardsStore } from 'features/CardsStore' -import { - Match, - Steps, - SubscriptionType, -} from 'features/BuyMatchPopup/types' +import type { Match, MatchPackage } from 'features/BuyMatchPopup/types' +import { Steps, SubscriptionType } from 'features/BuyMatchPopup/types' import { getProfileUrl } from 'features/ProfileLink/helpers' import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction' @@ -65,6 +63,7 @@ export const useBuyMatchPopup = () => { isOpen, open, } = useToggle() + const matchPackageRef = useRef<MatchPackage | null>(null) const setDataHighlights = useSetRecoilState(dataForPayHighlights) const goTo = useCallback( @@ -85,10 +84,10 @@ export const useBuyMatchPopup = () => { const { fetchSubscriptions, + matchPackages, matchSubscriptions, onPackageSelect, onPeriodSelect, - onSubscriptionSelect, resetSubscriptions, selectedPackage, selectedPeriod, @@ -97,24 +96,22 @@ export const useBuyMatchPopup = () => { subscriptions, } = useSubscriptions() - const onNext = (e: MouseEvent<HTMLButtonElement>) => goTo(Steps.SelectPackage, e) - const openPopup = useCallback((matchData: Match) => { setMatch(matchData) open() - setSteps([]) + setSteps([Steps.SelectPackage]) }, [open]) useEffect(() => { - if (isEmpty(matchSubscriptions)) return + if (isEmpty(matchPackages)) return - if (size(matchSubscriptions) === 1) { - setSteps([Steps.SelectPackage]) - onSubscriptionSelect(matchSubscriptions[0]) - } else { - setSteps([Steps.SelectSubscription]) + setSteps([Steps.SelectPackage]) + + if (size(matchPackages) === 1) { + onPackageSelect(matchPackages[0]) } - }, [matchSubscriptions, onSubscriptionSelect]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [matchPackages]) const closePopup = () => { close() @@ -122,6 +119,7 @@ export const useBuyMatchPopup = () => { setError('') resetSubscriptions() setSelectedPackage(null) + matchPackageRef.current = null if (isSubscribePopup()) { history.replace({ search: '' }) @@ -155,8 +153,10 @@ export const useBuyMatchPopup = () => { } const onConfirmationSuccess = ({ id }: PaymentIntent) => { - if (!selectedPackage) return - const item = selectedPackage?.originalObject + const subscriptionPackage = matchPackageRef.current || selectedPackage + + if (!subscriptionPackage) return + const item = subscriptionPackage?.originalObject notifySuccessfulSubscription({ item, paymentIntentId: id }) .then(onSuccessfulSubscription, goToError) } @@ -191,10 +191,12 @@ export const useBuyMatchPopup = () => { } const subscribeToMatch = () => { - if (!selectedPackage || !defaultCard) return + const subscriptionPackage = matchPackageRef.current || selectedPackage - const item = selectedPackage.originalObject - const buy = requests[selectedPackage.type] + if (!subscriptionPackage || !defaultCard) return + + const item = subscriptionPackage.originalObject + const buy = requests[subscriptionPackage.type] setLoader(true) buy({ cardId: defaultCard.id, item }).then( onSuccessfulSubscription, @@ -202,8 +204,13 @@ export const useBuyMatchPopup = () => { ) } - const onBuyClick = (e?: MouseEvent<HTMLButtonElement>) => { - e?.stopPropagation() + const onBuyClick = (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => { + e.stopPropagation() + + if (matchPackage) { + matchPackageRef.current = matchPackage + } + if (defaultCard) { subscribeToMatch() } else { @@ -212,10 +219,10 @@ export const useBuyMatchPopup = () => { } useEffect(() => { - if (match) { + if (match && isOpen) { fetchSubscriptions(match) } - }, [match, fetchSubscriptions]) + }, [match, isOpen, fetchSubscriptions]) return { close: closePopup, @@ -229,14 +236,13 @@ export const useBuyMatchPopup = () => { lastSelectedPackage, loader, match, + matchPackages, matchSubscriptions, onBuyClick, onConfirmationSuccess, onConfirmationSuccessHiglights, - onNext, onPackageSelect, onPeriodSelect, - onSubscriptionSelect, onSuccessfulHighlights, onUnsuccessfulSubscription, open: openPopup, @@ -246,6 +252,7 @@ export const useBuyMatchPopup = () => { selectedSubscription, setDisabledBuyBtn, setLastSelectedPackage, + setSelectedPackage, setShowClearBtn, showClearBtn, subscriptions, diff --git a/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx b/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx index 7855c7a2..9fce37d2 100644 --- a/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx +++ b/src/features/BuyMatchPopup/store/hooks/useSubscriptions.tsx @@ -2,12 +2,14 @@ import { useState, useCallback, useEffect, + useMemo, } from 'react' import find from 'lodash/find' import isEmpty from 'lodash/isEmpty' import first from 'lodash/first' import size from 'lodash/size' +import flatMap from 'lodash/flatMap' import { getSubscriptions } from 'requests/getSubscriptions' import { getSelectedSubscriptions } from 'requests/getSelectedSubscriptions' @@ -61,6 +63,12 @@ export const useSubscriptions = () => { setSelectedPackage, ] = useState<MatchPackage | null>(null) + const matchPackages: Array<MatchPackage> = useMemo(() => [ + ...flatMap(matchSubscriptions, 'packages.year'), + ...flatMap(matchSubscriptions, 'packages.month'), + ...flatMap(matchSubscriptions, 'packages.pay_per_view'), + ], [matchSubscriptions]) + const fetchSubscriptions = useCallback(async (match: Match) => { let subscriptions if (checkUrlParams('id') && checkUrlParams('subscribe')) { @@ -117,6 +125,7 @@ export const useSubscriptions = () => { return { fetchSubscriptions, + matchPackages, matchSubscriptions, onPackageSelect, onPeriodSelect: setSelectedPeriod, diff --git a/src/features/BuyMatchPopup/styled.tsx b/src/features/BuyMatchPopup/styled.tsx index 5b95c481..2d86e5a6 100644 --- a/src/features/BuyMatchPopup/styled.tsx +++ b/src/features/BuyMatchPopup/styled.tsx @@ -76,14 +76,15 @@ export const Button = styled(ButtonSolid)` type WrapperProps = { height?: number, + padding?: string, width?: number, } export const Wrapper = styled.div<WrapperProps>` position: relative; height: ${({ height }) => (height ? `${height}px` : 'auto')}; - width: ${({ width }) => (width ? `${width}px` : '830px')}; - padding: 40px 10px; + width: ${({ width }) => (width ? `${width}px` : 'auto')}; + padding: ${({ padding }) => (padding || '40px 10px')}; @media (max-width: 750px){ width: 100%; @@ -188,7 +189,20 @@ export const ButtonPrevious = styled.button` top: 30px; left: 20px; cursor: pointer; + + ${isMobileDevice + ? css` + top: 15px; + left: 10px; + + span[direction='left'] { + padding: 3px; + border-color: ${({ theme }) => theme.colors.white}; + } + ` + : ''} ` + export const ButtonClear = styled(ButtonOutline)` border: 1px solid #FFFFFF; border-radius: 4px; diff --git a/src/features/BuyMatchPopup/types.tsx b/src/features/BuyMatchPopup/types.tsx index 370fc8cb..e8145b99 100644 --- a/src/features/BuyMatchPopup/types.tsx +++ b/src/features/BuyMatchPopup/types.tsx @@ -8,7 +8,6 @@ export enum Steps { CardSelection = 'CardSelection', Error = 'Error', SelectPackage = 'SelectPackage', - SelectSubscription = 'SelectSubscription', Success = 'Success', } @@ -27,8 +26,12 @@ export type MatchPackage = { currency: string, description: Desciption, id: string, + isMainPackage?: boolean, + isMonthSubscription?: boolean, + match: Match, name: string, nameLexic?: LexicsId | null, + order: number, originalObject: SubscriptionResponse, pass: string, price: number, diff --git a/src/features/HomePage/hooks.tsx b/src/features/HomePage/hooks.tsx index 4fb99efc..8a7af3d8 100644 --- a/src/features/HomePage/hooks.tsx +++ b/src/features/HomePage/hooks.tsx @@ -8,11 +8,17 @@ import { endOfMonth, format } from 'date-fns' import { useSetRecoilState } from 'recoil' -import { client } from 'config/clients' +import { + isMobileDevice, + client, + COUNTRY, +} from 'config' + import { ClientNames } from 'config/clients/types' import { useAuthStore } from 'features/AuthStore' import { useHeaderFiltersStore } from 'features/HeaderFilters' +import { useBuyMatchPopupStore } from 'features/BuyMatchPopup' import { getHomeMatches, @@ -23,8 +29,6 @@ import { import { setLocalStorageItem } from 'helpers' -import { COUNTRY } from 'config' - import { isSportFilterShownAtom } from './Atoms/HomePageAtoms' import { useAds } from '../../components/Ads/hooks' @@ -50,6 +54,8 @@ export const useHomePage = () => { selectedMonthModeDate, } = useHeaderFiltersStore() + const { close: closeByMatchPopup, isPopupOpen } = useBuyMatchPopupStore() + const [isOpenDownload, setIsOpenDownload] = useState(false) const [isShowConfirmPopup, setIsShowConfirmPopup] = useState(false) const setIsSportFilterShown = useSetRecoilState(isSportFilterShownAtom) @@ -120,6 +126,11 @@ export const useHomePage = () => { } }, [setIsSportFilterShown]) + useEffect(() => { + isMobileDevice && isPopupOpen && closeByMatchPopup() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPopupOpen]) + return { ads, fetchMatches, diff --git a/src/features/HomePage/index.tsx b/src/features/HomePage/index.tsx index 49eec031..5b00d8d2 100644 --- a/src/features/HomePage/index.tsx +++ b/src/features/HomePage/index.tsx @@ -14,6 +14,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 +75,7 @@ const Home = () => { const HomePage = () => ( <HeaderFiltersStore> <Home /> + <BuyMatchPopup /> </HeaderFiltersStore> ) diff --git a/src/features/MatchCard/hooks.tsx b/src/features/MatchCard/hooks.tsx index 2083e3af..696d3272 100644 --- a/src/features/MatchCard/hooks.tsx +++ b/src/features/MatchCard/hooks.tsx @@ -4,7 +4,11 @@ import { useHistory } from 'react-router-dom' import includes from 'lodash/includes' -import { PAGES, ProfileTypes } from 'config' +import { + isMobileDevice, + PAGES, + ProfileTypes, +} from 'config' import type { Match } from 'features/Matches' import { useMatchPopupStore } from 'features/MatchPopup' @@ -12,7 +16,7 @@ import { useBuyMatchPopupStore } from 'features/BuyMatchPopup' import { useAuthStore } from 'features/AuthStore' import { getProfileUrl } from 'features/ProfileLink/helpers' import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction' -import { checkPage } from 'helpers/checkPage' +import { checkPage } from 'helpers' export const useCard = (match: Match) => { const { openMatchPopup } = useMatchPopupStore() @@ -39,6 +43,7 @@ export const useCard = (match: Match) => { break case MatchAccess.CanBuyMatch: openBuyMatchPopup(match) + isMobileDevice && history.push(PAGES.subscriptions) break case MatchAccess.ViewMatchPopup: openMatchPopup(match) @@ -56,6 +61,7 @@ export const useCard = (match: Match) => { openMatchPopup, openBuyMatchPopup, redirectToMatchPage, + history, ]) const onKeyPress = useCallback((e: KeyboardEvent<HTMLLIElement>) => { diff --git a/src/features/MatchPage/components/SubscriptionGuard/index.tsx b/src/features/MatchPage/components/SubscriptionGuard/index.tsx index c9cb51ac..4f341705 100644 --- a/src/features/MatchPage/components/SubscriptionGuard/index.tsx +++ b/src/features/MatchPage/components/SubscriptionGuard/index.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import { Fragment, useEffect } from 'react' import { useHistory } from 'react-router-dom' -import { PAGES } from 'config' +import { isMobileDevice, PAGES } from 'config' import { usePageParams } from 'hooks' @@ -49,6 +49,7 @@ export const SubscriptionGuard = ({ children }: Props) => { sportType, }) openBuyMatchPopup(profile) + isMobileDevice && history.replace(PAGES.subscriptions) } if (match?.access === MatchAccess.RedirectToProfile) { @@ -70,6 +71,7 @@ export const SubscriptionGuard = ({ children }: Props) => { sportType, user, match?.access, + history, ]) return ( diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index f8da4745..0723372e 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -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 /> + <BuyMatchPopup /> </TourProvider> </MatchPageStore> ) diff --git a/src/features/PaymentPeriodTabs/helpers.tsx b/src/features/PaymentPeriodTabs/helpers.tsx deleted file mode 100644 index e8ade685..00000000 --- a/src/features/PaymentPeriodTabs/helpers.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import omitBy from 'lodash/omitBy' -import isEmpty from 'lodash/isEmpty' -import map from 'lodash/map' - -import type { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types' - -type PaymentTab = { - tabLexic: string, - type: SubscriptionType, -} - -const getTabLexic = (type: string) => { - switch (type) { - case 'month': - return 'for_month' - case 'year': - return 'for_year' - case 'pay_per_view': - return 'for_view' - default: - return '' - } -} - -export const getCorrectPaymentTabs = (matchSubscriptions: MatchSubscription) => { - const matchSubscriptionsWithValues = omitBy(matchSubscriptions.packages, isEmpty) - return map(matchSubscriptionsWithValues, (matchSubscription, key) => ({ - tabLexic: getTabLexic(key), - type: key, - })) as Array<PaymentTab> -} diff --git a/src/features/PaymentPeriodTabs/index.tsx b/src/features/PaymentPeriodTabs/index.tsx deleted file mode 100644 index 9e4c7beb..00000000 --- a/src/features/PaymentPeriodTabs/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useMemo } from 'react' - -import map from 'lodash/map' -import size from 'lodash/size' -import styled, { css } from 'styled-components/macro' - -import { isMobileDevice } from 'config/userAgent' - -import type { MatchSubscription, SubscriptionType } from 'features/BuyMatchPopup/types' -import { T9n } from 'features/T9n' - -import { getCorrectPaymentTabs } from './helpers' - -type ListProps = { - countSubscriptions: number, -} -const List = styled.ul<ListProps>` - display: flex; - min-width: 395px; - justify-content: ${({ countSubscriptions }) => (countSubscriptions === 1 - ? 'center' - : 'space-between')}; - ${isMobileDevice - ? css` - min-width: 90%; - width: 90%; - position: relative; - justify-content: center; - @media screen and (orientation: landscape){ - margin-top: -5px; - width: 50%; - min-width: 50%; - } - ` - : ''}; -` - -type ItemProps = { - active?: boolean, -} - -const Item = styled.li<ItemProps>` - position: relative; - font-weight: 600; - font-size: 16px; - line-height: 47px; - display: flex; - align-items: center; - justify-content: center; - color: rgba(255, 255, 255, 0.5); - cursor: pointer; - transition: color 0.3s; - - ::after { - transition: background-color 0.3s; - position: absolute; - content: ''; - bottom: 0px; - width: 130px; - height: 3px; - } - - ${({ active }) => ( - active - ? css` - color: #fff; - ::after { - background-color: #fff; - } - ` - : '' - )} - ${isMobileDevice - ? css` - font-size: 10px; - width: 33%; - white-space: nowrap; - ::after { - height: 2px; - } - ` - : ''}; -` - -type Props = { - className?: string, - onPeriodSelect: (period: SubscriptionType) => void, - selectedPeriod: SubscriptionType, - selectedSubscription: MatchSubscription, -} - -export const PaymentPeriodTabs = ({ - className, - onPeriodSelect, - selectedPeriod, - selectedSubscription, -}: Props) => { - const matchSubscriptionsWithValues = useMemo(() => ( - getCorrectPaymentTabs(selectedSubscription) - ), [selectedSubscription]) - - return ( - <List className={className} countSubscriptions={size(matchSubscriptionsWithValues)}> - {map(matchSubscriptionsWithValues, ({ tabLexic, type }) => ( - <Item - key={type} - active={selectedPeriod === type} - onClick={() => onPeriodSelect(type)} - > - <T9n t={tabLexic} /> - </Item> - ))} - </List> - ) -} diff --git a/src/features/PlayerPage/hooks.tsx b/src/features/PlayerPage/hooks.tsx index eb56f013..cc35e791 100644 --- a/src/features/PlayerPage/hooks.tsx +++ b/src/features/PlayerPage/hooks.tsx @@ -4,9 +4,11 @@ import { useCallback, } from 'react' -import { usePageParams } from 'hooks/usePageParams' +import { isMobileDevice } from 'config' -import type { PlayerProfile } from 'requests/getPlayerInfo' +import { usePageParams } from 'hooks' + +import type { PlayerProfile } from 'requests' import { getPlayerInfo, getPlayerMatches } from 'requests' import { openSubscribePopup } from 'helpers' @@ -17,7 +19,11 @@ import { useBuyMatchPopupStore } from '../BuyMatchPopup' export const usePlayerPage = () => { const [playerProfile, setPlayerProfile] = useState<PlayerProfile>(null) const { profileId: playerId, sportType } = usePageParams() - const { open: openBuyMatchPopup } = useBuyMatchPopupStore() + const { + close: closeByMatchPopup, + isPopupOpen, + open: openBuyMatchPopup, + } = useBuyMatchPopupStore() const { firstname_eng = '', @@ -47,6 +53,11 @@ export const usePlayerPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + useEffect(() => { + isMobileDevice && isPopupOpen && closeByMatchPopup() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPopupOpen]) + const fetchMatches = useCallback( (limit: number, offset: number) => getPlayerMatches({ limit, diff --git a/src/features/PlayerPage/index.tsx b/src/features/PlayerPage/index.tsx index 56004820..a48f8ce5 100644 --- a/src/features/PlayerPage/index.tsx +++ b/src/features/PlayerPage/index.tsx @@ -9,6 +9,7 @@ import { Main, Content, } from 'features/PageLayout' +import { BuyMatchPopup } from 'features/BuyMatchPopup' import { usePlayerPage } from './hooks' @@ -31,6 +32,7 @@ const PlayerPage = () => { <Matches fetch={fetchMatches} /> </Content> </Main> + <BuyMatchPopup /> </PageWrapper> ) } diff --git a/src/features/Price/components/BasePrice/index.tsx b/src/features/Price/components/BasePrice/index.tsx deleted file mode 100644 index 4080f83a..00000000 --- a/src/features/Price/components/BasePrice/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { currencySymbols } from 'config' - -import { T9n } from 'features/T9n' - -import { - Prefix, - PriceAmount, - PriceDetails, - PriceWrapper, - Currency, - Period, -} from '../../styled' - -type Props = { - amount: number, - className?: string, - currency?: string, - isFrom?: boolean, - perPeriod?: string | null, -} - -export const BasePrice = ({ - amount, - className, - currency = currencySymbols.RUB, - isFrom, - perPeriod, -}: Props) => ( - <PriceWrapper className={className}> - { - isFrom - ? ( - <Prefix> - <T9n t='from_price' /> - </Prefix> - ) - : '' - } - <PriceAmount>{amount}</PriceAmount> - <PriceDetails> - <Currency>{currency}</Currency> - { - perPeriod && ( - <Period> - / <T9n t={perPeriod} /> - </Period> - ) - } - </PriceDetails> - </PriceWrapper> -) diff --git a/src/features/Price/components/BrazilianPrice/index.tsx b/src/features/Price/components/BrazilianPrice/index.tsx deleted file mode 100644 index d62bb6b0..00000000 --- a/src/features/Price/components/BrazilianPrice/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { currencySymbols } from 'config' - -import { T9n } from 'features/T9n' - -import { - Prefix, - PriceDetails, - PriceWrapper, - Currency, - Period, -} from '../../styled' -import { PriceAmount } from './styled' - -type Props = { - amount: number, - className?: string, - currency?: string, - isFrom?: boolean, - perPeriod?: string | null, -} - -export const BrazilianPrice = ({ - amount, - className, - currency = currencySymbols.BRL, - isFrom, - perPeriod, -}: Props) => ( - <PriceWrapper className={className}> - { - isFrom - ? ( - <Prefix> - <T9n t='from_price' /> - </Prefix> - ) - : '' - } - <PriceDetails> - <Currency>{currency}</Currency> - <PriceAmount>{amount.toFixed(2).replace('.', ',')}</PriceAmount> - { - perPeriod && ( - <Period> - / <T9n t={perPeriod} /> - </Period> - ) - } - </PriceDetails> - </PriceWrapper> -) diff --git a/src/features/Price/components/BrazilianPrice/styled.tsx b/src/features/Price/components/BrazilianPrice/styled.tsx deleted file mode 100644 index 69bbe934..00000000 --- a/src/features/Price/components/BrazilianPrice/styled.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import styled from 'styled-components' -import { PriceAmount as BasePriceAmount } from '../../styled' - -export const PriceAmount = styled(BasePriceAmount)` - margin-right: 4px; -` diff --git a/src/features/Price/index.tsx b/src/features/Price/index.tsx index 1c1d85d5..25971f9f 100644 --- a/src/features/Price/index.tsx +++ b/src/features/Price/index.tsx @@ -1,48 +1,39 @@ import { currencySymbols } from 'config' -import { useMemo } from 'react' -import { BasePrice } from './components/BasePrice' -import { BrazilianPrice } from './components/BrazilianPrice' +import { + PriceAmount, + PriceDetails, + PriceWrapper, + Currency, + Period, + Divider, + PerPeriod, +} from './styled' -export type PriceProps = { +type Props = { amount: number, className?: string, currency?: string, - isFrom?: boolean, perPeriod?: string | null, } -export const Price: React.FC<PriceProps> = ({ +export const Price = ({ amount, className, - currency, - isFrom, + currency = currencySymbols.RUB, perPeriod, -}) => { - const priceContent = useMemo(() => { - switch (currency) { - case currencySymbols.BRL: - return ( - <BrazilianPrice - amount={amount} - className={className} - currency={currency} - isFrom={isFrom} - perPeriod={perPeriod} - /> +}: Props) => ( + <PriceWrapper className={className}> + <PriceAmount>{amount}</PriceAmount> + <PriceDetails> + <Currency>{currency}</Currency> + { + perPeriod && ( + <Period> + <Divider /><PerPeriod t={perPeriod} /> + </Period> ) - default: - return ( - <BasePrice - amount={amount} - className={className} - currency={currency} - isFrom={isFrom} - perPeriod={perPeriod} - /> - ) - } - }, [amount, className, currency, isFrom, perPeriod]) - - return priceContent -} + } + </PriceDetails> + </PriceWrapper> +) diff --git a/src/features/Price/styled.tsx b/src/features/Price/styled.tsx index 4b583c49..74fd7831 100644 --- a/src/features/Price/styled.tsx +++ b/src/features/Price/styled.tsx @@ -1,62 +1,48 @@ -import styled, { css } from 'styled-components/macro' +import styled from 'styled-components/macro' -import { isMobileDevice } from 'config/userAgent' -import { devices } from 'config/devices' +import { isMobileDevice } from 'config' + +import { T9n } from 'features/T9n' export const PriceWrapper = styled.div` display: flex; align-items: flex-start; - - @media ${devices.tablet} { - justify-content: center; - } - ${isMobileDevice - ? css` - min-width: 80px; - ` - : ''}; ` export const PriceAmount = styled.span` font-style: normal; - font-weight: 600; - font-size: 48px; - line-height: 40px; + font-weight: 400; + font-size: 32px; color: ${({ theme: { colors } }) => colors.white}; - - @media ${devices.tablet} { - font-size: 36px; - } ` export const PriceDetails = styled.span` + display: flex; margin-left: 5px; font-style: normal; font-weight: normal; - font-size: 18px; - line-height: 21px; + font-size: 30px; color: ${({ theme: { colors } }) => colors.white}; - text-transform: uppercase; - - @media ${devices.tablet} { - font-size: 11px; - } ` export const Currency = styled.span` text-transform: uppercase; - margin-right: 4px; + font-size: ${isMobileDevice ? 8 : 10}px; ` export const Period = styled.span` - text-transform: uppercase; + font-style: italic; + font-weight: 400; + font-size: ${isMobileDevice ? 8 : 10}px; ` -export const Prefix = styled.span` - padding-top: 5px; - text-transform: lowercase; - line-height: 40px; - font-size: 12px; - font-weight: normal; - margin-right: 5px; +export const Divider = styled.div` + display: inline-block; + width: 0.5px; + height: 8px; + margin: 0 2px -1px 4px; + transform: skew(-20deg, 0); + background-color: ${({ theme: { colors } }) => colors.white}; ` + +export const PerPeriod = styled(T9n)`` diff --git a/src/features/TeamPage/hooks.tsx b/src/features/TeamPage/hooks.tsx index e8ab92d3..89d00209 100644 --- a/src/features/TeamPage/hooks.tsx +++ b/src/features/TeamPage/hooks.tsx @@ -4,6 +4,8 @@ import { useCallback, } from 'react' +import { isMobileDevice } from 'config' + import type { TeamInfo } from 'requests' import { getTeamInfo, getTeamMatches } from 'requests' @@ -15,7 +17,11 @@ import { useBuyMatchPopupStore } from '../BuyMatchPopup' export const useTeamPage = () => { const [teamProfile, setTeamProfile] = useState<TeamInfo>(null) const { profileId: teamId, sportType } = usePageParams() - const { open: openBuyMatchPopup } = useBuyMatchPopupStore() + const { + close: closeByMatchPopup, + isPopupOpen, + open: openBuyMatchPopup, + } = useBuyMatchPopupStore() useEffect( () => { @@ -36,6 +42,11 @@ export const useTeamPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + useEffect(() => { + isMobileDevice && isPopupOpen && closeByMatchPopup() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPopupOpen]) + const fetchMatches = useCallback( (limit: number, offset: number) => getTeamMatches({ limit, diff --git a/src/features/TeamPage/index.tsx b/src/features/TeamPage/index.tsx index f8b57fef..f453c391 100644 --- a/src/features/TeamPage/index.tsx +++ b/src/features/TeamPage/index.tsx @@ -9,6 +9,7 @@ import { Main, Content, } from 'features/PageLayout' +import { BuyMatchPopup } from 'features/BuyMatchPopup' import { useTeamPage } from './hooks' @@ -35,6 +36,7 @@ const TeamPage = () => { <Matches fetch={fetchMatches} /> </Content> </Main> + <BuyMatchPopup /> </PageWrapper> ) } diff --git a/src/features/TournamentPage/hooks.tsx b/src/features/TournamentPage/hooks.tsx index 2e93603f..340e3fda 100644 --- a/src/features/TournamentPage/hooks.tsx +++ b/src/features/TournamentPage/hooks.tsx @@ -13,7 +13,7 @@ import { import { openSubscribePopup, redirectToUrl } from 'helpers' -import { PAGES } from 'config' +import { PAGES, isMobileDevice } from 'config' import { useName } from 'features/Name' @@ -31,7 +31,11 @@ import { useAuthStore } from '../AuthStore' export const useTournamentPage = () => { const [tournamentProfile, setTournamentProfile] = useState<TournamentInfo>(null) const { profileId: tournamentId, sportType } = usePageParams() - const { open: openBuyMatchPopup } = useBuyMatchPopupStore() + const { + close: closeByMatchPopup, + isPopupOpen, + open: openBuyMatchPopup, + } = useBuyMatchPopupStore() const country = useName(tournamentProfile?.country || {}) const history = useHistory() @@ -96,6 +100,11 @@ export const useTournamentPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + useEffect(() => { + isMobileDevice && isPopupOpen && closeByMatchPopup() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPopupOpen]) + const fetchMatches = useCallback( (limit: number, offset: number) => getTournamentMatches({ limit, diff --git a/src/features/TournamentPage/index.tsx b/src/features/TournamentPage/index.tsx index 662c4793..030896b2 100644 --- a/src/features/TournamentPage/index.tsx +++ b/src/features/TournamentPage/index.tsx @@ -9,6 +9,7 @@ import { Main, Content, } from 'features/PageLayout' +import { BuyMatchPopup } from 'features/BuyMatchPopup' import { useTournamentPage } from './hooks' @@ -39,6 +40,7 @@ const TournamentPage = () => { </Content> </Main> {user && (tournamentId === 131 || tournamentId === 2032) && <FavouriteTeamPopup />} + <BuyMatchPopup /> </PageWrapper> ) } diff --git a/src/features/UserAccount/components/UserSubscriptionsList/index.tsx b/src/features/UserAccount/components/UserSubscriptionsList/index.tsx deleted file mode 100644 index 3a13cc44..00000000 --- a/src/features/UserAccount/components/UserSubscriptionsList/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import isEmpty from 'lodash/isEmpty' -import map from 'lodash/map' - -import type { MatchSubscriptions } from '../PageSubscriptions' -import { InlineButton } from '../../styled' -import { - Wrapper, - SportName, - List, - Item, - Subscription, - InfoWrapper, - Header, - Description, - Price, - SubscriptionEnd, -} from './styled' - -type Props = { - list: MatchSubscriptions, - sport: number, -} - -export const UserSubscriptionsList = ({ list, sport }: Props) => { - if (isEmpty(list)) return null - return ( - <Wrapper> - <SportName sport={sport} /> - <List> - { - map(list, ({ - description, - header, - isActive, - price, - subscription_id, - type, - }) => ( - <Item key={subscription_id}> - <Subscription> - <InfoWrapper> - <Header> - {header} - </Header> - <Description> - {description} - </Description> - </InfoWrapper> - - <Price amount={price} perPeriod={`per_${type}`} /> - <InlineButton color={isActive ? '#eb5757' : '#294FC3'}> - {isActive ? 'Удалить' : 'Восстановить'} - </InlineButton> - </Subscription> - <SubscriptionEnd>Следующее списание 31.02.2020</SubscriptionEnd> - </Item> - )) - } - </List> - </Wrapper> - ) -} diff --git a/src/features/UserAccount/components/UserSubscriptionsList/styled.tsx b/src/features/UserAccount/components/UserSubscriptionsList/styled.tsx deleted file mode 100644 index 2ab664a0..00000000 --- a/src/features/UserAccount/components/UserSubscriptionsList/styled.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import styled, { css } from 'styled-components/macro' - -import { isMobileDevice } from 'config/userAgent' - -import { SportName as SportNameBase } from 'features/Common/SportName' -import { Price as BasePrice } from 'features/Price' -import { PriceAmount, PriceDetails } from 'features/Price/styled' - -import { InlineButton } from '../../styled' - -export const Wrapper = styled.div` - :not(:first-child) { - margin-top: 24px; - } -` - -export const SportName = styled(SportNameBase)` - font-style: normal; - font-weight: 500; - font-size: 18px; - line-height: 22px; - color: rgba(255, 255, 255, 0.6); - ${isMobileDevice - ? css` - font-size: 14px; - ` - : ''} -` - -export const List = styled.ul` - margin-top: 6px; -` - -export const Item = styled.li` - display: flex; - align-items: center; - - ${isMobileDevice - ? css` - display: block; - ` - : ''} -` - -export const Price = styled(BasePrice)` - margin-right: 20px; - - ${PriceAmount} { - font-size: 24px; - line-height: 24px; - font-weight: normal; - ${isMobileDevice - ? css` - font-size: 14px; - ` - : ''} - } - - ${PriceDetails} { - font-weight: 500; - font-size: 12px; - line-height: 18px; - ${isMobileDevice - ? css` - font-size: 7px; - ` - : ''} - } -` - -export const Subscription = styled.div.attrs( - () => ({ - tabIndex: 0, - }), -)` - position: relative; - width: 800px; - height: 70px; - display: flex; - margin: 5px 0; - align-items: center; - justify-content: space-between; - background-color: #3F3F3F; - box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); - border-radius: 2px; - overflow: hidden; - - ${InlineButton} { - width: 133px; - } - - :focus-within, :hover { - ${InlineButton} { - transform: translateX(0); - } - } - ${isMobileDevice - ? css` - height: 86px; - width: 100%; - ` - : ''} -` - -export const InfoWrapper = styled.div` - width: 68%; - display: flex; - padding: 14px 0 14px 20px; - flex-direction: column; - color: #fff; - ${isMobileDevice - ? css` - padding: 10px 0 10px 15px; - ` - : ''} -` - -export const Header = styled.span` - font-style: normal; - font-weight: 600; - font-size: 16px; - line-height: 23px; - text-transform: uppercase; - ${isMobileDevice - ? css` - font-size: 14px; - line-height: 20px; - ` - : ''} -` - -export const Description = styled.p` - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 20px; - text-transform: capitalize; - ${isMobileDevice - ? css` - font-size: 10px; - line-height: 12px; - ` - : ''} -` - -export const SubscriptionEnd = styled.span` - margin-left: 24px; - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 24px; - color: rgba(255, 255, 255, 0.3); - ${isMobileDevice - ? css` - font-size: 10px; - margin-left: 0; - ` - : ''} -` diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index 6ec4e5c7..814a4711 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -16,3 +16,4 @@ export * from './isMatchPage' export * from './languageUrlParam' export * from './bodyScrollLock' export * from './getLocalStorage' +export * from './checkPage' diff --git a/src/pages/SubscriptionsPage/index.tsx b/src/pages/SubscriptionsPage/index.tsx new file mode 100644 index 00000000..c0ecaeba --- /dev/null +++ b/src/pages/SubscriptionsPage/index.tsx @@ -0,0 +1,16 @@ +import { ProfileHeader } from 'features/ProfileHeader' +import { Main } from 'features/PageLayout' +import { BuyMatchPopup } from 'features/BuyMatchPopup' + +import { Wrapper } from './styled' + +const SubscriptionsPage = () => ( + <Wrapper> + <ProfileHeader /> + <Main> + <BuyMatchPopup /> + </Main> + </Wrapper> +) + +export default SubscriptionsPage diff --git a/src/pages/SubscriptionsPage/styled.tsx b/src/pages/SubscriptionsPage/styled.tsx new file mode 100644 index 00000000..c655b166 --- /dev/null +++ b/src/pages/SubscriptionsPage/styled.tsx @@ -0,0 +1,31 @@ +import styled from 'styled-components/macro' + +import { PageWrapper } from 'features/PageLayout' +import { Form } from 'features/Search/styled' +import { MenuList } from 'features/Menu/styled' +import { HeaderStyled } from 'features/ProfileHeader/styled' +import { Logo } from 'features/Logo' +import { Body, Wrapper as ContentWrapper } from 'features/BuyMatchPopup/styled' + +export const Wrapper = styled(PageWrapper)` + ${Form}, ${MenuList} { + display: none; + } + + ${HeaderStyled} { + height: 60px; + } + + ${Logo} { + top: 7.5px; + } + + ${Body} { + margin-top: 18px; + padding: 0; + } + + ${ContentWrapper} { + padding: 20px 16px; + } +` diff --git a/src/requests/getSubscriptions.tsx b/src/requests/getSubscriptions.tsx index c4b1edbf..65c93a18 100644 --- a/src/requests/getSubscriptions.tsx +++ b/src/requests/getSubscriptions.tsx @@ -50,6 +50,9 @@ type Sub = { active_to: string, currency_id: number, id: number, + lexic1: number, + lexic2: number | null, + lexic3: number, match_id?: number, option: number, price: number,