From b14a4e3d8930de6f88a61234f945ea0933ee5ccd Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Fri, 22 Nov 2024 10:07:38 +0500 Subject: [PATCH 1/3] fix(episode): render Episode on the left hand side panel --- .../components/MediaPlayer/ToolBar/index.tsx | 250 +++++++++++++++ .../mindset/components/MediaPlayer/index.tsx | 295 ++++++++++++++++++ .../mindset/components/sidebar/index.tsx | 56 ++++ src/modules/mindset/index.tsx | 2 + 4 files changed, 603 insertions(+) create mode 100644 src/modules/mindset/components/MediaPlayer/ToolBar/index.tsx create mode 100644 src/modules/mindset/components/MediaPlayer/index.tsx create mode 100644 src/modules/mindset/components/sidebar/index.tsx diff --git a/src/modules/mindset/components/MediaPlayer/ToolBar/index.tsx b/src/modules/mindset/components/MediaPlayer/ToolBar/index.tsx new file mode 100644 index 000000000..1ca5f3319 --- /dev/null +++ b/src/modules/mindset/components/MediaPlayer/ToolBar/index.tsx @@ -0,0 +1,250 @@ +import { IconButton, Slider } from '@mui/material' +import { FC, useState } from 'react' +import styled from 'styled-components' +import ExitFullScreen from '~/components/Icons/ExitFullScreen' +import FullScreenIcon from '~/components/Icons/FullScreenIcon' +import PauseIcon from '~/components/Icons/PauseIcon' +import PlayIcon from '~/components/Icons/PlayIcon' +import VolumeIcon from '~/components/Icons/VolumeIcon' +import MuteVolumeIcon from '~/components/Icons/MuteVolumeIcon' +import { Flex } from '~/components/common/Flex' +import { colors } from '~/utils' +import { secondsToMediaTime } from '~/utils/secondsToMediaTime' + +type Props = { + isPlaying: boolean + isFullScreen: boolean + setIsPlaying: () => void + handleProgressChange: (_: Event, value: number | number[]) => void + handleVolumeChange: (_: Event, value: number | number[]) => void + playingTime: number + duration: number + onFullScreenClick: () => void + showToolbar: boolean +} + +export const Toolbar: FC = ({ + isPlaying, + isFullScreen, + setIsPlaying, + playingTime, + duration, + handleProgressChange, + handleVolumeChange, + onFullScreenClick, + showToolbar, +}) => { + const [volume, setVolume] = useState(0.5) + const [isMuted, setIsMuted] = useState(false) + const [previousVolume, setPreviousVolume] = useState(0.5) + + const volumeChangeHandler = (event: Event, value: number | number[]) => { + const newValue = Array.isArray(value) ? value[0] : value + + setVolume(newValue) + handleVolumeChange(event, newValue) + + if (isMuted) { + setIsMuted(false) + } + } + + const toggleMute = () => { + if (isMuted) { + setVolume(previousVolume) + handleVolumeChange(new Event('input'), previousVolume) + } else { + setPreviousVolume(volume) + setVolume(0) + handleVolumeChange(new Event('input'), 0) + } + + setIsMuted(!isMuted) + } + + return ( + + {(!showToolbar || isFullScreen) && ( + + )} + + + + {!isPlaying ? : } + + + {secondsToMediaTime(playingTime)} + / + {secondsToMediaTime(duration)} + + + + + + {isMuted ? ( + + + + ) : ( + + )} + + + + {!isFullScreen ? : } + + + + ) +} + +const Wrapper = styled(Flex)<{ showToolbar: boolean }>` + height: 60px; + padding: 12px 16px; + ${(props) => + props.showToolbar && + ` + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index:1; + background-color: rgba(0, 0, 0, 0.6); + `} + + &.error-wrapper { + color: ${colors.primaryRed}; + } +` + +const VolumeWrapper = styled.span`` + +const MuteVolumeWrapper = styled.span` + color: gray; +` + +const Action = styled(IconButton)` + && { + font-size: 36px; + padding: 2px; + margin-left: 8px; + } +` + +const VolumeControl = styled(Flex)` + height: 28px; + font-size: 26px; + border-radius: 200px; + color: ${colors.white}; + margin-left: auto; + + .volume-slider { + display: none; + color: ${colors.white}; + height: 3px; + .MuiSlider-track { + border: none; + } + .MuiSlider-thumb { + width: 2px; + height: 10px; + background-color: ${colors.white}; + &:before { + box-shadow: '0 4px 8px rgba(0,0,0,0.4)'; + } + &:hover, + &.Mui-focusVisible, + &.Mui-active { + box-shadow: none; + } + } + } + + &:hover { + background: rgba(42, 44, 55, 1); + .volume-slider { + width: 62px; + margin-right: 4px; + display: block; + } + } +` + +const Fullscreen = styled(Flex)` + cursor: pointer; + padding: 8px; + font-size: 32px; + color: #d9d9d9; +` + +const ProgressSlider = styled(Slider)<{ isFullScreen: boolean }>` + && { + z-index: 20; + color: ${colors.white}; + height: 3px; + width: calc(100% - 12px); + margin: ${(props) => (props.isFullScreen ? '80px auto' : '-12px auto')}; + box-sizing: border-box; + + ${(props) => + props.isFullScreen && + ` + width: calc(100% - 80px) + padding: 12px auto; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index:1; + `} + + .MuiSlider-track { + border: none; + } + .MuiSlider-thumb { + width: 10px; + height: 10px; + background-color: ${colors.white}; + &:before { + box-shadow: '0 4px 8px rgba(0,0,0,0.4)'; + } + &:hover, + &.Mui-focusVisible, + &.Mui-active { + box-shadow: none; + } + } + } +` + +const TimeStamp = styled(Flex)` + color: ${colors.white}; + font-size: 13px; + margin-left: 16px; + font-weight: 500; + + .separator { + color: ${colors.GRAY6}; + margin: 0 4px; + } + + .duration { + color: ${colors.GRAY6}; + } +` diff --git a/src/modules/mindset/components/MediaPlayer/index.tsx b/src/modules/mindset/components/MediaPlayer/index.tsx new file mode 100644 index 000000000..60bde0e04 --- /dev/null +++ b/src/modules/mindset/components/MediaPlayer/index.tsx @@ -0,0 +1,295 @@ +import { memo, useEffect, useRef, useState } from 'react' +import ReactPlayer from 'react-player' +import { ClipLoader } from 'react-spinners' +import styled from 'styled-components' +import { Avatar } from '~/components/common/Avatar' +import { Flex } from '~/components/common/Flex' +import { usePlayerStore } from '~/stores/usePlayerStore' +import { colors, videoTimeToSeconds } from '~/utils' +import { Toolbar } from './ToolBar' +import { useSelectedNode } from '~/stores/useGraphStore' + +type FullScreenProps = { + isFullScreen: boolean +} + +const MediaPlayerComponent = () => { + const playerRef = useRef(null) + const wrapperRef = useRef(null) + const [isFocused, setIsFocused] = useState(false) + const [isFullScreen, setIsFullScreen] = useState(false) + const [isMouseNearBottom, setIsMouseNearBottom] = useState(false) + const [status, setStatus] = useState<'buffering' | 'error' | 'ready'>('ready') + const [isReady, setIsReady] = useState(false) + const [NodeStartTime, setNodeStartTime] = useState('') + const [hasSeekedToStart, setHasSeekedToStart] = useState(false) + const selectedNode = useSelectedNode() + + useEffect(() => { + const timestamp = '00:02:00-00:12:00' + + const startTime = timestamp?.split('-')[0] as string + + setNodeStartTime(startTime as string) + }, [selectedNode]) + + const { + isPlaying, + playingTime, + duration, + setIsPlaying, + setPlayingTime, + setDuration, + playingNode, + volume, + setVolume, + setHasError, + resetPlayer, + isSeeking, + setIsSeeking, + } = usePlayerStore((s) => s) + + const mediaUrl = 'https://www.youtube.com/watch?v=o8Y0E5sPHr4' + + const isYouTubeVideo = true + + useEffect(() => () => resetPlayer(), [resetPlayer]) + + useEffect(() => { + if (playingNode && !isReady) { + setPlayingTime(0) + setDuration(0) + setIsReady(false) + setHasSeekedToStart(false) + } + }, [playingNode, setPlayingTime, setDuration, setIsReady, isReady]) + + useEffect(() => { + if (isSeeking && playerRef.current) { + playerRef.current.seekTo(playingTime, 'seconds') + setIsSeeking(false) + } + }, [playingTime, isSeeking, setIsSeeking]) + + useEffect(() => { + if (isReady && NodeStartTime && playerRef.current && !hasSeekedToStart) { + const startTimeInSeconds = videoTimeToSeconds(NodeStartTime) + + playerRef.current.seekTo(startTimeInSeconds, 'seconds') + setPlayingTime(startTimeInSeconds) + setHasSeekedToStart(true) + } + }, [isReady, NodeStartTime, setPlayingTime, hasSeekedToStart]) + + const togglePlay = () => { + setIsPlaying(!isPlaying) + } + + const handlePlay = () => { + setIsPlaying(true) + } + + const handlePause = () => { + setIsPlaying(false) + } + + const handleProgressChange = (_: Event, value: number | number[]) => { + const newValue = Array.isArray(value) ? value[0] : value + + setPlayingTime(newValue) + + if (playerRef.current && !isSeeking) { + playerRef.current.seekTo(newValue, 'seconds') + } + } + + const handleVolumeChange = (_: Event, value: number | number[]) => { + const newValue = Array.isArray(value) ? value[0] : value + + setVolume(newValue) + } + + const handleError = () => { + setHasError(true) + setStatus('error') + } + + const handleProgress = (progress: { playedSeconds: number }) => { + if (!isSeeking) { + const currentTime = progress.playedSeconds + + setPlayingTime(currentTime) + } + } + + const handleReady = () => { + if (playerRef.current) { + setStatus('ready') + + const videoDuration = playerRef.current.getDuration() + + setDuration(videoDuration) + + if (NodeStartTime && !hasSeekedToStart) { + const startTimeInSeconds = videoTimeToSeconds(NodeStartTime) + + playerRef.current.seekTo(startTimeInSeconds, 'seconds') + setPlayingTime(startTimeInSeconds) + setHasSeekedToStart(true) + } + } + } + + const toggleFullScreen = () => { + if (wrapperRef.current) { + if (!document.fullscreenElement) { + wrapperRef.current.requestFullscreen().then(() => { + document.addEventListener('fullscreenchange', handleFullScreenChange) + }) + } else { + document.exitFullscreen() + setTimeout(() => setIsFullScreen(false), 300) + } + } + } + + const handleFullScreenChange = () => { + setIsFullScreen(!!document.fullscreenElement) + document.removeEventListener('fullscreenchange', handleFullScreenChange) + } + + useEffect(() => () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange) + }) + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + if (isFullScreen) { + const windowHeight = window.screen.height + const mousePositionY = event.clientY + const distanceFromBottom = windowHeight - mousePositionY + const threshold = 50 + + setIsMouseNearBottom(distanceFromBottom <= threshold) + } + } + + document.addEventListener('mousemove', handleMouseMove) + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + } + }, [isFullScreen, isMouseNearBottom]) + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (isFullScreen && event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + } else if (isFocused && event.key === ' ') { + event.preventDefault() + togglePlay() + } + } + + document.addEventListener('fullscreenchange', handleFullScreenChange) + document.addEventListener('keydown', handleKeyDown) + + return () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange) + document.removeEventListener('keydown', handleKeyDown) + } + }) + + const handlePlayerClick = () => { + togglePlay() + } + + return mediaUrl ? ( + setIsFocused(false)} onFocus={() => setIsFocused(true)} tabIndex={0}> + + + + + setStatus('buffering')} + onBufferEnd={() => setStatus('ready')} + onError={handleError} + onPause={handlePause} + onPlay={handlePlay} + onProgress={handleProgress} + onReady={handleReady} + playing={isPlaying} + url={mediaUrl || ''} + volume={volume} + width="100%" + /> + + {status === 'error' ? ( + Error happened, please try later + ) : null} + {status === 'ready' ? ( + + ) : null} + {status === 'buffering' && !isYouTubeVideo ? ( + + + + ) : null} + + ) : null +} + +const Wrapper = styled(Flex)` + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + background: rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; + height: auto; + &:focus { + outline: none; + } +` + +const Cover = styled(Flex)` + position: absolute; + top: ${(props) => (props.isFullScreen ? '38%' : '18%')}; + left: 50%; + transform: translateX(-50%); + z-index: -1; +` + +const Buffering = styled(Flex)` + position: absolute; + top: ${(props) => (props.isFullScreen ? '43%' : '39%')}; + left: 50%; + transform: translateX(-50%); + z-index: 1; +` + +const ErrorWrapper = styled(Flex)` + height: 60px; + padding: 12px 16px; + color: ${colors.primaryRed}; +` + +const PlayerWrapper = styled.div<{ isFullScreen: boolean }>` + margin: ${(props) => (props.isFullScreen ? '80px auto' : '0')}; + width: 100%; + cursor: pointer; +` + +export const MediaPlayer = memo(MediaPlayerComponent) diff --git a/src/modules/mindset/components/sidebar/index.tsx b/src/modules/mindset/components/sidebar/index.tsx new file mode 100644 index 000000000..1f4096038 --- /dev/null +++ b/src/modules/mindset/components/sidebar/index.tsx @@ -0,0 +1,56 @@ +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import { colors } from '~/utils' +import { MENU_WIDTH } from '~/components/App/SideBar' +import { Text } from '~/components/common/Text' +import { MediaPlayer } from '~/modules/mindset/components/MediaPlayer' + +export const SideBar = () => ( + + What is the Deep State Party and what are their goals? + John Mearsheimer and Jeffrey Sachs | All-In Summit 2024 + + + + +) + +const Wrapper = styled(Flex)(({ theme }) => ({ + position: 'relative', + display: 'flex', + alignItems: 'flex-start', + padding: '20px', + background: colors.BG1, + height: '100vh', + width: '100%', + zIndex: 30, + [theme.breakpoints.up('sm')]: { + width: MENU_WIDTH, + }, +})) + +const Summary = styled(Text)` + font-size: 20px; + font-weight: 700; + overflow-wrap: break-word; + white-space: normal; + word-break: break-word; + margin-right: 10px; +` + +const EpisodeTitle = styled(Text)` + margin-top: 15px; + font-size: 14px; + font-weight: 500; + line-height: 19.6px; +` + +const MediaWrapper = styled(Flex)(({ theme }) => ({ + background: colors.BG1, + width: '100%', + margin: '16px auto', + zIndex: 29, + [theme.breakpoints.up('sm')]: { + width: '390px', + }, +})) diff --git a/src/modules/mindset/index.tsx b/src/modules/mindset/index.tsx index 25e8ac196..ead6335a5 100644 --- a/src/modules/mindset/index.tsx +++ b/src/modules/mindset/index.tsx @@ -1,8 +1,10 @@ import { Flex } from '~/components/common/Flex' import { Header } from './components/header' +import { SideBar } from './components/sidebar' export const MindSet = () => (
+ ) From 28b3b34c69e25c2f6ffd30631ad525f543cb7f26 Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Fri, 22 Nov 2024 15:09:20 +0500 Subject: [PATCH 2/3] fix(put-node): remove background --- src/modules/mindset/components/sidebar/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/mindset/components/sidebar/index.tsx b/src/modules/mindset/components/sidebar/index.tsx index 1f4096038..74b4ca81b 100644 --- a/src/modules/mindset/components/sidebar/index.tsx +++ b/src/modules/mindset/components/sidebar/index.tsx @@ -20,7 +20,7 @@ const Wrapper = styled(Flex)(({ theme }) => ({ display: 'flex', alignItems: 'flex-start', padding: '20px', - background: colors.BG1, + background: 'transparent', height: '100vh', width: '100%', zIndex: 30, @@ -32,6 +32,7 @@ const Wrapper = styled(Flex)(({ theme }) => ({ const Summary = styled(Text)` font-size: 20px; font-weight: 700; + line-height: 24.2px; overflow-wrap: break-word; white-space: normal; word-break: break-word; @@ -39,10 +40,10 @@ const Summary = styled(Text)` ` const EpisodeTitle = styled(Text)` - margin-top: 15px; + margin-top: 20px; font-size: 14px; font-weight: 500; - line-height: 19.6px; + line-height: 16.94px; ` const MediaWrapper = styled(Flex)(({ theme }) => ({ From 29ec91d97fbc4a1dea9ddfd749817be923b0da5b Mon Sep 17 00:00:00 2001 From: MahtabBukhari Date: Fri, 22 Nov 2024 18:00:40 +0500 Subject: [PATCH 3/3] fix(put-node): update style layout --- .../mindset/components/MediaPlayer/index.tsx | 65 +------------------ .../mindset/components/sidebar/index.tsx | 4 +- 2 files changed, 6 insertions(+), 63 deletions(-) diff --git a/src/modules/mindset/components/MediaPlayer/index.tsx b/src/modules/mindset/components/MediaPlayer/index.tsx index 60bde0e04..ada20d42c 100644 --- a/src/modules/mindset/components/MediaPlayer/index.tsx +++ b/src/modules/mindset/components/MediaPlayer/index.tsx @@ -1,12 +1,12 @@ import { memo, useEffect, useRef, useState } from 'react' import ReactPlayer from 'react-player' -import { ClipLoader } from 'react-spinners' + import styled from 'styled-components' import { Avatar } from '~/components/common/Avatar' import { Flex } from '~/components/common/Flex' import { usePlayerStore } from '~/stores/usePlayerStore' import { colors, videoTimeToSeconds } from '~/utils' -import { Toolbar } from './ToolBar' + import { useSelectedNode } from '~/stores/useGraphStore' type FullScreenProps = { @@ -36,13 +36,11 @@ const MediaPlayerComponent = () => { const { isPlaying, playingTime, - duration, setIsPlaying, setPlayingTime, setDuration, playingNode, volume, - setVolume, setHasError, resetPlayer, isSeeking, @@ -51,8 +49,6 @@ const MediaPlayerComponent = () => { const mediaUrl = 'https://www.youtube.com/watch?v=o8Y0E5sPHr4' - const isYouTubeVideo = true - useEffect(() => () => resetPlayer(), [resetPlayer]) useEffect(() => { @@ -93,22 +89,6 @@ const MediaPlayerComponent = () => { setIsPlaying(false) } - const handleProgressChange = (_: Event, value: number | number[]) => { - const newValue = Array.isArray(value) ? value[0] : value - - setPlayingTime(newValue) - - if (playerRef.current && !isSeeking) { - playerRef.current.seekTo(newValue, 'seconds') - } - } - - const handleVolumeChange = (_: Event, value: number | number[]) => { - const newValue = Array.isArray(value) ? value[0] : value - - setVolume(newValue) - } - const handleError = () => { setHasError(true) setStatus('error') @@ -140,19 +120,6 @@ const MediaPlayerComponent = () => { } } - const toggleFullScreen = () => { - if (wrapperRef.current) { - if (!document.fullscreenElement) { - wrapperRef.current.requestFullscreen().then(() => { - document.addEventListener('fullscreenchange', handleFullScreenChange) - }) - } else { - document.exitFullscreen() - setTimeout(() => setIsFullScreen(false), 300) - } - } - } - const handleFullScreenChange = () => { setIsFullScreen(!!document.fullscreenElement) document.removeEventListener('fullscreenchange', handleFullScreenChange) @@ -214,7 +181,7 @@ const MediaPlayerComponent = () => { setStatus('buffering')} onBufferEnd={() => setStatus('ready')} onError={handleError} @@ -231,24 +198,6 @@ const MediaPlayerComponent = () => { {status === 'error' ? ( Error happened, please try later ) : null} - {status === 'ready' ? ( - - ) : null} - {status === 'buffering' && !isYouTubeVideo ? ( - - - - ) : null} ) : null } @@ -272,14 +221,6 @@ const Cover = styled(Flex)` z-index: -1; ` -const Buffering = styled(Flex)` - position: absolute; - top: ${(props) => (props.isFullScreen ? '43%' : '39%')}; - left: 50%; - transform: translateX(-50%); - z-index: 1; -` - const ErrorWrapper = styled(Flex)` height: 60px; padding: 12px 16px; diff --git a/src/modules/mindset/components/sidebar/index.tsx b/src/modules/mindset/components/sidebar/index.tsx index 74b4ca81b..45911c5c3 100644 --- a/src/modules/mindset/components/sidebar/index.tsx +++ b/src/modules/mindset/components/sidebar/index.tsx @@ -30,8 +30,9 @@ const Wrapper = styled(Flex)(({ theme }) => ({ })) const Summary = styled(Text)` + font-family: Inter; font-size: 20px; - font-weight: 700; + font-weight: Bold; line-height: 24.2px; overflow-wrap: break-word; white-space: normal; @@ -40,6 +41,7 @@ const Summary = styled(Text)` ` const EpisodeTitle = styled(Text)` + font-family: Inter; margin-top: 20px; font-size: 14px; font-weight: 500;