Ott 145 matches list (#71)
parent
8efce0066a
commit
7c63c7da38
|
After Width: | Height: | Size: 261 B |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 690 B |
|
After Width: | Height: | Size: 691 B |
@ -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; |
||||||
|
` |
||||||
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
@ -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; |
||||||
|
` |
||||||
@ -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}` |
||||||
|
} |
||||||
Loading…
Reference in new issue