From dfd0d71b3bcee782fd8e746ea04b9a99768ea09b Mon Sep 17 00:00:00 2001 From: karela Date: Wed, 19 Oct 2022 09:58:34 +0400 Subject: [PATCH] feat(ott-2852): add circle animation --- src/features/CircleAnimationBar/helpers.tsx | 9 +++ src/features/CircleAnimationBar/index.tsx | 72 +++++++++++++++++++ src/features/CircleAnimationBar/styled.tsx | 43 +++++++++++ .../components/FinishedMatch/index.tsx | 8 ++- src/features/MatchPage/store/hooks/index.tsx | 8 +++ .../components/EventsList/index.tsx | 3 + .../components/TabEvents/index.tsx | 29 +++++++- src/features/MatchSidePlaylists/index.tsx | 7 ++ .../MultiSourcePlayer/hooks/index.tsx | 17 +++++ 9 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 src/features/CircleAnimationBar/helpers.tsx create mode 100644 src/features/CircleAnimationBar/index.tsx create mode 100644 src/features/CircleAnimationBar/styled.tsx diff --git a/src/features/CircleAnimationBar/helpers.tsx b/src/features/CircleAnimationBar/helpers.tsx new file mode 100644 index 00000000..4ac375a2 --- /dev/null +++ b/src/features/CircleAnimationBar/helpers.tsx @@ -0,0 +1,9 @@ +import reduce from 'lodash/reduce' + +import type { Events } from 'requests' + +export const fullEpisodesDuration = (filters: Events) => reduce( + filters, + (acc, filter) => acc + (Number(filter.e) - Number(filter.s)), + 0, +) diff --git a/src/features/CircleAnimationBar/index.tsx b/src/features/CircleAnimationBar/index.tsx new file mode 100644 index 00000000..6bf1acca --- /dev/null +++ b/src/features/CircleAnimationBar/index.tsx @@ -0,0 +1,72 @@ +import type { Dispatch, SetStateAction } from 'react' +import { useEffect } from 'react' + +import isEmpty from 'lodash/isEmpty' + +import type { Events } from 'requests' + +import { fullEpisodesDuration } from './helpers' +import { Svg, Circle } from './styled' + +export type TCircleAnimation = { + plaingOrder: number, + playedProgress: number, + playing: boolean, + ready: boolean, +} + +export const initialCircleAnimation: TCircleAnimation = { + plaingOrder: 0, + playedProgress: 0, + playing: false, + ready: false, +} + +type Props = { + circleAnimation?: TCircleAnimation, + filteredEvents: Events, + setWatchAllEpisodesTimer: (showTimer: boolean) => void, +} + +export type TSetCircleAnimation = Dispatch> + +export const CircleAnimationBar = ({ + circleAnimation, + filteredEvents, + setWatchAllEpisodesTimer, +}: Props) => { + const { + plaingOrder, + playedProgress, + playing, + ready, + } = circleAnimation! + const timeOfAllEpisodes = fullEpisodesDuration(filteredEvents) + const remainingEvents = filteredEvents.slice(plaingOrder - 1) + const fullTimeOfRemainingEpisodes = !isEmpty(remainingEvents) + ? fullEpisodesDuration(remainingEvents) + : 0 + const animationPause = !playing || !ready + + const currentAnimationTime = Math.round(fullTimeOfRemainingEpisodes - (playedProgress / 1000)) + const currentEpisodesPercent = 100 - (100 / (timeOfAllEpisodes / currentAnimationTime)) + + useEffect(() => { + if (currentEpisodesPercent >= 100) { + setWatchAllEpisodesTimer(false) + } + }, [currentEpisodesPercent, setWatchAllEpisodesTimer]) + + return ( + + + + ) +} diff --git a/src/features/CircleAnimationBar/styled.tsx b/src/features/CircleAnimationBar/styled.tsx new file mode 100644 index 00000000..230f5734 --- /dev/null +++ b/src/features/CircleAnimationBar/styled.tsx @@ -0,0 +1,43 @@ +import styled, { keyframes } from 'styled-components/macro' + +type TCircle = { + animationPause?: boolean, + currentAnimationTime: number, + currentEpisodesPercent: number, +} + +const strokeDashOffset = 43.5 + +const clockAnimation = (currentEpisodesPercent?: number) => keyframes` + from { + stroke-dashoffset: ${currentEpisodesPercent}; + } + to { + stroke-dashoffset: 0; + } +` + +export const Svg = styled.svg` + background-color: #5EB2FF; + width: 14px; + height: 14px; + position: relative; + border-radius: 50%; +` + +export const Circle = styled.circle` + fill: transparent; + stroke: white; + stroke-width: 14px; + stroke-dasharray: ${strokeDashOffset}; + stroke-dashoffset: ${strokeDashOffset}; + transform: rotate(-90deg); + transform-origin: center; + animation-name: ${({ currentEpisodesPercent }) => ( + clockAnimation(strokeDashOffset - (strokeDashOffset * currentEpisodesPercent / 100)) + )}; + animation-duration: ${({ currentAnimationTime }) => `${currentAnimationTime}s`}; + animation-play-state: ${({ animationPause }) => (animationPause ? 'paused' : 'running')}; + animation-timing-function: linear; + animation-fill-mode: forwards; +` diff --git a/src/features/MatchPage/components/FinishedMatch/index.tsx b/src/features/MatchPage/components/FinishedMatch/index.tsx index 9d11218a..d10f60d0 100644 --- a/src/features/MatchPage/components/FinishedMatch/index.tsx +++ b/src/features/MatchPage/components/FinishedMatch/index.tsx @@ -1,7 +1,9 @@ -import { Fragment } from 'react' +import { Fragment, useState } from 'react' import isEmpty from 'lodash/isEmpty' +import type { TCircleAnimation } from 'features/CircleAnimationBar' +import { initialCircleAnimation } from 'features/CircleAnimationBar' import { MatchSidePlaylists } from 'features/MatchSidePlaylists' import { MultiSourcePlayer } from 'features/MultiSourcePlayer' @@ -14,6 +16,7 @@ import { MatchDescription } from '../MatchDescription' import { useMatchPageStore } from '../../store' export const FinishedMatch = () => { + const [circleAnimation, setCircleAnimation] = useState(initialCircleAnimation) const { isOpenPopup, profile } = useMatchPageStore() const { chapters, @@ -44,6 +47,7 @@ export const FinishedMatch = () => { {!isEmpty(chapters) && ( { diff --git a/src/features/MatchPage/store/hooks/index.tsx b/src/features/MatchPage/store/hooks/index.tsx index e84766a5..8b472265 100644 --- a/src/features/MatchPage/store/hooks/index.tsx +++ b/src/features/MatchPage/store/hooks/index.tsx @@ -24,6 +24,7 @@ import { useTabEvents } from './useTabEvents' export const useMatchPage = () => { const [matchProfile, setMatchProfile] = useState(null) + const [watchAllEpisodesTimer, setWatchAllEpisodesTimer] = useState(false) const { profileId: matchId, sportType } = usePageParams() const { close: hideProfileCard, @@ -124,6 +125,10 @@ export const useMatchPage = () => { setPlaingOrder(currentOrder + 1) } const playEpisodes = () => { + if (!watchAllEpisodesTimer) { + setWatchAllEpisodesTimer(true) + } + setIsPlayinFiltersEpisodes(true) if (matchProfile?.live) { @@ -162,6 +167,7 @@ export const useMatchPage = () => { likeImage, likeToggle, matchPlaylists, + plaingOrder, playEpisodes, playNextEpisode, profile, @@ -175,10 +181,12 @@ export const useMatchPage = () => { setPlaingOrder, setReversed, setUnreversed, + setWatchAllEpisodesTimer, showProfileCard, toggleActiveEvents, toggleActivePlayers, togglePopup, tournamentData, + watchAllEpisodesTimer, } } diff --git a/src/features/MatchSidePlaylists/components/EventsList/index.tsx b/src/features/MatchSidePlaylists/components/EventsList/index.tsx index 4976aca0..d3af823e 100644 --- a/src/features/MatchSidePlaylists/components/EventsList/index.tsx +++ b/src/features/MatchSidePlaylists/components/EventsList/index.tsx @@ -26,6 +26,7 @@ type Props = { onSelect: (option: PlaylistOption) => void, profile: MatchInfo, selectedPlaylist?: PlaylistOption, + setWatchAllEpisodesTimer: (showTimer: boolean) => void, } export const EventsList = ({ @@ -34,6 +35,7 @@ export const EventsList = ({ onSelect, profile, selectedPlaylist, + setWatchAllEpisodesTimer, }: Props) => ( {map(events, (event) => { @@ -67,6 +69,7 @@ export const EventsList = ({ const eventWithoutClick = isLffClient && profile?.live const eventClick = () => { + setWatchAllEpisodesTimer(false) !eventWithoutClick && onSelect(eventPlaylist) if (disablePlayingEpisodes) { disablePlayingEpisodes() diff --git a/src/features/MatchSidePlaylists/components/TabEvents/index.tsx b/src/features/MatchSidePlaylists/components/TabEvents/index.tsx index 719f1f95..205e0ff2 100644 --- a/src/features/MatchSidePlaylists/components/TabEvents/index.tsx +++ b/src/features/MatchSidePlaylists/components/TabEvents/index.tsx @@ -1,4 +1,4 @@ -import { Fragment } from 'react' +import { Fragment, useEffect } from 'react' import isEmpty from 'lodash/isEmpty' import map from 'lodash/map' @@ -8,6 +8,8 @@ import size from 'lodash/size' import { T9n } from 'features/T9n' import type { PlaylistOption } from 'features/MatchPage/types' import { useMatchPageStore } from 'features/MatchPage/store' +import type { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar' +import { CircleAnimationBar } from 'features/CircleAnimationBar' import type { MatchInfo } from 'requests' import { EventsList } from '../EventsList' @@ -28,31 +30,48 @@ import { } from './styled' type Props = { + circleAnimation?: TCircleAnimation, onSelect: (option: PlaylistOption) => void, profile: MatchInfo, selectedPlaylist?: PlaylistOption, + setCircleAnimation?: TSetCircleAnimation, } export const TabEvents = ({ + circleAnimation, onSelect, profile, selectedPlaylist, + setCircleAnimation, }: Props) => { const { activeStatus, countOfFilters, disablePlayingEpisodes, + filteredEvents, isEmptyFilters, isLiveMatch, likeImage, likeToggle, + plaingOrder, playEpisodes, reversedGroupEvents, setReversed, setUnreversed, + setWatchAllEpisodesTimer, togglePopup, + watchAllEpisodesTimer, } = useMatchPageStore() + useEffect(() => { + if (setCircleAnimation) { + setCircleAnimation((state) => ({ + ...state, + plaingOrder, + })) + } + }, [setCircleAnimation, plaingOrder]) + if (!profile) return null return ( @@ -85,6 +104,13 @@ export const TabEvents = ({ + {watchAllEpisodesTimer && ( + + )} )} {isEmpty(reversedGroupEvents) @@ -111,6 +137,7 @@ export const TabEvents = ({ onSelect={onSelect} profile={profile} selectedPlaylist={selectedPlaylist} + setWatchAllEpisodesTimer={setWatchAllEpisodesTimer} /> ) diff --git a/src/features/MatchSidePlaylists/index.tsx b/src/features/MatchSidePlaylists/index.tsx index f4eb1acb..501b9681 100644 --- a/src/features/MatchSidePlaylists/index.tsx +++ b/src/features/MatchSidePlaylists/index.tsx @@ -4,6 +4,7 @@ import { useState, } from 'react' +import type { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar' import type { PlaylistOption } from 'features/MatchPage/types' import { Tab, TabsGroup } from 'features/Common' import { T9n } from 'features/T9n' @@ -31,13 +32,17 @@ const tabPanes = { } type Props = { + circleAnimation?: TCircleAnimation, onSelect: (option: PlaylistOption) => void, selectedPlaylist?: PlaylistOption, + setCircleAnimation?: TSetCircleAnimation, } export const MatchSidePlaylists = ({ + circleAnimation, onSelect, selectedPlaylist, + setCircleAnimation, }: Props) => { const { hideProfileCard, @@ -128,6 +133,8 @@ export const MatchSidePlaylists = ({ forVideoTab={selectedTab === Tabs.VIDEO} > void, onPlayingChange: (playing: boolean) => void, profile: MatchInfo, + setCircleAnimation: TSetCircleAnimation, } export const useMultiSourcePlayer = ({ chapters, onError, onPlayingChange, + setCircleAnimation, }: Props) => { const { isPlayFilterEpisodes, @@ -195,6 +198,20 @@ export const useMultiSourcePlayer = ({ onPlayingChange(playing) }, [playing, onPlayingChange]) + useEffect(() => { + setCircleAnimation((state) => ({ + ...state, + playedProgress, + playing, + ready, + })) + }, [ + playedProgress, + playing, + ready, + setCircleAnimation, + ]) + useEffect(() => { setPlayerState((state) => ({ ...initialState,