Ott 138 match page player (#97)

* Ott 138 match page player part 1 (#93)

* feat(#138): added svg icons

* feat(#138): added video player control components

Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com>

* feat(#138): added player (#96)

Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com>

Co-authored-by: mirlan.maksitaliev <mirlan.maksitaliev@instatsport.com>
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent 0526449a18
commit c5b6991c3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 6
      public/images/player-fullscreen-off.svg
  3. 3
      public/images/player-fullscreen-on.svg
  4. 4
      public/images/player-pause.svg
  5. 3
      public/images/player-play.svg
  6. 5
      public/images/player-prev.svg
  7. 8
      public/images/player-settings.svg
  8. 5
      public/images/player-volume-off.svg
  9. 5
      public/images/player-volume-on.svg
  10. 10
      src/features/MatchPage/index.tsx
  11. 11
      src/features/MatchPage/styled.tsx
  12. 1
      src/features/Menu/styled.tsx
  13. 39
      src/features/VideoPlayer/components/ProgressBar/index.tsx
  14. 44
      src/features/VideoPlayer/components/ProgressBar/styled.tsx
  15. 13
      src/features/VideoPlayer/components/TimeTooltip/index.tsx
  16. 33
      src/features/VideoPlayer/components/TimeTooltip/styled.tsx
  17. 36
      src/features/VideoPlayer/components/VolumeBar/index.tsx
  18. 61
      src/features/VideoPlayer/components/VolumeBar/styled.tsx
  19. 12
      src/features/VideoPlayer/config.tsx
  20. 91
      src/features/VideoPlayer/hooks/index.tsx
  21. 36
      src/features/VideoPlayer/hooks/useFullscreen.tsx
  22. 81
      src/features/VideoPlayer/hooks/useSlider.tsx
  23. 36
      src/features/VideoPlayer/hooks/useVolume.tsx
  24. 87
      src/features/VideoPlayer/index.tsx
  25. 106
      src/features/VideoPlayer/styled.tsx
  26. 1
      src/helpers/index.tsx
  27. 11
      src/helpers/secondsToHms/__tests__/index.tsx
  28. 16
      src/helpers/secondsToHms/index.tsx
  29. 1
      src/hooks/useToggle.tsx

@ -21,9 +21,11 @@
"react": "^16.13.1",
"react-datepicker": "^3.1.3",
"react-dom": "^16.13.1",
"react-player": "^2.6.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"screenfull": "^5.0.2",
"styled-components": "^5.1.1"
},
"devDependencies": {

@ -0,0 +1,6 @@
<svg width="25" height="23" viewBox="0 0 25 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 0V5C6 5.55228 5.55228 6 5 6H0" stroke="white" stroke-width="3"/>
<path d="M19 23L19 18C19 17.4477 19.4477 17 20 17L25 17" stroke="white" stroke-width="3"/>
<path d="M25 6L20 6C19.4477 6 19 5.55228 19 5L19 -2.62268e-07" stroke="white" stroke-width="3"/>
<path d="M7.86805e-07 17L5 17C5.55229 17 6 17.4477 6 18L6 23" stroke="white" stroke-width="3"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H6.99976V3H3V6.36364H0V0.5C0 0.223857 0.223858 0 0.5 0ZM0 13.6364V19.5C0 19.7761 0.223857 20 0.5 20H6.99976V17H3V13.6364H0ZM19 13.6364V17H14.9998V20H21.5C21.7761 20 22 19.7761 22 19.5V13.6364H19ZM22 6.36364V0.5C22 0.223858 21.7761 0 21.5 0H14.9998V3H19V6.36364H22Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 441 B

@ -0,0 +1,4 @@
<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="6" height="24" rx="0.5" fill="white"/>
<rect x="12" width="6" height="24" rx="0.5" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 218 B

@ -0,0 +1,3 @@
<svg width="19" height="24" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3507 11.5801C18.6554 11.7771 18.6554 12.2229 18.3507 12.4199L1.0215 23.6255C0.688812 23.8406 0.25 23.6018 0.25 23.2056V0.794387C0.25 0.398203 0.68881 0.159394 1.0215 0.374521L18.3507 11.5801Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

@ -0,0 +1,5 @@
<svg width="23" height="22" viewBox="0 0 23 22" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M4.13023 10.8163C3.81528 11.0119 3.81528 11.4702 4.13023 11.6658L17.1938 19.7788C17.5269 19.9857 17.9576 19.7461 17.9576 19.3541V3.12811C17.9576 2.73603 17.5269 2.4965 17.1938 2.70335L4.13023 10.8163Z" fill="white"/>
<rect width="2.32181" height="16.3527" rx="0.5" transform="matrix(-1 0 0 1 2.67188 3.06476)" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

@ -0,0 +1,8 @@
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.66318 4C9.87915 3.54537 10 3.0368 10 2.5C10 1.9632 9.87915 1.45463 9.66318 1H21.5C21.7761 1 22 1.22386 22 1.5V3.5C22 3.77614 21.7761 4 21.5 4H9.66318ZM3.33682 4H0.5C0.223857 4 0 3.77614 0 3.5V1.5C0 1.22386 0.223858 1 0.5 1H3.33682C3.12085 1.45463 3 1.9632 3 2.5C3 3.0368 3.12085 3.54537 3.33682 4Z" fill="white"/>
<circle cx="6.5" cy="2.5" r="2.5" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3368 12C12.1208 11.5454 12 11.0368 12 10.5C12 9.9632 12.1208 9.45463 12.3368 9H0.5C0.223858 9 0 9.22386 0 9.5V11.5C0 11.7761 0.223858 12 0.5 12H12.3368ZM18.6632 12H21.5C21.7761 12 22 11.7761 22 11.5V9.5C22 9.22386 21.7761 9 21.5 9H18.6632C18.8792 9.45463 19 9.9632 19 10.5C19 11.0368 18.8792 11.5454 18.6632 12Z" fill="white"/>
<circle r="2.5" transform="matrix(-1 0 0 1 15.5 10.5)" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.66318 20C9.87915 19.5454 10 19.0368 10 18.5C10 17.9632 9.87915 17.4546 9.66318 17H21.5C21.7761 17 22 17.2239 22 17.5V19.5C22 19.7761 21.7761 20 21.5 20H9.66318ZM3.33682 20H0.5C0.223857 20 0 19.7761 0 19.5V17.5C0 17.2239 0.223858 17 0.5 17H3.33682C3.12085 17.4546 3 17.9632 3 18.5C3 19.0368 3.12085 19.5454 3.33682 20Z" fill="white"/>
<circle cx="6.5" cy="18.5" r="2.5" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,5 @@
<svg width="41" height="28" viewBox="0 0 41 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.09325 8.14392H0.5C0.223858 8.14392 0 8.36778 0 8.64392V19.6439C0 19.9201 0.223858 20.1439 0.5 20.1439H9.09323L20.2246 27.4887C20.557 27.7081 21 27.4697 21 27.0714V1.21647C21 0.818198 20.557 0.579788 20.2246 0.799134L9.09325 8.14392Z" fill="white"/>
<rect x="27.0502" y="17.5355" width="12" height="2" rx="0.5" transform="rotate(-45 27.0502 17.5355)" fill="white"/>
<rect x="35.5355" y="18.9497" width="12" height="2" rx="0.5" transform="rotate(-135 35.5355 18.9497)" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 643 B

@ -0,0 +1,5 @@
<svg width="41" height="28" viewBox="0 0 41 28" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.09325 8.14392H0.5C0.223858 8.14392 0 8.36778 0 8.64392V19.6439C0 19.9201 0.223858 20.1439 0.5 20.1439H9.09323L20.2246 27.4887C20.557 27.7081 21 27.4697 21 27.0714V1.21647C21 0.818198 20.557 0.579788 20.2246 0.799134L9.09325 8.14392Z" fill="white"/>
<circle cx="21" cy="14" r="5" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

@ -1,13 +1,17 @@
import React from 'react'
import { VideoPlayer } from 'features/VideoPlayer'
import { MatchProfileCard } from './MatchProfileCard'
import { MainWrapper, Container } from './styled'
import {
MainWrapper,
} from './styled'
const url = 'https://bserv.instatfootball.tv/common/outhls.m3u8'
export const MatchPage = () => (
<MainWrapper>
<MatchProfileCard />
<Container>
<VideoPlayer url={url} />
</Container>
</MainWrapper>
)

@ -1,6 +1,13 @@
import styled from 'styled-components/macro'
export const MainWrapper = styled.div`
margin-left: 18px;
margin-top: 63px;
margin: 63px 16px 0 16px;
`
export const Container = styled.div`
max-width: 1504px;
max-height: 896px;
margin-top: 14px;
display: flex;
flex-direction: column;
`

@ -31,6 +31,7 @@ export const MenuList = styled.ul`
border-radius: 2px;
background-color: #666;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
z-index: 1;
:before {
content: '';

@ -0,0 +1,39 @@
import React from 'react'
import { secondsToHms } from 'helpers'
import { useSlider } from 'features/VideoPlayer/hooks/useSlider'
import { TimeTooltip } from '../TimeTooltip'
import {
ProgressBarList,
LoadedProgress,
PlayedProgress,
Scrubber,
} from './styled'
type Props = {
loadedProgress: number,
onPlayedProgressChange: (progress: number) => void,
playedProgress: number,
playedSeconds: number,
}
export const ProgressBar = ({
loadedProgress,
onPlayedProgressChange,
playedProgress,
playedSeconds,
}: Props) => {
const progressBarRef = useSlider({ onChange: onPlayedProgressChange })
return (
<ProgressBarList ref={progressBarRef}>
<LoadedProgress value={loadedProgress} />
<PlayedProgress value={playedProgress} />
<Scrubber value={playedProgress}>
<TimeTooltip time={secondsToHms(playedSeconds)} />
</Scrubber>
</ProgressBarList>
)
}

@ -0,0 +1,44 @@
import styled from 'styled-components/macro'
export const ProgressBarList = styled.div`
flex-grow: 1;
height: 4px;
position: relative;
background-color: rgba(255, 255, 255, 0.3);
cursor: pointer;
`
type ProgressProps = {
value: number,
}
export const LoadedProgress = styled.div<ProgressProps>`
position: absolute;
z-index: 1;
background-color: rgba(255, 255, 255, 0.6);;
height: 100%;
width: ${({ value }) => value}%;
`
export const PlayedProgress = styled.div<ProgressProps>`
position: absolute;
z-index: 2;
background-color: #F2C94C;
height: 100%;
width: ${({ value }) => value}%;
`
export const Scrubber = styled.button<ProgressProps>`
border: none;
outline: none;
position: absolute;
top: 0;
left: ${({ value }) => value}%;
transform: translate(-50%, -38%);
z-index: 3;
width: 18px;
height: 18px;
background-color: #F2C94C;
border-radius: 50%;
cursor: pointer;
`

@ -0,0 +1,13 @@
import React from 'react'
import { Wrapper, Time } from './styled'
type Props = {
time: string,
}
export const TimeTooltip = ({ time }: Props) => (
<Wrapper>
<Time>{time}</Time>
</Wrapper>
)

@ -0,0 +1,33 @@
import styled from 'styled-components/macro'
export const Wrapper = styled.div`
position: absolute;
bottom: 100%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
min-width: 60px;
height: 32px;
::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 9px;
height: 9px;
transform: rotate(45deg) translateY(50%);
background-color: #000;
}
`
export const Time = styled.span`
padding: 0 6px;
color: #F2C94C;
text-align: center;
font-weight: bold;
font-size: 16px;
line-height: 20px;
`

@ -0,0 +1,36 @@
import React from 'react'
import { useSlider } from 'features/VideoPlayer/hooks/useSlider'
import {
Wrapper,
VolumeButton,
VolumeProgressList,
VolumeProgress,
Scrubber,
} from './styled'
type Props = {
muted: boolean,
onChange: (progress: number) => void,
onClick: () => void,
value: number,
}
export const VolumeBar = ({
muted,
onChange,
onClick,
value,
}: Props) => {
const progressRef = useSlider({ onChange })
return (
<Wrapper>
<VolumeButton onClick={onClick} muted={muted} />
<VolumeProgressList ref={progressRef}>
<VolumeProgress value={muted ? 0 : value} />
<Scrubber value={muted ? 0 : value} />
</VolumeProgressList>
</Wrapper>
)
}

@ -0,0 +1,61 @@
import styled from 'styled-components/macro'
export const Wrapper = styled.div`
display: flex;
align-items: center;
margin-right: 18px;
`
type VolumeButtonProps = {
muted: boolean,
}
export const VolumeButton = styled.button<VolumeButtonProps>`
outline: none;
border: none;
padding: 0;
width: 40px;
height: 26px;
margin-right: 9px;
cursor: pointer;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
background-color: transparent;
background-image: ${({ muted }) => (
muted
? 'url(/images/player-volume-off.svg)'
: 'url(/images/player-volume-on.svg)'
)};
`
export const VolumeProgressList = styled.div`
width: 77px;
height: 4px;
position: relative;
background-color: rgba(255, 255, 255, 0.3);
cursor: pointer;
`
type VolumeProgressProps = {
value: number,
}
export const VolumeProgress = styled.div<VolumeProgressProps>`
height: 100%;
width: ${({ value }) => value}%;
background-color: #fff;
`
export const Scrubber = styled.button<VolumeProgressProps>`
border: none;
outline: none;
width: 13px;
height: 13px;
left: ${({ value }) => value}%;;
position: absolute;
border-radius: 50%;
background-color: #fff;
transform: translate(-50%, -65%);
cursor: pointer;
`

@ -0,0 +1,12 @@
export const playerConfig = {
file: {
forceHLS: true,
hlsOptions: {
liveSyncDuration: 10,
maxBufferLength: 10,
maxBufferSize: 0,
},
},
}
export const progressCallbackInterval = 100

@ -0,0 +1,91 @@
import type { MouseEvent } from 'react'
import {
useCallback,
useState,
useRef,
} from 'react'
import ReactPlayer from 'react-player'
import throttle from 'lodash/throttle'
import { useFullscreen } from './useFullscreen'
import { useVolume } from './useVolume'
type ProgressState = {
loaded: number,
loadedSeconds: number,
played: number,
playedSeconds: number,
}
export const useVideoPlayer = () => {
const [ready, setReady] = useState(false)
const [playing, setPlaying] = useState(false)
const [duration, setDuration] = useState(0)
const [loadedProgress, setLoadedProgress] = useState(0)
const [playedProgress, setPlayedProgress] = useState(0)
const [playedSeconds, setPlayedSeconds] = useState(0)
const wrapperRef = useRef<HTMLDivElement>(null)
const playerRef = useRef<ReactPlayer>(null)
const startPlaying = () => {
setReady(true)
setPlaying(true)
}
const togglePlaying = () => {
if (ready) {
setPlaying(!playing)
}
}
const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => {
if (e.target === playerRef.current?.getInternalPlayer()) {
togglePlaying()
}
}
const setPlayerProgress = useCallback(
throttle((value: number) => {
playerRef.current?.seekTo(value, 'fraction')
}, 100),
[],
)
const onProgressChange = useCallback((progress: number) => {
setPlayedProgress(progress * 100)
setPlayerProgress(progress)
}, [
setPlayedProgress,
setPlayerProgress,
])
const setProgress = ({
loaded,
played,
playedSeconds: seconds,
}: ProgressState) => {
setLoadedProgress(loaded * 100)
setPlayedProgress(played * 100)
setPlayedSeconds(seconds)
}
return {
duration,
loadedProgress,
onPlayerClick,
onProgressChange,
playedProgress,
playedSeconds,
playerRef,
playing,
setDuration,
setProgress,
setReady,
startPlaying,
togglePlaying,
wrapperRef,
...useFullscreen(wrapperRef),
...useVolume(),
}
}

@ -0,0 +1,36 @@
import type { RefObject } from 'react'
import { useEffect } from 'react'
import screenfull from 'screenfull'
import noop from 'lodash/noop'
import { useToggle } from 'hooks'
export const useFullscreen = (wrapperRef: RefObject<HTMLDivElement>) => {
const {
isOpen: isFullscreen,
setIsOpen: setFullscreen,
} = useToggle()
useEffect(() => {
if (!screenfull.isEnabled) return noop
const changeListener = () => {
setFullscreen(screenfull.isEnabled && screenfull.isFullscreen)
}
screenfull.onchange(changeListener)
return () => {
if (screenfull.isEnabled) {
screenfull.off('change', changeListener)
}
}
}, [setFullscreen])
const onFullscreenClick = () => {
if (screenfull.isEnabled && wrapperRef.current) {
screenfull.toggle(wrapperRef.current)
}
}
return { isFullscreen, onFullscreenClick }
}

@ -0,0 +1,81 @@
import {
useCallback,
useEffect,
useRef,
} from 'react'
const getNormalizedProgress = (fraction: number) => {
if (fraction > 1) return 1
if (fraction < 0) return 0
return fraction
}
const useMouseState = () => {
const mouseDownRef = useRef(false)
const setMouseDown = useCallback(() => {
mouseDownRef.current = true
}, [])
const setMouseUp = useCallback(() => {
mouseDownRef.current = false
}, [])
return {
mouseDownRef,
setMouseDown,
setMouseUp,
}
}
type Args = {
onChange: (progress: number) => void,
}
export const useSlider = ({ onChange }: Args) => {
const ref = useRef<HTMLDivElement>(null)
const {
mouseDownRef,
setMouseDown,
setMouseUp,
} = useMouseState()
useEffect(() => {
const handleProgress = (mouseX: number) => {
const track = ref.current
if (!mouseDownRef.current || !track) return
const x = mouseX - track.getBoundingClientRect().left
const progress = getNormalizedProgress(x / track.offsetWidth)
onChange(progress)
}
const mouseDownListener = ({ clientX, target }: MouseEvent) => {
const track = ref.current
if (target === track || track?.contains(target as Node)) {
setMouseDown()
handleProgress(clientX)
}
}
const mouseUpListener = setMouseUp
const mouseMoveListener = (e: MouseEvent) => {
handleProgress(e.clientX)
}
window.addEventListener('mousedown', mouseDownListener)
window.addEventListener('mouseup', mouseUpListener)
window.addEventListener('mousemove', mouseMoveListener)
return () => {
window.removeEventListener('mousedown', mouseDownListener)
window.removeEventListener('mouseup', mouseUpListener)
window.removeEventListener('mousemove', mouseMoveListener)
}
}, [
onChange,
mouseDownRef,
setMouseDown,
setMouseUp,
])
return ref
}

@ -0,0 +1,36 @@
import { useState } from 'react'
import { useToggle } from 'hooks'
export const useVolume = () => {
const {
close: unmute,
isOpen: muted,
open: mute,
toggle: toggleMuted,
} = useToggle()
const [volume, setVolume] = useState(0.5)
const onVolumeChange = (value: number) => {
if (value === 0) {
mute()
} else {
unmute()
}
setVolume(value)
}
const onVolumeClick = () => {
if (muted && volume === 0) {
setVolume(0.1)
}
toggleMuted()
}
return {
muted,
onVolumeChange,
onVolumeClick,
volume,
}
}

@ -0,0 +1,87 @@
import React from 'react'
import { playerConfig, progressCallbackInterval } from './config'
import { useVideoPlayer } from './hooks'
import { ProgressBar } from './components/ProgressBar'
import { VolumeBar } from './components/VolumeBar'
import {
PlayerWrapper,
ReactPlayer,
Controls,
Prev,
Next,
PlayStop,
Fullscreen,
Settings,
} from './styled'
type Props = {
url: string,
}
export const VideoPlayer = ({ url }: Props) => {
const {
isFullscreen,
loadedProgress,
muted,
onFullscreenClick,
onPlayerClick,
onProgressChange,
onVolumeChange,
onVolumeClick,
playedProgress,
playedSeconds,
playerRef,
playing,
setDuration,
setProgress,
startPlaying,
togglePlaying,
volume,
wrapperRef,
} = useVideoPlayer()
const volumeInPercent = volume * 100
return (
<PlayerWrapper
ref={wrapperRef}
playing={playing}
onClick={onPlayerClick}
>
<ReactPlayer
width='100%'
height='100%'
url={url}
ref={playerRef}
playing={playing}
volume={volume}
config={playerConfig}
progressInterval={progressCallbackInterval}
onProgress={setProgress}
onDuration={setDuration}
onEnded={togglePlaying}
onReady={startPlaying}
/>
<Controls>
<Prev />
<PlayStop onClick={togglePlaying} playing={playing} />
<Next />
<VolumeBar
value={volumeInPercent}
muted={muted}
onChange={onVolumeChange}
onClick={onVolumeClick}
/>
<ProgressBar
onPlayedProgressChange={onProgressChange}
playedSeconds={playedSeconds}
playedProgress={playedProgress}
loadedProgress={loadedProgress}
/>
<Fullscreen onClick={onFullscreenClick} isFullscreen={isFullscreen} />
<Settings />
</Controls>
</PlayerWrapper>
)
}

@ -0,0 +1,106 @@
import styled from 'styled-components/macro'
import ReactPlayerBase from 'react-player'
export const ReactPlayer = styled(ReactPlayerBase)`
position: absolute;
top: 0;
left: 0;
`
export const Controls = styled.div`
position: absolute;
width: 100%;
bottom: 22px;
display: flex;
align-items: center;
padding: 0 25px;
transition: opacity 0.3s ease-in-out;
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
`
export const PlayerWrapper = styled.div<PlayStopProps>`
position: relative;
width: 100%;
height: 0px;
padding-top: 56.25%;
background-color: #000;
:hover ${Controls} {
opacity: 1;
}
${Controls} {
opacity: ${({ playing }) => (
playing
? '0'
: '1'
)};
}
`
const ButtonBase = styled.button`
outline: none;
border: none;
color: #fff;
cursor: pointer;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
`
type PlayStopProps = {
playing: boolean,
}
export const PlayStop = styled(ButtonBase)<PlayStopProps>`
width: 24px;
height: 24px;
margin-right: 18px;
background-image: ${({ playing }) => (
playing
? 'url(/images/player-pause.svg)'
: 'url(/images/player-play.svg)'
)};
`
export const Prev = styled(ButtonBase)`
width: 29px;
height: 28px;
margin-right: 19px;
background-image: url(/images/player-prev.svg);
`
export const Next = styled(Prev)`
margin-right: 24px;
transform: rotate(180deg);
`
export const Volume = styled(ButtonBase)`
width: 40px;
height: 26px;
margin-right: 23px;
background-image: url(/images/player-volume-off.svg);
`
type FullscreenProps = {
isFullscreen: boolean,
}
export const Fullscreen = styled(ButtonBase)<FullscreenProps>`
width: 22px;
height: 20px;
margin-left: 31px;
margin-right: 23px;
background-image: ${({ isFullscreen }) => (
isFullscreen
? 'url(/images/player-fullscreen-off.svg)'
: 'url(/images/player-fullscreen-on.svg)'
)};
`
export const Settings = styled(ButtonBase)`
width: 22px;
height: 20px;
background-image: url(/images/player-settings.svg);
`

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

@ -0,0 +1,11 @@
import { secondsToHms } from '..'
describe('secondsToHms helper', () => {
it('returns correct formatted time', () => {
expect(secondsToHms(0)).toBe('00:00')
expect(secondsToHms(120)).toBe('02:00')
expect(secondsToHms(600)).toBe('10:00')
expect(secondsToHms(3600)).toBe('01:00:00')
expect(secondsToHms(3700)).toBe('01:01:40')
})
})

@ -0,0 +1,16 @@
const prependZero = (value: number | string) => (
`0${value}`.slice(-2)
)
/**
* Форматирует секунды в hh:mm:ss
*/
export const secondsToHms = (seconds: number) => {
const hours = prependZero(Math.floor(seconds / 3600))
const minutes = prependZero(Math.floor(seconds % 3600 / 60))
const secondsStr = prependZero(Math.floor(seconds % 3600 % 60))
if (Number(hours) > 0) return `${hours}:${minutes}:${secondsStr}`
return `${minutes}:${secondsStr}`
}

@ -9,6 +9,7 @@ export const useToggle = () => {
close,
isOpen,
open,
setIsOpen,
toggle,
}
}

Loading…
Cancel
Save