* 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"
/>
<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."
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<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"
/>
<script src="https://js.stripe.com/v3" async></script>
@ -27,5 +27,14 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="modal-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>
</html>

@ -6,11 +6,15 @@ const matchPopupLexics = {
apply: 13491,
episode_duration: 13410,
events: 1020,
from_end_match: 15396,
from_start_match: 15395,
gk: 3515,
go_back_to_match: 13405,
half_time: 1033,
languages: 15030,
match_interviews: 13031,
match_settings: 13490,
no_data: 15397,
players_episodes: 13398,
playlist_format: 13406,
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_match_second: 'get_user_match_second',
get_user_subscriptions: 'get_user_subscriptions',
landing_get_match_info: 'landing_get_match_info',
lst_c_country: 'lst_c_country',
ott_match_events: 'ott_match_events',
ott_match_popup: 'ott_match_popup',
ott_match_popup_actions: 'ott_match_popup_actions',
ott_match_popup_player_playlist: 'ott_match_popup_player_playlist',

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

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

@ -8,12 +8,16 @@ import { useAuthStore } from 'features/AuthStore'
import { Background } from 'features/Background'
import { GlobalStyles } from 'features/GlobalStyles'
import { Theme } from 'features/Theme'
import { JoinMatchPage } from 'features/JoinMatchPage'
import { AuthenticatedApp } from './AuthenticatedApp'
import { isMatchPage } from '../JoinMatchPage/helpers'
const Main = () => {
const { loadingUser, user } = useAuthStore()
if (!user && isMatchPage()) return <JoinMatchPage />
// юзер считывается из localstorage или
// access_token токен истек и запрашивается новый
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);
border-color: transparent;
width: ${({ width = 'auto' }) => width};
height: 50px;
height: 2.4rem;
background-color: ${({ backgroundColor = '#294fc4' }) => backgroundColor};
border-radius: 5px;
display: flex;

@ -17,6 +17,7 @@ import { setCookie, removeCookie } from 'helpers/cookie'
import { useToggle } from 'hooks'
import { queryParamStorage } from 'features/QueryParamsStorage'
import { isMatchPage } from 'features/JoinMatchPage/helpers'
import { getClientSettings } from '../helpers'
@ -83,8 +84,9 @@ export const useAuth = () => {
if (isRedirectedBackFromAuthProvider) {
signinRedirectCallback()
} else {
// обычная загрузка страницы, проверяем пользователя в localstorage
checkUser().catch(login)
checkUser().catch(() => {
if (!isMatchPage()) login()
})
}
}, [
checkUser,
@ -117,11 +119,13 @@ export const useAuth = () => {
const auth = useMemo(() => ({
loadingUser,
login,
logout,
user,
}), [
logout,
user,
login,
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 { useEpisodes } from './useEpisodes'
import { useEvents } from './useEvents'
import { useChapters } from './useChapters'
export type Props = {
@ -37,6 +38,7 @@ export const useFinishedMatch = ({ profile }: Props) => {
} = useToggle()
const { episodes } = useEpisodes()
const { events } = useEvents()
const { logPlaylistChange, onPlayingChange } = usePlayerLogger()
@ -75,6 +77,7 @@ export const useFinishedMatch = ({ profile }: Props) => {
return {
closeSettingsPopup,
events,
isSettingsPopupOpen,
onPlayingChange,
onPlaylistSelect,

@ -40,7 +40,8 @@ export const useEpisodes = () => {
settings: popupSettings,
sportType,
}).then(setEpisodes)
} else if (playlistOption.type === PlaylistTypes.MATCH) {
} else if (playlistOption.type === PlaylistTypes.MATCH
|| playlistOption.type === PlaylistTypes.EVENT) {
setEpisodes(playlistOption.data)
}
}, [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 {
chapters,
closeSettingsPopup,
events,
isSettingsPopupOpen,
onPlayingChange,
onPlaylistSelect,
@ -51,6 +52,7 @@ export const FinishedMatch = (props: Props) => {
{playlists && (
<MatchSidePlaylists
events={events}
playlists={playlists}
selectedPlaylist={selectedPlaylist}
onSelect={onPlaylistSelect}

@ -6,6 +6,7 @@ export enum PlaylistTypes {
MATCH,
PLAYER,
INTERVIEW,
EVENT,
}
export type MatchPlaylistOption = {
@ -33,10 +34,17 @@ export type InterviewPlaylistOption = {
type: PlaylistTypes.INTERVIEW,
}
export type EventPlaylistOption = {
data: Episodes,
id: number,
type: PlaylistTypes.EVENT,
}
export type PlaylistOption = (
MatchPlaylistOption
| PlayerPlaylistOption
| InterviewPlaylistOption
| EventPlaylistOption
)
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 { Tab, TabsGroup } from 'features/Common'
import { T9n } from 'features/T9n'
import { Tabs } from './config'
import { TabEvents } from './components/TabEvents'
import { TabWatch } from './components/TabWatch'
import { useMatchSidePlaylists } from './hooks'
import {
@ -15,11 +16,12 @@ import {
const tabPanes = {
[Tabs.WATCH]: TabWatch,
[Tabs.EVENTS]: () => null,
[Tabs.EVENTS]: TabEvents,
[Tabs.LANGUAGES]: () => null,
}
type Props = {
events: Events,
onSelect: (option: PlaylistOption) => void,
playlists: Playlists,
profile: MatchInfo,
@ -27,6 +29,7 @@ type Props = {
}
export const MatchSidePlaylists = ({
events,
onSelect,
playlists,
profile,
@ -50,7 +53,6 @@ export const MatchSidePlaylists = ({
<T9n t='watch' />
</Tab>
<Tab
disabled
selected={selectedTab === Tabs.EVENTS}
onClick={() => onTabClick(Tabs.EVENTS)}
>
@ -67,6 +69,7 @@ export const MatchSidePlaylists = ({
</TabsWrapper>
<Container>
<TabPane
events={events}
onSelect={onSelect}
playlists={playlists}
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
type Team = {
export type Team = {
abbrev_eng: string,
abbrev_rus: string,
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 './getUserInfo'
export * from './getMatchInfo'
export * from './getUnauthenticatedMatch'
export * from './reportPlayerProgress'
export * from './getVideos'
export * from './saveUserInfo'
@ -18,6 +19,7 @@ export * from './getPlayerInfo'
export * from './getMatchLastWatchSeconds'
export * from './getMatchesPreviewImages'
export * from './getSportActions'
export * from './getMatchEvents'
export * from './getMatchPlaylists'
export * from './getPlayerPlaylists'
export * from './getSubscriptions'

Loading…
Cancel
Save