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