From c5b6991c3b5e1f11fc86e6ead1c2a312fe0a152d Mon Sep 17 00:00:00 2001 From: Mirlan Date: Mon, 24 Aug 2020 15:39:52 +0600 Subject: [PATCH] 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 * feat(#138): added player (#96) Co-authored-by: mirlan.maksitaliev Co-authored-by: mirlan.maksitaliev --- package.json | 2 + public/images/player-fullscreen-off.svg | 6 + public/images/player-fullscreen-on.svg | 3 + public/images/player-pause.svg | 4 + public/images/player-play.svg | 3 + public/images/player-prev.svg | 5 + public/images/player-settings.svg | 8 ++ public/images/player-volume-off.svg | 5 + public/images/player-volume-on.svg | 5 + src/features/MatchPage/index.tsx | 10 +- src/features/MatchPage/styled.tsx | 11 +- src/features/Menu/styled.tsx | 1 + .../components/ProgressBar/index.tsx | 39 +++++++ .../components/ProgressBar/styled.tsx | 44 ++++++++ .../components/TimeTooltip/index.tsx | 13 +++ .../components/TimeTooltip/styled.tsx | 33 ++++++ .../components/VolumeBar/index.tsx | 36 ++++++ .../components/VolumeBar/styled.tsx | 61 ++++++++++ src/features/VideoPlayer/config.tsx | 12 ++ src/features/VideoPlayer/hooks/index.tsx | 91 +++++++++++++++ .../VideoPlayer/hooks/useFullscreen.tsx | 36 ++++++ src/features/VideoPlayer/hooks/useSlider.tsx | 81 +++++++++++++ src/features/VideoPlayer/hooks/useVolume.tsx | 36 ++++++ src/features/VideoPlayer/index.tsx | 87 ++++++++++++++ src/features/VideoPlayer/styled.tsx | 106 ++++++++++++++++++ src/helpers/index.tsx | 1 + src/helpers/secondsToHms/__tests__/index.tsx | 11 ++ src/helpers/secondsToHms/index.tsx | 16 +++ src/hooks/useToggle.tsx | 1 + 29 files changed, 762 insertions(+), 5 deletions(-) create mode 100644 public/images/player-fullscreen-off.svg create mode 100644 public/images/player-fullscreen-on.svg create mode 100644 public/images/player-pause.svg create mode 100644 public/images/player-play.svg create mode 100644 public/images/player-prev.svg create mode 100644 public/images/player-settings.svg create mode 100644 public/images/player-volume-off.svg create mode 100644 public/images/player-volume-on.svg create mode 100644 src/features/VideoPlayer/components/ProgressBar/index.tsx create mode 100644 src/features/VideoPlayer/components/ProgressBar/styled.tsx create mode 100644 src/features/VideoPlayer/components/TimeTooltip/index.tsx create mode 100644 src/features/VideoPlayer/components/TimeTooltip/styled.tsx create mode 100644 src/features/VideoPlayer/components/VolumeBar/index.tsx create mode 100644 src/features/VideoPlayer/components/VolumeBar/styled.tsx create mode 100644 src/features/VideoPlayer/config.tsx create mode 100644 src/features/VideoPlayer/hooks/index.tsx create mode 100644 src/features/VideoPlayer/hooks/useFullscreen.tsx create mode 100644 src/features/VideoPlayer/hooks/useSlider.tsx create mode 100644 src/features/VideoPlayer/hooks/useVolume.tsx create mode 100644 src/features/VideoPlayer/index.tsx create mode 100644 src/features/VideoPlayer/styled.tsx create mode 100644 src/helpers/secondsToHms/__tests__/index.tsx create mode 100644 src/helpers/secondsToHms/index.tsx diff --git a/package.json b/package.json index 4f53754f..ad47603a 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/public/images/player-fullscreen-off.svg b/public/images/player-fullscreen-off.svg new file mode 100644 index 00000000..91bb6bdf --- /dev/null +++ b/public/images/player-fullscreen-off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/player-fullscreen-on.svg b/public/images/player-fullscreen-on.svg new file mode 100644 index 00000000..53315974 --- /dev/null +++ b/public/images/player-fullscreen-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/player-pause.svg b/public/images/player-pause.svg new file mode 100644 index 00000000..806217ed --- /dev/null +++ b/public/images/player-pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/player-play.svg b/public/images/player-play.svg new file mode 100644 index 00000000..8ba6d772 --- /dev/null +++ b/public/images/player-play.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/player-prev.svg b/public/images/player-prev.svg new file mode 100644 index 00000000..88b896d3 --- /dev/null +++ b/public/images/player-prev.svg @@ -0,0 +1,5 @@ + + + + diff --git a/public/images/player-settings.svg b/public/images/player-settings.svg new file mode 100644 index 00000000..4bbcfc2a --- /dev/null +++ b/public/images/player-settings.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/player-volume-off.svg b/public/images/player-volume-off.svg new file mode 100644 index 00000000..fd03f7bc --- /dev/null +++ b/public/images/player-volume-off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/player-volume-on.svg b/public/images/player-volume-on.svg new file mode 100644 index 00000000..a3fb34bd --- /dev/null +++ b/public/images/player-volume-on.svg @@ -0,0 +1,5 @@ + + + + diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 919190e9..971edfc6 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -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 = () => ( + + + ) diff --git a/src/features/MatchPage/styled.tsx b/src/features/MatchPage/styled.tsx index 75cdf835..8c42984b 100644 --- a/src/features/MatchPage/styled.tsx +++ b/src/features/MatchPage/styled.tsx @@ -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; ` diff --git a/src/features/Menu/styled.tsx b/src/features/Menu/styled.tsx index 11b6e771..e5a54910 100644 --- a/src/features/Menu/styled.tsx +++ b/src/features/Menu/styled.tsx @@ -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: ''; diff --git a/src/features/VideoPlayer/components/ProgressBar/index.tsx b/src/features/VideoPlayer/components/ProgressBar/index.tsx new file mode 100644 index 00000000..1bc9e6c7 --- /dev/null +++ b/src/features/VideoPlayer/components/ProgressBar/index.tsx @@ -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 ( + + + + + + + + ) +} diff --git a/src/features/VideoPlayer/components/ProgressBar/styled.tsx b/src/features/VideoPlayer/components/ProgressBar/styled.tsx new file mode 100644 index 00000000..d43ab47c --- /dev/null +++ b/src/features/VideoPlayer/components/ProgressBar/styled.tsx @@ -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` + position: absolute; + z-index: 1; + background-color: rgba(255, 255, 255, 0.6);; + height: 100%; + width: ${({ value }) => value}%; +` + +export const PlayedProgress = styled.div` + position: absolute; + z-index: 2; + background-color: #F2C94C; + height: 100%; + width: ${({ value }) => value}%; +` + +export const Scrubber = styled.button` + 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; +` diff --git a/src/features/VideoPlayer/components/TimeTooltip/index.tsx b/src/features/VideoPlayer/components/TimeTooltip/index.tsx new file mode 100644 index 00000000..3738043a --- /dev/null +++ b/src/features/VideoPlayer/components/TimeTooltip/index.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +import { Wrapper, Time } from './styled' + +type Props = { + time: string, +} + +export const TimeTooltip = ({ time }: Props) => ( + + + +) diff --git a/src/features/VideoPlayer/components/TimeTooltip/styled.tsx b/src/features/VideoPlayer/components/TimeTooltip/styled.tsx new file mode 100644 index 00000000..893cd5c3 --- /dev/null +++ b/src/features/VideoPlayer/components/TimeTooltip/styled.tsx @@ -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; +` diff --git a/src/features/VideoPlayer/components/VolumeBar/index.tsx b/src/features/VideoPlayer/components/VolumeBar/index.tsx new file mode 100644 index 00000000..51d2806a --- /dev/null +++ b/src/features/VideoPlayer/components/VolumeBar/index.tsx @@ -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 ( + + + + + + + + ) +} diff --git a/src/features/VideoPlayer/components/VolumeBar/styled.tsx b/src/features/VideoPlayer/components/VolumeBar/styled.tsx new file mode 100644 index 00000000..784a02f8 --- /dev/null +++ b/src/features/VideoPlayer/components/VolumeBar/styled.tsx @@ -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` + 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` + height: 100%; + width: ${({ value }) => value}%; + background-color: #fff; +` + +export const Scrubber = styled.button` + 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; +` diff --git a/src/features/VideoPlayer/config.tsx b/src/features/VideoPlayer/config.tsx new file mode 100644 index 00000000..490efda1 --- /dev/null +++ b/src/features/VideoPlayer/config.tsx @@ -0,0 +1,12 @@ +export const playerConfig = { + file: { + forceHLS: true, + hlsOptions: { + liveSyncDuration: 10, + maxBufferLength: 10, + maxBufferSize: 0, + }, + }, +} + +export const progressCallbackInterval = 100 diff --git a/src/features/VideoPlayer/hooks/index.tsx b/src/features/VideoPlayer/hooks/index.tsx new file mode 100644 index 00000000..1ae88352 --- /dev/null +++ b/src/features/VideoPlayer/hooks/index.tsx @@ -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(null) + const playerRef = useRef(null) + + const startPlaying = () => { + setReady(true) + setPlaying(true) + } + + const togglePlaying = () => { + if (ready) { + setPlaying(!playing) + } + } + + const onPlayerClick = (e: MouseEvent) => { + 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(), + } +} diff --git a/src/features/VideoPlayer/hooks/useFullscreen.tsx b/src/features/VideoPlayer/hooks/useFullscreen.tsx new file mode 100644 index 00000000..cb8004ed --- /dev/null +++ b/src/features/VideoPlayer/hooks/useFullscreen.tsx @@ -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) => { + 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 } +} diff --git a/src/features/VideoPlayer/hooks/useSlider.tsx b/src/features/VideoPlayer/hooks/useSlider.tsx new file mode 100644 index 00000000..3598fc72 --- /dev/null +++ b/src/features/VideoPlayer/hooks/useSlider.tsx @@ -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(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 +} diff --git a/src/features/VideoPlayer/hooks/useVolume.tsx b/src/features/VideoPlayer/hooks/useVolume.tsx new file mode 100644 index 00000000..9d3802bf --- /dev/null +++ b/src/features/VideoPlayer/hooks/useVolume.tsx @@ -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, + } +} diff --git a/src/features/VideoPlayer/index.tsx b/src/features/VideoPlayer/index.tsx new file mode 100644 index 00000000..7d2cd07a --- /dev/null +++ b/src/features/VideoPlayer/index.tsx @@ -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 ( + + + + + + + + + + + + + ) +} diff --git a/src/features/VideoPlayer/styled.tsx b/src/features/VideoPlayer/styled.tsx new file mode 100644 index 00000000..6d5490e4 --- /dev/null +++ b/src/features/VideoPlayer/styled.tsx @@ -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` + 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)` + 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)` + 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); +` diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx index dcebcabb..2254fefc 100644 --- a/src/helpers/index.tsx +++ b/src/helpers/index.tsx @@ -8,3 +8,4 @@ export * from './getSportColor' export * from './getSportLexic' export * from './handleImg' export * from './msToMinutesAndSeconds' +export * from './secondsToHms' diff --git a/src/helpers/secondsToHms/__tests__/index.tsx b/src/helpers/secondsToHms/__tests__/index.tsx new file mode 100644 index 00000000..108e4f6e --- /dev/null +++ b/src/helpers/secondsToHms/__tests__/index.tsx @@ -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') + }) +}) diff --git a/src/helpers/secondsToHms/index.tsx b/src/helpers/secondsToHms/index.tsx new file mode 100644 index 00000000..a18e654b --- /dev/null +++ b/src/helpers/secondsToHms/index.tsx @@ -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}` +} diff --git a/src/hooks/useToggle.tsx b/src/hooks/useToggle.tsx index a2827d0f..f05f3d84 100644 --- a/src/hooks/useToggle.tsx +++ b/src/hooks/useToggle.tsx @@ -9,6 +9,7 @@ export const useToggle = () => { close, isOpen, open, + setIsOpen, toggle, } }