diff --git a/.drone.yml b/.drone.yml index 0df88fec..26b31dc5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -658,9 +658,28 @@ trigger: - refs/heads/test-auth steps: - - name: deploy script + - name: npm-install + image: node:16-alpine + environment: + REACT_APP_STRIPE_PK: + from_secret: REACT_APP_STRIPE_PK + commands: + - apk add --no-cache make + - npm install --legacy-peer-deps + + - name: make-auth image: node:16-alpine + environment: + REACT_APP_STRIPE_PK: + from_secret: REACT_APP_STRIPE_PK + commands: + - apk add --no-cache make + - make auth-build + depends_on: + - npm-install + - name: deploy-S3-auth + image: amazon/aws-cli:latest environment: AWS_ACCESS_KEY_ID: from_secret: AWS_ACCESS_KEY_ID @@ -668,23 +687,12 @@ steps: from_secret: AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION: from_secret: AWS_DEFAULT_REGION - REACT_APP_STRIPE_PK: - from_secret: REACT_APP_STRIPE_PK_TEST - SSH_KEY_AUTH_TEST: - from_secret: SSH_KEY_AUTH_TEST - + AWS_MAX_ATTEMPTS: 10 commands: - - apk add --no-cache aws-cli bash git openssh-client make rsync - - npm install --legacy-peer-deps - - make auth-build - - eval $(ssh-agent -s) - - echo -n "$SSH_KEY_AUTH_TEST" | tr -d '\r' | ssh-add - - - mkdir -p ~/.ssh && chmod 700 ~/.ssh - - ssh-keyscan auth.test.insports.tv >> ~/.ssh/known_hosts - - rsync -v -r -C build_auth/ ubuntu@auth.test.insports.tv:/home/ubuntu/ott-auth/src/frontend/ - - rsync -v -r -C build_auth/clients/* ubuntu@auth.test.insports.tv:/home/ubuntu/ott-auth/src/frontend/templates - - aws s3 sync build_auth s3://auth-insports-test --delete + - aws s3 sync build_auth s3://insports-auth-test --delete - aws cloudfront create-invalidation --distribution-id E10YI3RFOZZDLZ --paths "/*" + depends_on: + - make-auth --- diff --git a/package.json b/package.json index 8e59a5b2..c023ddac 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@reactour/tour": "^3.3.0", + "@sentry/react": "^7.53.1", "@stripe/react-stripe-js": "^1.4.0", "@stripe/stripe-js": "^1.13.2", "babel-polyfill": "^6.26.0", diff --git a/public/ads.txt b/public/ads.txt new file mode 100644 index 00000000..346e3d7b --- /dev/null +++ b/public/ads.txt @@ -0,0 +1 @@ +google.com, pub-6802442215403184, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/public/images/checkedRadiobutton.png b/public/images/checkedRadiobutton.png new file mode 100644 index 00000000..6ded0e59 Binary files /dev/null and b/public/images/checkedRadiobutton.png differ diff --git a/public/images/downloadIcon.png b/public/images/downloadIcon.png new file mode 100644 index 00000000..48f39d4c Binary files /dev/null and b/public/images/downloadIcon.png differ diff --git a/public/images/radiobutton.png b/public/images/radiobutton.png new file mode 100644 index 00000000..191398a7 Binary files /dev/null and b/public/images/radiobutton.png differ diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx index b3a27e45..4f3ad182 100644 --- a/src/components/ErrorBoundary/index.tsx +++ b/src/components/ErrorBoundary/index.tsx @@ -1,8 +1,8 @@ -// eslint-disable react/destructuring-assignment +import React from 'react' -import { Component } from 'react' +import type{ ReactNode } from 'react' -import type{ ErrorInfo, ReactNode } from 'react' +import * as Sentry from '@sentry/react' import { Error } from '../Error' @@ -10,41 +10,15 @@ interface Props { children?: ReactNode, } -interface State { - hasError: boolean, -} - -class ErrorBoundary extends Component { - // eslint-disable-next-line react/state-in-constructor - public state: State = { - hasError: false, - } - - public static getDerivedStateFromError(_: Error): State { - // Update state so the next render will show the fallback UI. - return { hasError: true } - } - - public componentDidCatch(error: Error, errorInfo: ErrorInfo) { - // eslint-disable-next-line no-console - console.error( - 'Uncaught error:', - error, - errorInfo, - ) - } - - public render() { - const { hasError } = this.state - const { children } = this.props - - return ( - <> - {hasError && } - {children} - - ) - } -} +export const ErrorBoundary = ({ children }: Props) => ( + + {children} + + + )} + > + { children } + +) -export default ErrorBoundary diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index 7830ed5e..5d498ce0 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.tsx @@ -4,6 +4,7 @@ import { highlightsPageLexic } from './highlightsPageLexic' import { mailingsLexics } from './mailings' import { sportsLexic } from './sportsLexic' import { landingLexics } from './landingLexics' +import { matchDownload } from './matchDownload' const matchPopupLexics = { actions: 1020, @@ -221,4 +222,5 @@ export const indexLexics = { ...sportsLexic, ...sportsPopup, ...landingLexics, + ...matchDownload, } diff --git a/src/config/lexics/matchDownload.tsx b/src/config/lexics/matchDownload.tsx new file mode 100644 index 00000000..43f7291a --- /dev/null +++ b/src/config/lexics/matchDownload.tsx @@ -0,0 +1,8 @@ +export const matchDownload = { + choose_what_to_download: 20193, + download_files_for_periods: 20197, + download_full_match: 9435, + download_single_file: 20196, + entire_record: 20195, + in_game_time_only: 20194, +} diff --git a/src/features/App/index.tsx b/src/features/App/index.tsx index af66da71..d65ffd0a 100644 --- a/src/features/App/index.tsx +++ b/src/features/App/index.tsx @@ -23,7 +23,7 @@ import { GlobalStyles } from 'features/GlobalStyles' import { Theme } from 'features/Theme' import { UnavailableText } from 'components/UnavailableText' -import ErrorBoundary from 'components/ErrorBoundary' +import { ErrorBoundary } from 'components/ErrorBoundary' import { AuthenticatedApp } from './AuthenticatedApp' import { useAuthStore } from '../AuthStore' diff --git a/src/features/MatchPage/components/MatchDownloadPopup/index.tsx b/src/features/MatchPage/components/MatchDownloadPopup/index.tsx new file mode 100644 index 00000000..ec97ad86 --- /dev/null +++ b/src/features/MatchPage/components/MatchDownloadPopup/index.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' + +import { T9n } from 'features/T9n' + +import { + Header, + Footer, + Modal, + ScApplyButton, + HeaderTitle, + Wrapper, + DownloadIcon, + FirstTitle, + SecondTitle, + RadioButtonsWrapper, + Input, + Label, +} from './styled' + +type Props = { + closePopup: () => void, + isModalOpen: boolean, +} + +export const MatchDownloadPopup = ({ + closePopup, + isModalOpen, +}: Props) => { + const [selected, setSelected] = useState(true) + + return ( + + +
+ + + + + +
+ + + + +
+ + + + + + +
+
+
+ ) +} diff --git a/src/features/MatchPage/components/MatchDownloadPopup/styled.tsx b/src/features/MatchPage/components/MatchDownloadPopup/styled.tsx new file mode 100644 index 00000000..9ec9dc8a --- /dev/null +++ b/src/features/MatchPage/components/MatchDownloadPopup/styled.tsx @@ -0,0 +1,158 @@ +import styled, { css } from 'styled-components/macro' + +import { isMobileDevice } from 'config' + +import { T9n } from 'features/T9n' +import { ModalWindow, ModalCloseButton } from 'features/Modal/styled' +import { Header as BaseHeader } from 'features/PopupComponents' +import { + ApplyButton, + Modal as BaseModal, +} from 'features/AuthServiceApp/components/RegisterPopup/styled' + +export const Modal = styled(BaseModal)` + + ${ModalWindow} { + width: 577px; + height: 490px; + padding: 80px 90px 65px; + min-height: auto; + + ${ModalCloseButton} { + height: 28px; + width: 28px; + + svg { + width: 20px; + height: 12px; + } + } + + ${isMobileDevice + ? css` + max-width: 95vw; + max-height: 75vh; + top: -3vh; + padding: 60px 14px 20px;` + : ''}; +` + +export const Wrapper = styled.div`` + +export const Header = styled(BaseHeader)` + display: flex; + align-items: center; + height: 100%; +` + +export const DownloadIcon = styled.img` + margin-right: 25px; + + ${isMobileDevice + ? css` + height: 55px; + width: 44px;` + : ''}; +` + +export const HeaderTitle = styled.div` + display: flex; + flex-direction: column; +` + +export const FirstTitle = styled(T9n)` + font-weight: 700; + font-size: 24px; + line-height: 24px; + margin-bottom: 10px; + + ${isMobileDevice + ? css` + font-size: 20px;` + : ''}; +` + +export const SecondTitle = styled(T9n)` + font-weight: 400; + font-size: 20px; + line-height: 28px; + + ${isMobileDevice + ? css` + font-size: 14px; + line-height: 17px;` + : ''}; +` + +export const RadioButtonsWrapper = styled.div` + margin: 25px 0; +` + +export const Label = styled.label` + display: flex; + cursor: pointer; + font-size: 18px; + line-height: 50px; + align-items: center; + + ${isMobileDevice + ? css` + font-size: 14px;` + : ''}; +` + +export const Input = styled.input.attrs(() => ({ + type: 'radio', +}))` + margin: 0 25px 0 0; + appearance: none; + width: 25px; + height: 25px; + cursor: pointer; + background-image: url(/images/${({ defaultChecked }) => ( + defaultChecked + ? 'checkedRadiobutton.png' + : 'radiobutton.png' + )}); + + + ${isMobileDevice + ? css` + margin-right: 10px;` + : ''}; + +` + +export const Footer = styled.div`` + +export const ScApplyButton = styled(ApplyButton)` + width: 400px; + height: 50px; + margin: 0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #3F3F3F; + transition: background-color 0.3s; + + :first-child { + margin-bottom: 20px; + } + + &:hover { + background-color: ${({ theme: { colors } }) => colors.button}; + } + + :disabled { + pointer-events: none; + } + + ${isMobileDevice + ? css` + font-size: 14px; + width: 100%; + height: 50px; + + :first-child { + margin-bottom: 10px; + }` + : ''}; +` + diff --git a/src/features/MatchSidePlaylists/components/MatchDownloadButton/index.tsx b/src/features/MatchSidePlaylists/components/MatchDownloadButton/index.tsx new file mode 100644 index 00000000..2f15f826 --- /dev/null +++ b/src/features/MatchSidePlaylists/components/MatchDownloadButton/index.tsx @@ -0,0 +1,67 @@ +import some from 'lodash/some' + +import type { TeamsAndTournaments } from 'requests' + +import { useToggle } from 'hooks/useToggle' + +import { T9n } from 'features/T9n' +import { useMatchPageStore } from 'features/MatchPage/store' +import { useAuthStore } from 'features/AuthStore' +import { MatchDownloadPopup } from 'features/MatchPage/components/MatchDownloadPopup' + +import { usePageParams } from 'hooks/usePageParams' + +import { Item } from '../MatchPlaylists' +import { DownloadButton, Title } from '../../styled' + +export const MatchDownloadButton = () => { + const { user, userInfo } = useAuthStore() + const { profile } = useMatchPageStore() + const { sportType } = usePageParams() + + const { + close, + isOpen, + open, + } = useToggle() + + const checkDownloadData = ( + checkArray: Array, + id?: number, + ) => some(checkArray, (['c_sport', sportType] && ['id', id])) + + const teams = userInfo?.download.teams + const tournaments = userInfo?.download.tournaments + + const isAvailableTeams = () => { + if (teams) { + return checkDownloadData(teams, profile?.team1.id) + || checkDownloadData(teams, profile?.team2.id) + } + + return null + } + + const isAvailableTournaments = () => { + if (tournaments) return checkDownloadData(tournaments, profile?.tournament.id) + return null + } + + const isNeedDownloadButton = user && (isAvailableTournaments() || isAvailableTeams()) + + if (!isNeedDownloadButton) return null + + return ( + + + + + <T9n t='download_full_match' /> + + + + ) +} diff --git a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx index fcb98356..f9f8766e 100644 --- a/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx @@ -20,6 +20,7 @@ import { useMatchPageStore } from 'features/MatchPage/store' import { useLexicsStore } from 'features/LexicsStore' import { PlayButton } from '../PlayButton' +import { MatchDownloadButton } from '../MatchDownloadButton' export const LIST_INDENT = 30 @@ -34,7 +35,7 @@ const List = styled.ul` margin-bottom: ${LIST_INDENT}px; ` -const Item = styled.li` +export const Item = styled.li` margin-bottom: 12px; width: 100%; height: 36px; @@ -94,6 +95,7 @@ export const MatchPlaylists = forwardRef( )) } + ) }, diff --git a/src/features/MatchSidePlaylists/styled.tsx b/src/features/MatchSidePlaylists/styled.tsx index c43f595d..e4e15c3d 100644 --- a/src/features/MatchSidePlaylists/styled.tsx +++ b/src/features/MatchSidePlaylists/styled.tsx @@ -219,6 +219,21 @@ export const Button = styled.button` }} ` +export const DownloadButton = styled(Button)` + + :hover { + background-color: ${({ theme }) => theme.colors.buttonHover}; + } + + ${({ active }) => (active + ? css` + border: 1px solid #FFFFFF; + border-radius: 2px; + background-color: black; + ` + : '')} +` + export const Title = styled.span` font-weight: 600; font-size: 14px; diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index b70c5f4a..485528dc 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -20,7 +20,11 @@ import values from 'lodash/values' import Hls from 'hls.js' -import { isIOS, KEYBOARD_KEYS } from 'config' +import { + isIOS, + isMobileDevice, + KEYBOARD_KEYS, +} from 'config' import { useObjectState, @@ -601,13 +605,13 @@ export const useVideoPlayer = ({ }, [setPlayerState]) useEffect(() => { - if (ready && videoRef && !isPlayingEpisode) { + if (ready && videoRef && !isMobileDevice) { videoRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', }) } - }, [ready, videoRef, isPlayingEpisode]) + }, [ready, videoRef]) useEffect(() => { setCircleAnimation((state) => ({ diff --git a/src/helpers/callApi/logoutIfUnauthorized.tsx b/src/helpers/callApi/logoutIfUnauthorized.tsx index 18e71881..d9958362 100644 --- a/src/helpers/callApi/logoutIfUnauthorized.tsx +++ b/src/helpers/callApi/logoutIfUnauthorized.tsx @@ -1,5 +1,13 @@ +import * as Sentry from '@sentry/react' + export const logoutIfUnauthorized = async (response: Response) => { /* отключили из-за доступа без авторизации */ + const body = await response.json() + + if (response.status === 400) { + Sentry.captureException(body) + } + if (response.status === 401 || response.status === 403) { window.dispatchEvent(new Event('FORBIDDEN_REQUEST')) } @@ -8,6 +16,5 @@ export const logoutIfUnauthorized = async (response: Response) => { // eslint-disable-next-line no-console console.error(error) - const body = await response.json() return Promise.reject(body) } diff --git a/src/index.tsx b/src/index.tsx index 81041121..8622573d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,11 +5,24 @@ import { } from 'react' import ReactDOM from 'react-dom' -import { isIOS } from 'config/userAgent' +import * as Sentry from '@sentry/react' +import { BrowserTracing } from '@sentry/react' + +import { isIOS, ENV } from 'config' + // import { makeServer } from 'utilits/mirage/Mirage' import * as serviceWorker from './serviceWorker' +if (process.env.NODE_ENV !== 'development') { + Sentry.init({ + dsn: 'https://bbe0cdfb954644ebaf3be16bb472cc3d@sentry.insports.tv/21', + environment: ENV, + integrations: [new BrowserTracing()], + tracesSampleRate: 1.0, + }) +} + export const App = process.env.REACT_APP_TYPE === 'auth-service' ? lazy(() => import('features/AuthServiceApp')) : lazy(() => import('features/App')) diff --git a/src/requests/getUserInfo.tsx b/src/requests/getUserInfo.tsx index e02528b6..78a97b7d 100644 --- a/src/requests/getUserInfo.tsx +++ b/src/requests/getUserInfo.tsx @@ -6,6 +6,13 @@ import { callApi } from 'helpers' const proc = PROCEDURES.get_user_info +export type TeamsAndTournaments = { + c_sport: number, + id: number, + name_eng: string, + name_rus: string, +} + export type UserInfo = { address_line1: string | null, address_line2: string | null, @@ -16,6 +23,11 @@ export type UserInfo = { name_eng: string, name_rus: string, }, + download: { + id?: number, + teams: Array | null, + tournaments: Array | null, + }, email: string, firstname: string | null, has_subscription: boolean,