Ott 145 matches list (#71)

keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Ruslan Khayrullin 5 years ago committed by GitHub
parent 8efce0066a
commit 7c63c7da38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      public/images/closeIcon.svg
  2. BIN
      public/images/preview.png
  3. 5
      public/images/slideLeft.svg
  4. 5
      public/images/slideRight.svg
  5. 14
      src/config/lexics/authenticated.tsx
  6. 2
      src/config/sportTypes.tsx
  7. 18
      src/features/Common/CloseButton/index.tsx
  8. 11
      src/features/Common/SportName/index.tsx
  9. 2
      src/features/Common/index.tsx
  10. 1
      src/features/HeaderFilters/components/SportTypeFilter/styled.tsx
  11. 1
      src/features/HeaderFilters/components/TournamentFilter/styled.tsx
  12. 2
      src/features/HeaderFilters/components/TournamentList/index.tsx
  13. 98
      src/features/HeaderFilters/store/hooks/index.tsx
  14. 45
      src/features/HomePage/components/Matches/hooks.tsx
  15. 22
      src/features/HomePage/components/Matches/index.tsx
  16. 8
      src/features/HomePage/index.tsx
  17. 14
      src/features/HomePage/styled.tsx
  18. 3
      src/features/ItemsList/index.tsx
  19. 10
      src/features/ItemsList/styled.tsx
  20. 2
      src/features/MainWrapper/index.tsx
  21. 85
      src/features/MatchesSlider/components/CardFinished/index.tsx
  22. 41
      src/features/MatchesSlider/components/CardFinishedHover/index.tsx
  23. 109
      src/features/MatchesSlider/components/CardLive/index.tsx
  24. 35
      src/features/MatchesSlider/components/CardLiveHover/index.tsx
  25. 102
      src/features/MatchesSlider/components/CardSoon/index.tsx
  26. 21
      src/features/MatchesSlider/components/MatchCard/index.tsx
  27. 122
      src/features/MatchesSlider/components/styled.tsx
  28. 3
      src/features/MatchesSlider/config/index.tsx
  29. 79
      src/features/MatchesSlider/hooks.tsx
  30. 53
      src/features/MatchesSlider/index.tsx
  31. 32
      src/features/MatchesSlider/styled.tsx
  32. 1
      src/features/Search/styled.tsx
  33. 4
      src/features/UserFavorites/styled.tsx
  34. 1
      src/helpers/index.tsx
  35. 7
      src/helpers/msToMinutesAndSeconds/__tests__/index.tsx
  36. 8
      src/helpers/msToMinutesAndSeconds/index.tsx
  37. 24
      src/requests/getMatches.tsx

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.54" fill-rule="evenodd" clip-rule="evenodd" d="M10 1L9 0L5 4L1 0L0 1L4 5L0 9L1 10L5 6L9 10L10 9L6 5L10 1Z" fill="white" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 20C40 31.0457 31.0457 40 20 40C8.9543 40 0 31.0457 0 20C0 8.9543 8.9543 0 20 0C31.0457 0 40 8.9543 40 20ZM18.6569 27.0711L12.2929 20.7071C11.9024 20.3166 11.9024 19.6834 12.2929 19.2929L18.6569 12.9289C19.0474 12.5384 19.6805 12.5384 20.0711 12.9289C20.4616 13.3195 20.4616 13.9526 20.0711 14.3431L15.4142 19L25 19C25.5523 19 26 19.4477 26 20C26 20.5523 25.5523 21 25 21L15.4142 21L20.0711 25.6569C20.4616 26.0474 20.4616 26.6805 20.0711 27.0711C19.6805 27.4616 19.0474 27.4616 18.6569 27.0711Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 690 B

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 20C0 31.0457 8.95431 40 20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.95431 0 0 8.9543 0 20ZM21.3431 27.0711L27.7071 20.7071C28.0976 20.3166 28.0976 19.6834 27.7071 19.2929L21.3431 12.9289C20.9526 12.5384 20.3195 12.5384 19.9289 12.9289C19.5384 13.3195 19.5384 13.9526 19.9289 14.3431L24.5858 19L15 19C14.4477 19 14 19.4477 14 20C14 20.5523 14.4477 21 15 21L24.5858 21L19.9289 25.6569C19.5384 26.0474 19.5384 26.6805 19.9289 27.0711C20.3195 27.4616 20.9526 27.4616 21.3431 27.0711Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 691 B

@ -3,17 +3,31 @@ import { proceduresLexics } from './procedures'
export const authenticatedLexics = {
basketball: 6960,
football: 6958,
full_game: 13028,
game_finished: 13026,
game_time: 13029,
goals: 13030,
hide_score: 12982,
highlights: 13033,
hockey: 6959,
interview: 13031,
kickoff_in: 13027,
live: 13024,
logout: 4306,
match_status_finished: 12985,
match_status_live: 12984,
match_status_soon: 12986,
match_video: 13025,
player: 630,
players_video: 13032,
sport: 12993,
team: 658,
tournament: 1009,
user_account: 12928,
video_from_my_subscriptions: 13023,
watch_from_beginning: 13021,
watch_from_last_pause: 13022,
watch_now: 13020,
...proceduresLexics,
}

@ -8,4 +8,4 @@ export const SPORT_NAMES = {
[SportTypes.BASKETBALL]: 'basketball',
[SportTypes.FOOTBALL]: 'football',
[SportTypes.HOCKEY]: 'hockey',
}
} as const

@ -0,0 +1,18 @@
import styled from 'styled-components/macro'
export const CloseButton = styled.button.attrs({
'aria-label': 'Close',
})`
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 10px;
cursor: pointer;
border-style: none;
outline: none;
background: none;
background-image: url(/images/closeIcon.svg);
background-repeat: no-repeat;
background-size: contain;
`

@ -0,0 +1,11 @@
import styled from 'styled-components/macro'
import { T9n } from 'features/T9n'
export const SportName = styled(T9n)<{ color: string }>`
margin-right: 10px;
font-size: 11px;
color: ${({ color }) => color};
letter-spacing: 0.02em;
text-transform: uppercase;
`

@ -3,5 +3,7 @@ export * from './Button'
export * from './Radio'
export * from './Checkbox'
export * from './Arrows'
export * from './CloseButton'
export * from './SportName'
export * from './сustomScrollbar'
export * from './customStyles'

@ -24,6 +24,7 @@ export const SportList = styled.ul`
background: #666;
border-radius: 2px;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
z-index: 1;
`
export const CustomOption = styled(T9n)<{ image: string }>`

@ -9,6 +9,7 @@ export const ListWrapper = styled.div`
width: 448px;
height: 456px;
overflow-y: scroll;
z-index: 2;
${сustomScrollbar}
`

@ -8,10 +8,10 @@ import {
LogoWrapper,
ItemInfo,
Name,
SportName,
TeamOrCountry,
Wrapper,
} from 'features/ItemsList/styled'
import { SportName } from 'features/Common'
import { ListItem } from './styled'

@ -5,49 +5,124 @@ import {
useCallback,
} from 'react'
import debounce from 'lodash/debounce'
import map from 'lodash/map'
import flatten from 'lodash/flatten'
import pipe from 'lodash/fp/pipe'
import fpMap from 'lodash/fp/map'
import fpOrderBy from 'lodash/fp/orderBy'
import format from 'date-fns/format'
import startOfDay from 'date-fns/startOfDay'
import { SportTypes } from 'config'
import type { Matches, Content } from 'requests'
import { SportTypes, SPORT_NAMES } from 'config'
import { getMatches } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
import { getProfileLogo } from 'helpers'
type Name = 'name_rus' | 'name_eng'
export type Match = {
date: string,
id: number,
preview: string,
sportName: string,
sportType: number,
streamStatus: number,
team1Logo: string,
team1Name: string,
team1Score: number,
team2Logo: string,
team2Name: string,
team2Score: number,
tournamentName: string,
}
export enum MatchStatuses {
Live = 1,
Finished = 2,
Soon = 3,
Live = 2,
Finished = 3,
Soon = 1,
}
const dateFormat = 'dd/MM/yyyy HH:mm:ss'
export const useFilters = () => {
const {
suffix,
} = useLexicsStore()
const [selectedDate, setSelectedDate] = useState(new Date())
const [selectedMatchStatus, setSelectedMatchStatus] = useState<MatchStatuses>(MatchStatuses.Live)
const [selectedSportTypeId, setSelectedSportTypeId] = useState<SportTypes>(SportTypes.FOOTBALL)
const [selectedTournamentId, setSelectedTournamentId] = useState<number | null>(null)
const fetchMatches = useCallback(debounce(getMatches, 300), [])
const [matches, setMatches] = useState<Matches>({
broadcast: [],
features: [],
highlights: [],
})
// временно здесь запрашиваются матчи при изменении фильтров,
// можно эту логику вырезать и вставить в компонент матчей
useEffect(() => {
if (!selectedTournamentId) return
const formattedDate = format(startOfDay(selectedDate), dateFormat)
fetchMatches({
getMatches({
date: formattedDate,
matchStatus: selectedMatchStatus,
sportType: selectedSportTypeId,
tournamentId: selectedTournamentId,
})
}).then(setMatches)
}, [
selectedDate,
selectedMatchStatus,
selectedSportTypeId,
selectedTournamentId,
fetchMatches,
])
const prepareMatches = useCallback((content: Array<Content>) => pipe(
fpMap<Content, Array<Match>>(({
matches: matchesList,
sport,
...rest
}) => map(matchesList, ({
date,
id,
stream_status,
team1,
team2,
}) => ({
date,
id,
preview: '/images/preview.png',
sportName: SPORT_NAMES[sport],
sportType: sport,
streamStatus: stream_status,
team1Logo: getProfileLogo({
id: team1.id,
profileType: 2,
sportType: sport,
}),
team1Name: team1[`name_${suffix}` as Name],
team1Score: team1.score,
team2Logo: getProfileLogo({
id: team2.id,
profileType: 2,
sportType: sport,
}),
team2Name: team2[`name_${suffix}` as Name],
team2Score: team2.score,
tournamentName: rest[`name_${suffix}` as Name],
}))),
flatten,
fpOrderBy((match: Match) => Number(new Date(match.date)), 'desc'),
)(content) as Array<Match>, [suffix])
const preparedMatches = {
broadcast: prepareMatches(matches.broadcast),
features: prepareMatches(matches.features),
highlights: prepareMatches(matches.highlights),
}
const store = useMemo(() => ({
matches: preparedMatches,
selectedDate,
selectedMatchStatus,
selectedSportTypeId,
@ -62,6 +137,7 @@ export const useFilters = () => {
selectedSportTypeId,
selectedTournamentId,
setSelectedTournamentId,
preparedMatches,
])
return store

@ -0,0 +1,45 @@
import size from 'lodash/size'
import slice from 'lodash/slice'
import chunk from 'lodash/chunk'
import type { Match } from 'features/HeaderFilters'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
const MATCHES_PER_ROW = 6
const ROWS_COUNT = 3
export const useMatches = () => {
const {
matches: {
broadcast: matchesList,
},
} = useHeaderFiltersStore()
const matchesCount = size(matchesList)
let groupedMatches: Array<Array<Match>> = []
/**
* Распределяем матчи в ROWS_COUNT рядов
* Если общее количество матчей не больше количества
* ячеек в сетке размера ROWS_COUNT x MATCHES_PER_ROW,
* заполняем по максимуму каждый ряд с MATCHES_PER_ROW
* матчей в каждом, иначе распределяем поровну
*/
if (matchesCount <= MATCHES_PER_ROW * ROWS_COUNT) {
for (let i = 0; i < ROWS_COUNT; i++) {
const matches = slice(
matchesList,
i * MATCHES_PER_ROW,
i * MATCHES_PER_ROW + MATCHES_PER_ROW,
)
groupedMatches.push(matches)
}
} else {
groupedMatches = chunk(matchesList, Math.ceil(matchesCount / ROWS_COUNT))
}
return {
groupedMatches,
}
}

@ -0,0 +1,22 @@
import React, { Fragment } from 'react'
import map from 'lodash/map'
import { MatchesSlider } from 'features/MatchesSlider'
import { useMatches } from './hooks'
export const Matches = () => {
const { groupedMatches } = useMatches()
return (
<Fragment>
{map(groupedMatches, (matches, i) => (
<MatchesSlider
key={i}
matches={matches}
/>
))}
</Fragment>
)
}

@ -11,9 +11,13 @@ import {
HeaderFiltersStore,
} from 'features/HeaderFilters'
import { UserFavorites } from 'features/UserFavorites'
import { T9n } from 'features/T9n'
import { Matches } from './components/Matches'
import {
FilterWrapper,
Title,
Content,
} from './styled'
export const HomePage = () => (
@ -38,6 +42,10 @@ export const HomePage = () => (
<TournamentFilter />
</FilterWrapper>
</Header>
<Content>
<Title><T9n t='video_from_my_subscriptions' /></Title>
<Matches />
</Content>
</MainWrapper>
</HeaderFiltersStore>
)

@ -6,3 +6,17 @@ export const FilterWrapper = styled.div`
margin-right: 16px;
display: flex;
`
export const Content = styled.main`
margin-top: 75px;
padding: 0 16px;
`
export const Title = styled.h1`
margin-bottom: 17px;
font-weight: bold;
font-size: 36px;
line-height: 24px;
letter-spacing: -0.02em;
color: #fff;
`

@ -2,6 +2,8 @@ import React from 'react'
import map from 'lodash/map'
import { SportName } from 'features/Common'
import { useItemsList } from './hooks'
import {
Logo,
@ -9,7 +11,6 @@ import {
Item,
ItemInfo,
Name,
SportName,
StyledLink,
TeamOrCountry,
Wrapper,

@ -2,8 +2,6 @@ import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { T9n } from 'features/T9n'
export const Wrapper = styled.ul`
margin: 0;
padding: 0;
@ -40,14 +38,6 @@ export const Name = styled.div`
color: #ccc;
`
export const SportName = styled(T9n)<{ color: string }>`
margin-right: 10px;
font-size: 11px;
color: ${({ color }) => color};
letter-spacing: 0.02em;
text-transform: uppercase;
`
export const TeamOrCountry = styled.span`
font-size: 11px;
color: #ccc;

@ -2,5 +2,5 @@ import styled from 'styled-components/macro'
export const MainWrapper = styled.div`
width: 100%;
display: flex;
padding-left: 80px;
`

@ -0,0 +1,85 @@
import React from 'react'
import styled from 'styled-components/macro'
import type { Match } from 'features/HeaderFilters'
import { getSportColor } from 'helpers'
import { useCard } from 'features/MatchesSlider/hooks'
import { SportName } from 'features/Common'
import { T9n } from 'features/T9n'
import { CardFinishedHover } from '../CardFinishedHover'
import {
CardWrapper,
Info,
MatchStatus as CommonMatchStatus,
Preview,
PreviewWrapper,
Score,
Team,
TeamName,
Teams,
TournamentName,
} from '../styled'
const MatchStatus = styled(CommonMatchStatus)`
color: #313131;
background-color: #EACB6F;
`
type CardFinishedProps = {
match: Match,
}
export const CardFinished = ({
match: {
date,
preview,
sportName,
sportType,
team1Name,
team1Score,
team2Name,
team2Score,
tournamentName,
},
}: CardFinishedProps) => {
const {
close,
isOpen,
onKeyPress,
open,
} = useCard()
if (isOpen) return <CardFinishedHover onClose={close} />
return (
<CardWrapper
onClick={open}
onKeyPress={onKeyPress}
>
<MatchStatus><T9n t='game_finished' /></MatchStatus>
<PreviewWrapper>
<Preview
alt={tournamentName}
title={tournamentName}
src={preview}
/>
</PreviewWrapper>
<Info>
<SportName t={sportName} color={getSportColor(sportType)} />
<TournamentName>{tournamentName}</TournamentName>
<Teams>
<Team>
<TeamName>{team1Name}</TeamName>
<Score>{team1Score}</Score>
</Team>
<Team>
<TeamName>{team2Name}</TeamName>
<Score>{team2Score}</Score>
</Team>
</Teams>
</Info>
</CardWrapper>
)
}

@ -0,0 +1,41 @@
import React from 'react'
import { CloseButton } from 'features/Common'
import {
CardHoverInner,
CardHoverTitle,
CardHoverWrapper,
MoreVideo,
Row,
Rows,
} from '../styled'
type CardFinishedHoverProps = {
onClose: () => void,
}
export const CardFinishedHover = ({ onClose }: CardFinishedHoverProps) => (
<CardHoverWrapper>
<CardHoverInner>
<CloseButton onClick={onClose} />
<CardHoverTitle t='match_video' />
<Rows>
<Row>
<MoreVideo t='full_game' />
<MoreVideo t='game_time' />
</Row>
<Row>
<MoreVideo t='highlights' />
<MoreVideo t='goals' />
<MoreVideo t='interview' />
</Row>
<Row>
<MoreVideo t='players_video' />
</Row>
</Rows>
</CardHoverInner>
</CardHoverWrapper>
)

@ -0,0 +1,109 @@
import React, {
useState,
useEffect,
useCallback,
} from 'react'
import styled from 'styled-components/macro'
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'
import type { Match } from 'features/HeaderFilters'
import { getSportColor, msToMinutesAndSeconds } from 'helpers'
import { useCard } from 'features/MatchesSlider/hooks'
import { SportName } from 'features/Common'
import { T9n } from 'features/T9n'
import {
CardWrapper,
Info,
MatchStatus as CommonMatchStatus,
Preview,
PreviewWrapper,
Score,
Team,
TeamName,
Teams,
TournamentName,
} from '../styled'
import { CardLiveHover } from '../CardLiveHover'
const UPDATE_INTERVAL = 1000
const MatchStatus = styled(CommonMatchStatus)`
min-width: 100px;
color: #fff;
background-color: #cc0000;
`
type CardLiveProps = {
match: Match,
}
export const CardLive = ({
match: {
date,
id,
preview,
sportName,
sportType,
team1Name,
team1Score,
team2Name,
team2Score,
tournamentName,
},
}: CardLiveProps) => {
const {
close,
isOpen,
onKeyPress,
open,
} = useCard()
const getMs = useCallback(() => differenceInMilliseconds(
new Date(),
new Date(date),
), [date])
const [matchDuration, setMatchDuration] = useState(msToMinutesAndSeconds(getMs()))
useEffect(() => {
const timer = setInterval(() => {
setMatchDuration(msToMinutesAndSeconds(getMs()))
}, UPDATE_INTERVAL)
return () => clearInterval(timer)
}, [getMs])
if (isOpen) return <CardLiveHover onClose={close} />
return (
<CardWrapper
onClick={open}
onKeyPress={onKeyPress}
>
<MatchStatus><T9n t='live' /> {matchDuration}</MatchStatus>
<PreviewWrapper>
<Preview
alt={tournamentName}
title={tournamentName}
src={preview}
/>
</PreviewWrapper>
<Info>
<SportName t={sportName} color={getSportColor(sportType)} />
<TournamentName>{tournamentName}</TournamentName>
<Teams>
<Team>
<TeamName>{team1Name}</TeamName>
<Score>{team1Score}</Score>
</Team>
<Team>
<TeamName>{team2Name}</TeamName>
<Score>{team2Score}</Score>
</Team>
</Teams>
</Info>
</CardWrapper>
)
}

@ -0,0 +1,35 @@
import React from 'react'
import { CloseButton } from 'features/Common'
import {
CardHoverInner,
CardHoverTitle,
CardHoverWrapper,
MoreVideo,
Row,
Rows,
} from '../styled'
type CardLiveHoverProps = {
onClose: () => void,
}
export const CardLiveHover = ({ onClose }: CardLiveHoverProps) => (
<CardHoverWrapper>
<CardHoverInner>
<CloseButton onClick={onClose} />
<CardHoverTitle t='match_video' />
<Rows>
<Row>
<MoreVideo t='watch_now' />
<MoreVideo t='watch_from_beginning' />
</Row>
<Row>
<MoreVideo t='watch_from_last_pause' />
</Row>
</Rows>
</CardHoverInner>
</CardHoverWrapper>
)

@ -0,0 +1,102 @@
import type { BaseSyntheticEvent } from 'react'
import React, { useCallback } from 'react'
import styled from 'styled-components/macro'
import format from 'date-fns/format'
import type { Match } from 'features/HeaderFilters'
import { getSportColor, handleImageError } from 'helpers'
import { SportName } from 'features/Common'
import { T9n } from 'features/T9n'
import {
MatchStatus as CommonMatchStatus,
CardWrapper as CommonCardWrapper,
Info,
PreviewWrapper,
Team,
TeamName as CommonTeamName,
Teams,
TournamentName,
} from '../styled'
const MatchStatus = styled(CommonMatchStatus)`
color: rgba(255, 255, 255, 0.3);
border: 1px solid currentColor;
`
const CardWrapper = styled(CommonCardWrapper)`
cursor: default;
`
const TeamLogos = styled.div`
display: flex;
padding-left: 24px;
`
const TeamLogo = styled.img`
width: 70px;
`
const TeamName = styled(CommonTeamName)`
max-width: none;
`
type CardSoonProps = {
match: Match,
}
export const CardSoon = ({
match: {
date,
sportName,
sportType,
team1Logo,
team1Name,
team2Logo,
team2Name,
tournamentName,
},
}: CardSoonProps) => {
const startTime = format(new Date(date), 'HH:mm')
const onError = useCallback((e: BaseSyntheticEvent) => handleImageError({
e,
sport: sportType,
type: 2,
}), [sportType])
return (
<CardWrapper>
<MatchStatus><T9n t='kickoff_in' /> {startTime}</MatchStatus>
<PreviewWrapper>
<TeamLogos>
<TeamLogo
src={team1Logo}
alt={team1Name}
title={team1Name}
onError={onError}
/>
<TeamLogo
src={team2Logo}
alt={team2Name}
title={team2Name}
onError={onError}
/>
</TeamLogos>
</PreviewWrapper>
<Info>
<SportName t={sportName} color={getSportColor(sportType)} />
<TournamentName>{tournamentName}</TournamentName>
<Teams>
<Team>
<TeamName>{team1Name}</TeamName>
</Team>
<Team>
<TeamName>{team2Name}</TeamName>
</Team>
</Teams>
</Info>
</CardWrapper>
)
}

@ -0,0 +1,21 @@
import React from 'react'
import type { Match } from 'features/HeaderFilters'
import { MatchStatuses } from 'features/HeaderFilters'
import { CardLive } from '../CardLive'
import { CardFinished } from '../CardFinished'
import { CardSoon } from '../CardSoon'
type MatchCardProps = {
match: Match,
}
export const MatchCard = ({ match }: MatchCardProps) => {
switch (match.streamStatus) {
case MatchStatuses.Soon: return <CardSoon match={match} />
case MatchStatuses.Live: return <CardLive match={match} />
case MatchStatuses.Finished: return <CardFinished match={match} />
default: return null
}
}

@ -0,0 +1,122 @@
import styled from 'styled-components/macro'
import { T9n } from 'features/T9n'
import { MATCH_CARD_WIDTH, MATCH_CARD_GAP } from '../config'
export const CardWrapper = styled.li.attrs({
tabIndex: 0,
})`
position: relative;
flex: 0 0 auto;
width: ${MATCH_CARD_WIDTH}px;
margin-right: ${MATCH_CARD_GAP}px;
padding-bottom: 38px;
border-radius: 2px;
background-color: #3F3F3F;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
cursor: pointer;
`
export const PreviewWrapper = styled.div`
display: flex;
flex-direction: column-reverse;
height: 160px;
`
export const Preview = styled.img`
width: 100%;
height: 100%;
`
export const MatchStatus = styled.div`
position: absolute;
top: 24px;
left: 24px;
border-radius: 2px;
padding: 7px 10px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.1em;
`
export const Info = styled.div`
padding: 27px 24px 0;
color: #fff;
`
export const TournamentName = styled.span`
color: rgba(255, 255, 255, 0.5);
font-size: 10px;
`
export const Teams = styled.div`
margin-top: 21px;
`
export const Team = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 17px;
font-weight: bold;
line-height: 21px;
color: #fff;
`
export const TeamName = styled.span`
max-width: 90%;
word-break: break-word;
`
export const Score = styled.span``
export const Rows = styled.div`
margin-top: 20px;
`
export const Row = styled.div`
white-space: nowrap;
`
export const MoreVideo = styled(T9n)`
display: inline-block;
margin: 0 8px 8px 0;
padding: 8px;
border-radius: 2px;
font-weight: 500;
font-size: 11px;
text-align: center;
color: rgba(255, 255, 255, 0.5);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 100%
),
#5C5C5C;
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
cursor: pointer;
:hover {
background: rgba(153, 153, 153, 0.9);
color: #fff;
}
`
export const CardHoverWrapper = styled(CardWrapper)`
padding: 16px 24px;
cursor: default;
`
export const CardHoverInner = styled.div`
position: relative;
overflow: hidden;
`
export const CardHoverTitle = styled(T9n)`
font-size: 10px;
letter-spacing: 0.03em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
`

@ -0,0 +1,3 @@
export const MATCH_CARD_WIDTH = 288
export const MATCH_CARD_GAP = 16

@ -0,0 +1,79 @@
import type { SyntheticEvent, KeyboardEvent } from 'react'
import {
useEffect,
useRef,
useState,
useCallback,
} from 'react'
import type { Match } from 'features/HeaderFilters'
import { useToggle } from 'hooks'
import { MATCH_CARD_WIDTH, MATCH_CARD_GAP } from './config'
export const useCard = () => {
const {
close,
isOpen,
open,
} = useToggle()
const onKeyPress = useCallback((e: KeyboardEvent<HTMLLIElement>) => {
if (e.key === 'Enter') {
open()
}
}, [open])
return {
close,
isOpen,
onKeyPress,
open,
}
}
export const useMatchesSlider = (matches: Array<Match>) => {
const slidesRef = useRef<HTMLUListElement>(null)
const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRigthArrow] = useState(false)
useEffect(() => {
const {
clientWidth = 0,
scrollLeft = 0,
scrollWidth = 0,
} = slidesRef.current || {}
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowRigthArrow(scrollRight > 1)
}, [matches, slidesRef])
const onScroll = useCallback((e: SyntheticEvent<HTMLUListElement>) => {
const {
clientWidth: targetClientWidth,
scrollLeft: targetScrollLeft,
scrollWidth: targetScrollWidth,
} = e.currentTarget
setShowLeftArrow(targetScrollLeft > 1)
setShowRigthArrow((targetScrollWidth - (targetScrollLeft + targetClientWidth)) > 1)
}, [])
const slideLeft = useCallback(() => {
slidesRef.current!.scrollBy(-(MATCH_CARD_WIDTH + MATCH_CARD_GAP), 0)
}, [])
const slideRight = useCallback(() => {
slidesRef.current!.scrollBy(MATCH_CARD_WIDTH + MATCH_CARD_GAP, 0)
}, [])
return {
onScroll,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
slidesRef,
}
}

@ -0,0 +1,53 @@
import React from 'react'
import map from 'lodash/map'
import isEmpty from 'lodash/isEmpty'
import type { Match } from 'features/HeaderFilters'
import { useMatchesSlider } from './hooks'
import { MatchCard } from './components/MatchCard'
import {
Wrapper,
Arrow,
Slides,
} from './styled'
type MatchesSliderProps = {
matches: Array<Match>,
}
export const MatchesSlider = ({ matches }: MatchesSliderProps) => {
const {
onScroll,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
slidesRef,
} = useMatchesSlider(matches)
if (isEmpty(matches)) return null
return (
<Wrapper>
{showLeftArrow && (
<Arrow
type='arrowLeft'
aria-label='Slide left'
onClick={slideLeft}
/>
)}
<Slides ref={slidesRef} onScroll={onScroll}>
{map(matches, (match) => <MatchCard match={match} key={match.id} />)}
</Slides>
{showRightArrow && (
<Arrow
type='arrowRight'
aria-label='Slide right'
onClick={slideRight}
/>
)}
</Wrapper>
)
}

@ -0,0 +1,32 @@
import styled from 'styled-components/macro'
export const Wrapper = styled.div`
position: relative;
margin-bottom: 16px;
`
export const Slides = styled.ul`
display: flex;
overflow-x: auto;
scroll-behavior: smooth;
&::-webkit-scrollbar {
display: none;
}
`
export const Arrow = styled.div<{ type: 'arrowLeft' | 'arrowRight' }>`
position: absolute;
top: 50%;
left: ${({ type }) => (type === 'arrowLeft' ? '10px' : 'calc(100% - 10px)')};
width: 40px;
height: 40px;
background-position: center;
background-repeat: no-repeat;
background-image: url(${({ type }) => (type === 'arrowLeft'
? '/images/slideLeft.svg'
: '/images/slideRight.svg')});
cursor: pointer;
transform: translate(-50%, -50%);
z-index: 1;
`

@ -59,6 +59,7 @@ export const Results = styled.div`
width: 448px;
max-height: 431px;
overflow-y: auto;
z-index: 1;
${сustomScrollbar}
`

@ -8,6 +8,10 @@ import { TooltipBlockWrapper } from './TooltipBlock/styled'
export const StyledLink = styled(Link)``
export const UserSportFavWrapper = styled.div`
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
flex: 0 0 auto;

@ -7,3 +7,4 @@ export * from './getProfileUrl'
export * from './getSportColor'
export * from './getSportLexic'
export * from './handleImg'
export * from './msToMinutesAndSeconds'

@ -0,0 +1,7 @@
import { msToMinutesAndSeconds } from '..'
describe('msToMinutesAndSeconds helper', () => {
it('returns correct formatted time', () => {
expect(msToMinutesAndSeconds(1000000)).toBe('16:40')
})
})

@ -0,0 +1,8 @@
export const msToMinutesAndSeconds = (ms: number) => {
const minutes = Math.floor(ms / 60000)
const seconds = Number(((ms % 60000) / 1000).toFixed(0))
return seconds === 60
? `${minutes + 1}:00`
: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
}

@ -30,13 +30,13 @@ export type Content = {
matches: Array<Match>,
name_eng: string,
name_rus: string,
sport: number,
sport: 1 | 2 | 3,
}
export type Match = {
date: string,
id: number,
round_id: number,
round_id: number | null,
stream_status: number,
team1: Team,
team2: Team,
@ -53,7 +53,13 @@ type Args = {
date: string,
matchStatus: MatchStatuses,
sportType: SportTypes,
tournamentId: number,
tournamentId: number | null,
}
export type Matches = {
broadcast: Array<Content>,
features: Array<Content>,
highlights: Array<Content>,
}
export const getMatches = async ({
@ -79,5 +85,15 @@ export const getMatches = async ({
url: DATA_URL,
}).then(getResponseData(proc))
return data.video_content.broadcast.content || []
const {
broadcast,
features,
highlights,
} = data.video_content
return {
broadcast: broadcast.content || [],
features: features.content || [],
highlights: highlights.content || [],
}
}

Loading…
Cancel
Save