* feat(ott-1503): connecting events (#464)

* feat(ott-1503): connecting events

* fix(ott-1503): pr fixes

Co-authored-by: boyvanov <boyvanov.sergey@gmail.com>

* feat(ott-1293): add matchpage for unauthenticated users (#465)

* feat(ott-1293): add matchpage for unauthenticated users

* feat(ott-1293): fix pr

* Ott 1553 fix loading indicator in payment (#466)

* fix(#1553): fix arrow loader in page cards

* fix(#1553): delete env file

* feat(#1576): added technical support widget (#468)

Co-authored-by: boyvanov <50294488+boyvanov@users.noreply.github.com>
Co-authored-by: boyvanov <boyvanov.sergey@gmail.com>
Co-authored-by: PolyakovaM <55061222+PolyakovaM@users.noreply.github.com>
Co-authored-by: Andrei Dekterev <57942757+dekterev@users.noreply.github.com>
Co-authored-by: Иван Пиминов <61900450+ivan-piminov@users.noreply.github.com>
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 4 years ago committed by GitHub
parent 88cdcb9d58
commit bb756f4109
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. BIN
      public/images/landing_1.png
  2. BIN
      public/images/landing_2.png
  3. BIN
      public/images/landing_3.png
  4. 13
      public/index.html
  5. 4
      src/config/lexics/indexLexics.tsx
  6. 6
      src/config/lexics/joinMatch.tsx
  7. 2
      src/config/procedures.tsx
  8. 2
      src/features/AddCardForm/components/Form/hooks/index.tsx
  9. 2
      src/features/AddCardForm/styled.tsx
  10. 4
      src/features/App/index.tsx
  11. 2
      src/features/ArrowLoader/styled.tsx
  12. 8
      src/features/AuthStore/hooks/useAuth.tsx
  13. 10
      src/features/JoinMatchPage/helpers.tsx
  14. 48
      src/features/JoinMatchPage/hooks.tsx
  15. 90
      src/features/JoinMatchPage/index.tsx
  16. 258
      src/features/JoinMatchPage/styled.tsx
  17. 3
      src/features/MatchPage/components/FinishedMatch/hooks/index.tsx
  18. 3
      src/features/MatchPage/components/FinishedMatch/hooks/useEpisodes.tsx
  19. 28
      src/features/MatchPage/components/FinishedMatch/hooks/useEvents.tsx
  20. 25
      src/features/MatchPage/components/FinishedMatch/hooks/useEventsLexics.tsx
  21. 2
      src/features/MatchPage/components/FinishedMatch/index.tsx
  22. 8
      src/features/MatchPage/types.tsx
  23. 88
      src/features/MatchSidePlaylists/components/EventButton/index.tsx
  24. 63
      src/features/MatchSidePlaylists/components/EventsList/index.tsx
  25. 119
      src/features/MatchSidePlaylists/components/TabEvents/index.tsx
  26. 143
      src/features/MatchSidePlaylists/components/TabEvents/styled.tsx
  27. 9
      src/features/MatchSidePlaylists/index.tsx
  28. 69
      src/requests/getMatchEvents.tsx
  29. 2
      src/requests/getMatchInfo.tsx
  30. 42
      src/requests/getUnauthenticatedMatch.tsx
  31. 2
      src/requests/index.tsx

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

@ -11,13 +11,13 @@
content="width=device-width, initial-scale=1.0, maximum-scale=1 user-scalable=0" content="width=device-width, initial-scale=1.0, maximum-scale=1 user-scalable=0"
/> />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Live sports streaming platform. Football, basketball, ice hockey and more. <meta name="description" content="Live sports streaming platform. Football, basketball, ice hockey and more.
Access to various player playlists and game highlights. Multiple subscription options. Available across all devices." Access to various player playlists and game highlights. Multiple subscription options. Available across all devices."
/> />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link <link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<script src="https://js.stripe.com/v3" async></script> <script src="https://js.stripe.com/v3" async></script>
@ -27,5 +27,14 @@
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="modal-root"></div> <div id="modal-root"></div>
<div id="root"></div> <div id="root"></div>
<!--Start of Zendesk Chat Script-->
<script type="text/javascript">
window.$zopim||(function(d,s){var z=$zopim=function(c){z._.push(c)},$=z.s=
d.createElement(s),e=d.getElementsByTagName(s)[0];z.set=function(o){z.set.
_.push(o)};z._=[];z.set._=[];$.async=!0;$.setAttribute("charset","utf-8");
$.src="https://v2.zopim.com/?4XmH9AAhZQdZEM8bAELzxRok01q4QXut";z.t=+new Date;$.
type="text/javascript";e.parentNode.insertBefore($,e)})(document,"script");
</script>
<!--End of Zendesk Chat Script-->
</body> </body>
</html> </html>

@ -6,11 +6,15 @@ const matchPopupLexics = {
apply: 13491, apply: 13491,
episode_duration: 13410, episode_duration: 13410,
events: 1020, events: 1020,
from_end_match: 15396,
from_start_match: 15395,
gk: 3515, gk: 3515,
go_back_to_match: 13405, go_back_to_match: 13405,
half_time: 1033,
languages: 15030, languages: 15030,
match_interviews: 13031, match_interviews: 13031,
match_settings: 13490, match_settings: 13490,
no_data: 15397,
players_episodes: 13398, players_episodes: 13398,
playlist_format: 13406, playlist_format: 13406,
playlist_format_all_actions: 13408, playlist_format_all_actions: 13408,

@ -0,0 +1,6 @@
export const joinMatchLexics = {
join_instat_tv: 15420,
join_now: 15422,
promo_text: 15421,
watch_live: 15423,
}

@ -17,7 +17,9 @@ export const PROCEDURES = {
get_user_info: 'get_user_info', get_user_info: 'get_user_info',
get_user_match_second: 'get_user_match_second', get_user_match_second: 'get_user_match_second',
get_user_subscriptions: 'get_user_subscriptions', get_user_subscriptions: 'get_user_subscriptions',
landing_get_match_info: 'landing_get_match_info',
lst_c_country: 'lst_c_country', lst_c_country: 'lst_c_country',
ott_match_events: 'ott_match_events',
ott_match_popup: 'ott_match_popup', ott_match_popup: 'ott_match_popup',
ott_match_popup_actions: 'ott_match_popup_actions', ott_match_popup_actions: 'ott_match_popup_actions',
ott_match_popup_player_playlist: 'ott_match_popup_player_playlist', ott_match_popup_player_playlist: 'ott_match_popup_player_playlist',

@ -130,7 +130,7 @@ export const useFormSubmit = ({ onAddSuccess }: Props) => {
setLoader(true) setLoader(true)
onAddCard(token.id) onAddCard(token.id)
.then(onAddSuccess) .then(onAddSuccess)
.then(() => setLoader(false)) .finally(() => setLoader(false))
} }
} }

@ -13,7 +13,7 @@ export const Column = styled.div`
export const ButtonsBlock = styled.div` export const ButtonsBlock = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: start;
margin-top: 25px; margin-top: 25px;
` `

@ -8,12 +8,16 @@ import { useAuthStore } from 'features/AuthStore'
import { Background } from 'features/Background' import { Background } from 'features/Background'
import { GlobalStyles } from 'features/GlobalStyles' import { GlobalStyles } from 'features/GlobalStyles'
import { Theme } from 'features/Theme' import { Theme } from 'features/Theme'
import { JoinMatchPage } from 'features/JoinMatchPage'
import { AuthenticatedApp } from './AuthenticatedApp' import { AuthenticatedApp } from './AuthenticatedApp'
import { isMatchPage } from '../JoinMatchPage/helpers'
const Main = () => { const Main = () => {
const { loadingUser, user } = useAuthStore() const { loadingUser, user } = useAuthStore()
if (!user && isMatchPage()) return <JoinMatchPage />
// юзер считывается из localstorage или // юзер считывается из localstorage или
// access_token токен истек и запрашивается новый // access_token токен истек и запрашивается новый
if (loadingUser || user?.expired) return null if (loadingUser || user?.expired) return null

@ -17,7 +17,7 @@ export const Wrapper = styled.button<ArrowLoaderProps>`
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
border-color: transparent; border-color: transparent;
width: ${({ width = 'auto' }) => width}; width: ${({ width = 'auto' }) => width};
height: 50px; height: 2.4rem;
background-color: ${({ backgroundColor = '#294fc4' }) => backgroundColor}; background-color: ${({ backgroundColor = '#294fc4' }) => backgroundColor};
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;

@ -17,6 +17,7 @@ import { setCookie, removeCookie } from 'helpers/cookie'
import { useToggle } from 'hooks' import { useToggle } from 'hooks'
import { queryParamStorage } from 'features/QueryParamsStorage' import { queryParamStorage } from 'features/QueryParamsStorage'
import { isMatchPage } from 'features/JoinMatchPage/helpers'
import { getClientSettings } from '../helpers' import { getClientSettings } from '../helpers'
@ -83,8 +84,9 @@ export const useAuth = () => {
if (isRedirectedBackFromAuthProvider) { if (isRedirectedBackFromAuthProvider) {
signinRedirectCallback() signinRedirectCallback()
} else { } else {
// обычная загрузка страницы, проверяем пользователя в localstorage checkUser().catch(() => {
checkUser().catch(login) if (!isMatchPage()) login()
})
} }
}, [ }, [
checkUser, checkUser,
@ -117,11 +119,13 @@ export const useAuth = () => {
const auth = useMemo(() => ({ const auth = useMemo(() => ({
loadingUser, loadingUser,
login,
logout, logout,
user, user,
}), [ }), [
logout, logout,
user, user,
login,
loadingUser, loadingUser,
]) ])

@ -0,0 +1,10 @@
import toNumber from 'lodash/toNumber'
import isUndefined from 'lodash/isUndefined'
export const isMatchPage = () => {
const splitPath = window.location.pathname.split('/')
const pageType = splitPath[2]
const matchId = toNumber(splitPath[3])
return pageType === 'matches' && !isUndefined(matchId)
}

@ -0,0 +1,48 @@
import type { FormEvent } from 'react'
import {
useEffect,
useState,
} from 'react'
import format from 'date-fns/format'
import type { UnauthenticatedMatch } from 'requests'
import { getUnauthenticatedMatch } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import { useLexicsStore } from 'features/LexicsStore'
import { getName } from 'features/Name'
import { useAuthStore } from 'features/AuthStore'
export const useUnauthenticatedMatch = () => {
const [
matchInfo, setMatchInfo,
] = useState<UnauthenticatedMatch>(null)
const { suffix } = useLexicsStore()
const { profileId: matchId, sportType } = usePageParams()
const teamName1 = getName({ nameObj: matchInfo?.team1 || {}, suffix })
const teamName2 = getName({ nameObj: matchInfo?.team2 || {}, suffix })
const date = matchInfo?.date
const matchDate = (date && format(new Date(date), 'd MMM HH:mm')) || ''
const { login } = useAuthStore()
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
login()
}
useEffect(() => {
getUnauthenticatedMatch(sportType, matchId).then(setMatchInfo)
}, [sportType, matchId])
return {
handleSubmit,
matchDate,
teamName1,
teamName2,
}
}

@ -0,0 +1,90 @@
import format from 'date-fns/format'
import { joinMatchLexics } from 'config/lexics/joinMatch'
import { usePageParams } from 'hooks/usePageParams'
import { T9n } from 'features/T9n'
import { useLexicsConfig } from 'features/LexicsStore'
import { useUnauthenticatedMatch } from './hooks'
import {
Wrapper,
MainLogo,
HeaderWrapper,
Footer,
BlockWrapper,
MatchInfo,
SportImgWrapper,
DateInfoWrapper,
DateInfo,
WatchLive,
TeamsNameWrapper,
MainInfoTitle,
MainInfoButton,
MainInfoText,
EmptySpan,
WatchLiveCircle,
WatchLiveText,
SportImgMobileDevice,
FooterLogo,
FooterRights,
} from './styled'
export const JoinMatchPage = () => {
useLexicsConfig(joinMatchLexics)
const { sportType } = usePageParams()
const {
handleSubmit,
matchDate,
teamName1,
teamName2,
} = useUnauthenticatedMatch()
const currentYear = format(new Date(), 'Y')
return (
<Wrapper>
<HeaderWrapper>
<MainLogo />
</HeaderWrapper>
<BlockWrapper>
<SportImgWrapper sportType={sportType} />
<MatchInfo>
<DateInfoWrapper>
<DateInfo>{matchDate}</DateInfo>
<WatchLive>
<WatchLiveCircle />
<WatchLiveText>
<T9n t='watch_live' />
</WatchLiveText>
</WatchLive>
</DateInfoWrapper>
<TeamsNameWrapper>
<EmptySpan>{teamName1}</EmptySpan>
<EmptySpan> </EmptySpan>
<EmptySpan>{teamName2}</EmptySpan>
</TeamsNameWrapper>
<MainInfoTitle>
<T9n t='join_instat_tv' />
</MainInfoTitle>
<SportImgMobileDevice sportType={sportType} />
<MainInfoText>
<T9n t='promo_text' />
</MainInfoText>
<form onSubmit={handleSubmit}>
<MainInfoButton type='submit'>
<T9n t='join_now' />
</MainInfoButton>
</form>
</MatchInfo>
</BlockWrapper>
<Footer>
<FooterLogo />
<FooterRights>©InStat TV {currentYear}</FooterRights>
</Footer>
</Wrapper>
)
}

@ -0,0 +1,258 @@
import styled, { css } from 'styled-components/macro'
import { devices, SportTypes } from 'config'
import { isMobileDevice } from 'config/userAgent'
import { ButtonSolid } from 'features/Common'
import { Logo } from 'features/Logo'
type Props = {
sportType: SportTypes,
}
export const Wrapper = styled.div`
width: 100vw;
height: 100vh;
color: white;
display: flex;
flex-direction: column;
justify-content: space-between;
`
export const HeaderWrapper = styled.div`
background-color: #13151B;
padding: 20px 0;
padding-left: 20%;
width: 100%;
@media ${devices.laptop} {
padding-left: 5%;
}
${isMobileDevice
? css`
display: none;
`
: ''};
`
export const MainLogo = styled(Logo)`
height: 20px;
width: 80px;
`
export const BlockWrapper = styled.div`
height: 100%;
display: flex;
align-items: center;
padding-left: 20%;
width: 100%;
@media ${devices.laptop} {
padding-left: 5%;
}
@media ${devices.mobile} {
padding-left: 5.4rem;
}
${isMobileDevice
? css`
padding: 5.4rem;
overflow: scroll;
@media screen and (orientation: landscape){
padding-top: 20px;
height: auto;
}
`
: ''};
`
export const SportImgWrapper = styled.div<Props>`
background-image: url(/images/landing_${({ sportType }) => sportType}.png);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
width: 512px;
height: 641px;
margin-right: 5%;
${isMobileDevice
? css`
display: none;
@media screen and (orientation: landscape){
display: block;
height: 100%;
width: 70%;
}
`
: ''};
`
export const MatchInfo = styled.div`
${isMobileDevice
? css`
width: 100%;
height: 100%;
@media screen and (orientation: landscape){
padding-top: 0;
}
`
: ''};
`
export const DateInfoWrapper = styled.div`
display: flex;
align-items: center;
font-size: 15px;
font-weight: 600;
${isMobileDevice
? css`
justify-content: space-between;
font-size: 14px;
`
: ''};
`
export const DateInfo = styled.div`
text-transform: uppercase;
background-color: rgba(0,0,0,0.4);
padding: 8px 25px;
color: #B9B9B9;
margin-right: 25px;
border-radius: 5px;
${isMobileDevice
? css`
padding: 0.7em 2.5rem;
`
: ''};
`
export const WatchLive = styled.div`
display: flex;
align-items: center;
`
export const WatchLiveText = styled.div`
color: #EB5757;
letter-spacing: .2rem;
`
export const WatchLiveCircle = styled.div`
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #CC0000;
margin-right: 5px;
${isMobileDevice
? css`
width: 15px;
height: 15px;
`
: ''};
`
export const TeamsNameWrapper = styled.div`
margin: 35px 0 40px;
font-size: 20px;
font-weight: 500;
${isMobileDevice
? css`
margin: 10px 0;
font-size: 16px;
`
: ''};
`
export const MainInfoTitle = styled.div`
font-weight: 600;
font-size: 46px;
${isMobileDevice
? css`
font-size: 32px;
margin-bottom: 15px;
`
: ''};
`
export const MainInfoButton = styled(ButtonSolid)`
width: 260px;
height: 50px;
font-size: 20px;
${isMobileDevice
? css`
width: 100%;
border-radius: 10px;
font-size: 17px;
margin-bottom: 30px;
`
: ''};
`
export const MainInfoText = styled.div`
max-width: 400px;
margin: 40px 0;
font-size: 17px;
line-height: 150%;
${isMobileDevice
? css`
font-size: 15px;
margin: 15px 0 25px;
width: 85%;
`
: ''};
`
export const Footer = styled.div`
font-size: 14px;
background-color: black;
padding: 16px 0;
padding-left: 20%;
width: 100%;
@media ${devices.laptop} {
padding-left: 5%;
}
@media ${devices.mobile} {
padding-left: 35px;
}
${isMobileDevice
? css`
display: flex;
padding: 15px 35px;
justify-content: space-between;
align-items: center;
`
: ''};
`
export const FooterLogo = styled(Logo)`
display: none;
${isMobileDevice
? css`
display: block;
width: 48px;
height: 11px;
opacity: .4;
`
: ''};
`
export const FooterRights = styled.div`
opacity: .5;
${isMobileDevice
? css`
font-size: 12px;
`
: ''};
`
export const SportImgMobileDevice = styled(SportImgWrapper)`
display: none;
${isMobileDevice
? css`
display: block;
height: 53%;
width: 100%;
margin-right: 0;
@media screen and (orientation: landscape){
display: none;
}
`
: ''};
`
export const EmptySpan = styled.span``

@ -13,6 +13,7 @@ import { useMatchPopupStore } from 'features/MatchPopup'
import { usePlayerLogger } from './usePlayerLogger' import { usePlayerLogger } from './usePlayerLogger'
import { useEpisodes } from './useEpisodes' import { useEpisodes } from './useEpisodes'
import { useEvents } from './useEvents'
import { useChapters } from './useChapters' import { useChapters } from './useChapters'
export type Props = { export type Props = {
@ -37,6 +38,7 @@ export const useFinishedMatch = ({ profile }: Props) => {
} = useToggle() } = useToggle()
const { episodes } = useEpisodes() const { episodes } = useEpisodes()
const { events } = useEvents()
const { logPlaylistChange, onPlayingChange } = usePlayerLogger() const { logPlaylistChange, onPlayingChange } = usePlayerLogger()
@ -75,6 +77,7 @@ export const useFinishedMatch = ({ profile }: Props) => {
return { return {
closeSettingsPopup, closeSettingsPopup,
events,
isSettingsPopupOpen, isSettingsPopupOpen,
onPlayingChange, onPlayingChange,
onPlaylistSelect, onPlaylistSelect,

@ -40,7 +40,8 @@ export const useEpisodes = () => {
settings: popupSettings, settings: popupSettings,
sportType, sportType,
}).then(setEpisodes) }).then(setEpisodes)
} else if (playlistOption.type === PlaylistTypes.MATCH) { } else if (playlistOption.type === PlaylistTypes.MATCH
|| playlistOption.type === PlaylistTypes.EVENT) {
setEpisodes(playlistOption.data) setEpisodes(playlistOption.data)
} }
}, [matchId, sportType]) }, [matchId, sportType])

@ -0,0 +1,28 @@
import { useEffect, useState } from 'react'
import type { Events } from 'requests'
import { getMatchEvents } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import { useEventsLexics } from './useEventsLexics'
export const useEvents = () => {
const [events, setEvents] = useState<Events>([])
const { fetchLexics } = useEventsLexics()
const { profileId: matchId, sportType } = usePageParams()
useEffect(() => {
getMatchEvents({
matchId,
sportType,
}).then(fetchLexics)
.then(setEvents)
}, [
fetchLexics,
matchId,
sportType,
])
return { events }
}

@ -0,0 +1,25 @@
import { useCallback } from 'react'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import uniq from 'lodash/uniq'
import type { Events } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
export const useEventsLexics = () => {
const { addLexicsConfig } = useLexicsStore()
const fetchLexics = useCallback((events: Events) => {
const lexics = uniq(map(events, ({ l }) => l))
if (!isEmpty(lexics)) {
addLexicsConfig(lexics)
}
return events
}, [addLexicsConfig])
return { fetchLexics }
}

@ -17,6 +17,7 @@ export const FinishedMatch = (props: Props) => {
const { const {
chapters, chapters,
closeSettingsPopup, closeSettingsPopup,
events,
isSettingsPopupOpen, isSettingsPopupOpen,
onPlayingChange, onPlayingChange,
onPlaylistSelect, onPlaylistSelect,
@ -51,6 +52,7 @@ export const FinishedMatch = (props: Props) => {
{playlists && ( {playlists && (
<MatchSidePlaylists <MatchSidePlaylists
events={events}
playlists={playlists} playlists={playlists}
selectedPlaylist={selectedPlaylist} selectedPlaylist={selectedPlaylist}
onSelect={onPlaylistSelect} onSelect={onPlaylistSelect}

@ -6,6 +6,7 @@ export enum PlaylistTypes {
MATCH, MATCH,
PLAYER, PLAYER,
INTERVIEW, INTERVIEW,
EVENT,
} }
export type MatchPlaylistOption = { export type MatchPlaylistOption = {
@ -33,10 +34,17 @@ export type InterviewPlaylistOption = {
type: PlaylistTypes.INTERVIEW, type: PlaylistTypes.INTERVIEW,
} }
export type EventPlaylistOption = {
data: Episodes,
id: number,
type: PlaylistTypes.EVENT,
}
export type PlaylistOption = ( export type PlaylistOption = (
MatchPlaylistOption MatchPlaylistOption
| PlayerPlaylistOption | PlayerPlaylistOption
| InterviewPlaylistOption | InterviewPlaylistOption
| EventPlaylistOption
) )
export type InterviewPlaylistOptions = Array<InterviewPlaylistOption> export type InterviewPlaylistOptions = Array<InterviewPlaylistOption>

@ -0,0 +1,88 @@
import { ProfileTypes } from 'config'
import type { Event, Team } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import type { PlayerPlaylistOption } from 'features/MatchPage/types'
import { Name } from 'features/Name'
import { T9n } from 'features/T9n'
import {
Avatar,
Button,
EventInfo,
EventDesc,
Title,
SubTitle,
EventTime,
} from '../TabEvents/styled'
type Props = {
active?: boolean,
disabled?: boolean,
event: Event,
isHomeTeam: boolean,
onClick: () => void,
player?: PlayerPlaylistOption,
team?: Team,
}
export const EventButton = ({
active,
disabled,
event,
isHomeTeam,
onClick,
player,
team,
}: Props) => {
const { sportType } = usePageParams()
const {
c: clearTime,
l: lexica,
p: playerId,
sc1: score1,
sc2: score2,
t: teamId,
} = event
const avatarId = playerId || teamId
const profileType = playerId
? ProfileTypes.PLAYERS
: ProfileTypes.TEAMS
const nameObj = player || team
return (
<Button
onClick={onClick}
active={active}
disabled={disabled}
isHomeTeam={isHomeTeam}
>
{avatarId && (
<Avatar
id={avatarId}
isHomeTeam={isHomeTeam}
isPlayer={Boolean(playerId)}
profileType={profileType}
sportType={sportType}
/>
)}
<EventInfo>
<EventTime>
{clearTime}
</EventTime>
<EventDesc>
<Title>
<T9n t={lexica} />
{(score1 || score2) && ` (${score1}-${score2})`}
</Title>
<SubTitle>
{(playerId || teamId) && nameObj && <Name nameObj={nameObj} />}
</SubTitle>
</EventDesc>
</EventInfo>
</Button>
)
}

@ -0,0 +1,63 @@
import find from 'lodash/find'
import map from 'lodash/map'
import type { Events, MatchInfo } from 'requests'
import type { PlaylistOption, Playlists } from 'features/MatchPage/types'
import { PlaylistTypes } from 'features/MatchPage/types'
import { isEqual } from '../../helpers'
import { EventButton } from '../EventButton'
import { Event, List } from '../TabEvents/styled'
type Props = {
events: Events,
onSelect: (option: PlaylistOption) => void,
playlists: Playlists,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
}
export const EventsList = ({
events,
onSelect,
playlists,
profile,
selectedPlaylist,
}: Props) => (
<List>
{map(events, (event) => {
const eventPlaylist = {
data: [{
e: event.e,
h: event.h,
s: event.s,
}],
id: event.n,
type: PlaylistTypes.EVENT,
} as PlaylistOption
const isHomeTeam = event.t === profile?.team1.id
const team = isHomeTeam
? profile?.team1
: profile?.team2
const players = isHomeTeam
? playlists.players.team1
: playlists.players.team2
const player = find(players, { id: event.p })
return (
<Event key={event.n}>
<EventButton
active={isEqual(eventPlaylist, selectedPlaylist)}
event={event}
isHomeTeam={isHomeTeam}
onClick={() => onSelect(eventPlaylist)}
player={player}
team={team}
/>
</Event>
)
})}
</List>
)

@ -0,0 +1,119 @@
import { Fragment, useMemo } from 'react'
import groupBy from 'lodash/groupBy'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import reverse from 'lodash/reverse'
import values from 'lodash/values'
import type { Events, MatchInfo } from 'requests'
import { useToggle } from 'hooks'
import type { PlaylistOption, Playlists } from 'features/MatchPage/types'
import { T9n } from 'features/T9n'
import { EventsList } from '../EventsList'
import {
BlockTitle,
Wrapper,
HalfList,
HalfEvents,
Tabs,
Tab,
} from './styled'
type Props = {
events: Events,
onSelect: (option: PlaylistOption) => void,
playlists: Playlists,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
}
export const TabEvents = ({
events,
onSelect,
playlists,
profile,
selectedPlaylist,
}: Props) => {
const {
close: setUnreversed,
isOpen: areEventsReversed,
open: setReversed,
} = useToggle()
const groupedEvents = useMemo(() => values(
groupBy(
areEventsReversed ? reverse([...events]) : events,
'h',
),
), [areEventsReversed, events])
if (!profile) return null
return (
<Wrapper>
{isEmpty(events)
? (
<BlockTitle>
<T9n t='no_data' />
</BlockTitle>
)
: (
<Fragment>
<Tabs>
<Tab
active={!areEventsReversed}
onClick={setUnreversed}
>
<T9n t='from_start_match' />
</Tab>
<Tab
active={areEventsReversed}
onClick={setReversed}
>
<T9n t='from_end_match' />
</Tab>
</Tabs>
<HalfList>
{
map(
areEventsReversed
? reverse(groupedEvents)
: groupedEvents,
(halfEvents, idx) => {
const firstEvent = halfEvents[0]
const isFirstBlock = idx === 0
return (
<HalfEvents key={firstEvent.h}>
{
!isFirstBlock && (
<BlockTitle>
<T9n t='half_time' />
</BlockTitle>
)
}
<BlockTitle>
<T9n t={firstEvent.l} />
</BlockTitle>
<EventsList
events={halfEvents}
onSelect={onSelect}
playlists={playlists}
profile={profile}
selectedPlaylist={selectedPlaylist}
/>
</HalfEvents>
)
},
)
}
</HalfList>
</Fragment>
)}
</Wrapper>
)
}

@ -0,0 +1,143 @@
import styled, { css } from 'styled-components/macro'
import { ProfileLogo } from 'features/ProfileLogo'
import { Tabs as TabsBase, Tab as TabBase } from '../PlayersPlaylists/styled'
import {
BlockTitle as BlockTitleBase,
Button as ButtonBase,
} from '../../styled'
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`
export const HalfList = styled.ul`
width: 100%;
`
export const HalfEvents = styled.li`
:not(:last-child) {
margin-bottom: 12px;
}
`
export const List = styled.ul`
margin-top: 12px;
`
export const Event = styled.li`
width: 100%;
height: 48px;
:not(:last-child) {
margin-bottom: 12px;
}
`
type AvatarProps = {
isHomeTeam: boolean,
isPlayer: boolean,
}
export const Avatar = styled(ProfileLogo)<AvatarProps>`
position: absolute;
width: 35px;
height: 43px;
object-fit: cover;
${({ isHomeTeam }) => (
isHomeTeam
? css`left: 5px;`
: css`right: 5px;`
)}
${({ isPlayer }) => (
isPlayer
? css`bottom: 0;`
: css`
top: 50%;
transform: translateY(-50%);
`
)}
`
export const EventInfo = styled.div`
display: flex;
width: 100%;
`
export const EventDesc = styled.div`
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
export const Title = styled.span`
font-weight: 600;
font-size: 14px;
line-height: 15px;
color: #ffffff;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
`
export const SubTitle = styled(Title)`
font-size: 10px;
line-height: 16px;
font-weight: normal;
color: rgba(255, 255, 255, 0.7);
margin-top: 2px;
`
export const EventTime = styled(Title)`
font-size: 14px;
font-weight: normal;
margin-right: 10px;
width: auto;
overflow: initial;
min-width: 29px;
`
export const Tabs = styled(TabsBase)`
margin-top: 0px;
margin-bottom: 18px;
`
export const Tab = styled(TabBase)`
text-transform: none;
:first-child {
margin-right: 15px;
}
`
export const BlockTitle = styled(BlockTitleBase)`
text-align: center;
width: 100%;
display: inline-block;
`
type ButtonProps = {
isHomeTeam: boolean,
}
export const Button = styled(ButtonBase)<ButtonProps>`
&:hover ${EventDesc} {
overflow: visible;
}
${({ isHomeTeam }) => (
isHomeTeam
? css`padding-left: 60px;`
: css`padding: 0 40px 0 60px;`
)}
`

@ -1,10 +1,11 @@
import type { MatchInfo } from 'requests' import type { Events, MatchInfo } from 'requests'
import type { PlaylistOption, Playlists } from 'features/MatchPage/types' import type { PlaylistOption, Playlists } from 'features/MatchPage/types'
import { Tab, TabsGroup } from 'features/Common' import { Tab, TabsGroup } from 'features/Common'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
import { Tabs } from './config' import { Tabs } from './config'
import { TabEvents } from './components/TabEvents'
import { TabWatch } from './components/TabWatch' import { TabWatch } from './components/TabWatch'
import { useMatchSidePlaylists } from './hooks' import { useMatchSidePlaylists } from './hooks'
import { import {
@ -15,11 +16,12 @@ import {
const tabPanes = { const tabPanes = {
[Tabs.WATCH]: TabWatch, [Tabs.WATCH]: TabWatch,
[Tabs.EVENTS]: () => null, [Tabs.EVENTS]: TabEvents,
[Tabs.LANGUAGES]: () => null, [Tabs.LANGUAGES]: () => null,
} }
type Props = { type Props = {
events: Events,
onSelect: (option: PlaylistOption) => void, onSelect: (option: PlaylistOption) => void,
playlists: Playlists, playlists: Playlists,
profile: MatchInfo, profile: MatchInfo,
@ -27,6 +29,7 @@ type Props = {
} }
export const MatchSidePlaylists = ({ export const MatchSidePlaylists = ({
events,
onSelect, onSelect,
playlists, playlists,
profile, profile,
@ -50,7 +53,6 @@ export const MatchSidePlaylists = ({
<T9n t='watch' /> <T9n t='watch' />
</Tab> </Tab>
<Tab <Tab
disabled
selected={selectedTab === Tabs.EVENTS} selected={selectedTab === Tabs.EVENTS}
onClick={() => onTabClick(Tabs.EVENTS)} onClick={() => onTabClick(Tabs.EVENTS)}
> >
@ -67,6 +69,7 @@ export const MatchSidePlaylists = ({
</TabsWrapper> </TabsWrapper>
<Container> <Container>
<TabPane <TabPane
events={events}
onSelect={onSelect} onSelect={onSelect}
playlists={playlists} playlists={playlists}
profile={profile} profile={profile}

@ -0,0 +1,69 @@
import {
DATA_URL,
PROCEDURES,
SportTypes,
} from 'config'
import { Episode, Episodes } from 'requests'
import { callApi, getSportLexic } from 'helpers'
const proc = PROCEDURES.ott_match_events
type Args = {
matchId: number,
sportType: SportTypes,
}
export type Event = Episode & {
/** clear time */
c: string,
/** event lexic */
l: number,
/** event id */
n: number,
/** player id */
p?: number,
/** repeat */
rep?: Episodes,
/** score team1 */
sc1?: number,
/** score team2 */
sc2?: number,
/** team id */
t?: number,
}
export type Events = Array<Event>
type Response = {
data?: Events,
}
export const getMatchEvents = async ({
matchId,
sportType,
}: Args) => {
const config = {
body: {
params: {
_p_match_id: matchId,
},
proc,
},
}
const response: Response = await callApi({
config,
url: `${DATA_URL}/${getSportLexic(sportType)}`,
})
return response?.data || []
}

@ -7,7 +7,7 @@ import { callApi } from 'helpers'
const proc = PROCEDURES.get_match_info const proc = PROCEDURES.get_match_info
type Team = { export type Team = {
abbrev_eng: string, abbrev_eng: string,
abbrev_rus: string, abbrev_rus: string,
id: number, id: number,

@ -0,0 +1,42 @@
import {
DATA_URL,
PROCEDURES,
SportTypes,
} from 'config'
import { callApi } from 'helpers'
const proc = PROCEDURES.landing_get_match_info
type Team = {
name_eng: string,
name_rus: string,
}
export type UnauthenticatedMatch = {
date: string,
team1: Team,
team2: Team,
tournament: {
name_eng: string,
name_rus: string,
},
} | null
export const getUnauthenticatedMatch = (sportId: SportTypes, matchId: number)
: Promise<UnauthenticatedMatch> => {
const config = {
body: {
params: {
_p_match_id: matchId,
_p_sport: sportId,
},
proc,
},
}
return callApi({
config,
url: DATA_URL,
})
}

@ -11,6 +11,7 @@ export * from './getTournamentInfo'
export * from './getTeamInfo' export * from './getTeamInfo'
export * from './getUserInfo' export * from './getUserInfo'
export * from './getMatchInfo' export * from './getMatchInfo'
export * from './getUnauthenticatedMatch'
export * from './reportPlayerProgress' export * from './reportPlayerProgress'
export * from './getVideos' export * from './getVideos'
export * from './saveUserInfo' export * from './saveUserInfo'
@ -18,6 +19,7 @@ export * from './getPlayerInfo'
export * from './getMatchLastWatchSeconds' export * from './getMatchLastWatchSeconds'
export * from './getMatchesPreviewImages' export * from './getMatchesPreviewImages'
export * from './getSportActions' export * from './getSportActions'
export * from './getMatchEvents'
export * from './getMatchPlaylists' export * from './getMatchPlaylists'
export * from './getPlayerPlaylists' export * from './getPlayerPlaylists'
export * from './getSubscriptions' export * from './getSubscriptions'

Loading…
Cancel
Save