├── src ├── components │ ├── home │ │ ├── home.css │ │ └── Skeletons.js │ ├── navbar │ │ ├── navbar.css │ │ ├── SidenavSectionContainer.js │ │ ├── SidenavSectionTitle.js │ │ ├── OverflowSection.js │ │ ├── SidenavLinkSub.js │ │ ├── LoginSection.js │ │ ├── Socials.js │ │ ├── SidenavLink.js │ │ ├── SearchSuggestionContainer.js │ │ ├── SidenavUnactiveItems.js │ │ └── BottomNavbar.js │ ├── toast │ │ ├── toast.css │ │ ├── ToastMessage.js │ │ ├── Toast.js │ │ └── toast.scss │ ├── library │ │ ├── library.css │ │ └── components │ │ │ ├── VideosContainer.js │ │ │ ├── UserInfo.js │ │ │ └── SectionContainer.js │ ├── searchresults │ │ ├── search.css │ │ ├── SearchResults.js │ │ └── VideoResults.js │ ├── video │ │ ├── css │ │ │ ├── videomodal.css │ │ │ ├── videowrapper.css │ │ │ └── videomodal.scss │ │ ├── ModalItemTitle.js │ │ ├── VideoModalItem.js │ │ ├── VideoModalHeader.js │ │ ├── UserCommentImage.js │ │ ├── VideoModalHeaderButton.js │ │ ├── VideoModalHeaderSecondaryButton.js │ │ ├── UserData.js │ │ ├── VideoModalContainer.js │ │ ├── DescriptionText.js │ │ ├── VideoPlayerContainer.js │ │ ├── SaveButton.js │ │ ├── ShareButton.js │ │ ├── VideoComments.js │ │ ├── UserComment.js │ │ ├── MobileVolumeControl.js │ │ ├── LoadingSkeleton.js │ │ ├── VideoTitle.js │ │ ├── VideoDescriptions.js │ │ ├── VideoMiddleLayer.js │ │ ├── CommentSection.js │ │ ├── VoteButton.js │ │ ├── BufferedTime.js │ │ ├── VideoLayers.js │ │ ├── Comment.js │ │ ├── VideoWrapper.js │ │ ├── PlaybackSpeedContainer.js │ │ ├── CommentOptionsButton.js │ │ ├── CommentsVotesContainer.js │ │ ├── VideoModal.js │ │ └── LikesWrapper.js │ ├── relatedvideos │ │ ├── css │ │ │ └── relatedvideos.css │ │ ├── VideoCardContainer.js │ │ ├── ButtonContainer.js │ │ ├── LoadingCard.js │ │ ├── VideoThumbnail.js │ │ ├── VideoInfo.js │ │ ├── NextVideoSection.js │ │ └── RelatedVideoModal.js │ ├── subscriptions │ │ ├── ChannelsContainer.js │ │ ├── FeedChannelContainer.js │ │ ├── ChannelItemContainer.js │ │ ├── ChannelMetadata.js │ │ ├── SubscribeButton.js │ │ ├── subscriptions.css │ │ ├── ChannelItem.js │ │ └── subscriptions.scss │ ├── shared │ │ ├── css │ │ │ ├── overlay.scss │ │ │ ├── ownerbadges.scss │ │ │ ├── tooltip.css │ │ │ ├── tooltip.scss │ │ │ ├── notloggedscreen.scss │ │ │ ├── confirmactionmodal.css │ │ │ └── confirmactionmodal.scss │ │ ├── ButtonContainer.js │ │ ├── searchthumbnail │ │ │ ├── Badges.js │ │ │ ├── Thumbnail.js │ │ │ ├── Author.js │ │ │ └── Metadata.js │ │ ├── PlaylistNameForm.js │ │ ├── PrivacySelection.js │ │ ├── Overlay.js │ │ ├── SearchThumbnail.js │ │ ├── PrivacySelectionContainer.js │ │ ├── PlaylistThumbnail.js │ │ ├── OwnerBadges.js │ │ ├── NewPlaylistForm.js │ │ ├── ToggleButton.js │ │ ├── ToolTip.js │ │ ├── NotLoggedInScreen.js │ │ ├── ConfirmActionModal.js │ │ ├── HomeVideoThumbnail.js │ │ ├── HistoryVideoThumbnail.js │ │ ├── LibraryThumbnail.js │ │ └── SmallModal.js │ ├── trending │ │ ├── trending.css │ │ └── Trending.js │ ├── videothumbnail │ │ ├── MiniatureContainer.js │ │ ├── MiniaturePlayerContainer.js │ │ ├── MiniaturePlayerControls.js │ │ ├── PlayerInput.js │ │ └── VideoContainer.js │ ├── ErrorBoundary.js │ ├── AppContainer.js │ ├── playlists │ │ ├── searchhistory.scss │ │ ├── OptionsButton.js │ │ ├── PlaylistVideos.js │ │ ├── PlaylistCard.js │ │ └── SearchHistory.js │ ├── VisitorsModal.js │ └── LoadingBar.js ├── assets │ ├── sass │ │ ├── _variables.scss │ │ ├── _mixins.scss │ │ └── _colors.scss │ ├── dar_logo.png │ ├── dark_logo.png │ └── light_logo.png ├── utils │ └── Utils.js ├── store │ └── store.js ├── locales │ ├── socials.js │ ├── trending.js │ ├── dates.js │ ├── visitorsmodal.js │ ├── miniatureplayer.js │ ├── relatedvideos.js │ ├── comments.js │ ├── subscriptions.js │ └── library.js ├── App.test.js ├── credentials │ └── credentialsExample.js ├── index.css ├── helpers │ ├── languages.js │ └── navigation.js ├── index.js ├── logo.svg ├── actions │ └── reduxActions.js ├── App.css └── reducers │ └── rootReducer.js ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── opensearch.xml ├── manifest.json └── index.html ├── .gitignore ├── package.json └── README.md /src/components/home/home.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/navbar/navbar.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/toast/toast.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/library/library.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/searchresults/search.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/video/css/videomodal.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/video/css/videowrapper.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $loader-color: red; -------------------------------------------------------------------------------- /src/components/relatedvideos/css/relatedvideos.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/Utils.js: -------------------------------------------------------------------------------- 1 | export const host = 'http://localhost:5500'; 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelruizc/youtube-clone-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelruizc/youtube-clone-react/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelruizc/youtube-clone-react/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/dar_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelruizc/youtube-clone-react/HEAD/src/assets/dar_logo.png -------------------------------------------------------------------------------- /src/assets/dark_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelruizc/youtube-clone-react/HEAD/src/assets/dark_logo.png -------------------------------------------------------------------------------- /src/assets/light_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelruizc/youtube-clone-react/HEAD/src/assets/light_logo.png -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import rootReducer from '../reducers/rootReducer'; 3 | 4 | const store = createStore(rootReducer); 5 | 6 | export default store; 7 | -------------------------------------------------------------------------------- /src/components/subscriptions/ChannelsContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChannelsContainer = (props) => ( 4 |
{props.children}
5 | ); 6 | 7 | export default ChannelsContainer; 8 | -------------------------------------------------------------------------------- /src/components/navbar/SidenavSectionContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SidenavSectionContainer = ({ children }) => ( 4 |
{children}
5 | ); 6 | 7 | export default SidenavSectionContainer; 8 | -------------------------------------------------------------------------------- /src/components/navbar/SidenavSectionTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SidenavSectionTitle = ({ children }) => ( 4 | {children} 5 | ); 6 | 7 | export default SidenavSectionTitle; 8 | -------------------------------------------------------------------------------- /src/components/subscriptions/FeedChannelContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FeedChannelContainer = ({ className, children }) => ( 4 |
{children}
5 | ); 6 | 7 | export default FeedChannelContainer; 8 | -------------------------------------------------------------------------------- /src/components/subscriptions/ChannelItemContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChannelItemContainer = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default ChannelItemContainer; 8 | -------------------------------------------------------------------------------- /src/locales/socials.js: -------------------------------------------------------------------------------- 1 | const locales = { 2 | website: 'https://www.manuelruiz.co', 3 | linkedin: 'https://www.linkedin.com/in/mannyruizwebdev/', 4 | youtube: 'https://www.youtube.com', 5 | github: 'https://github.com/manuelruizc', 6 | }; 7 | 8 | export default locales; 9 | -------------------------------------------------------------------------------- /src/components/shared/css/overlay.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | height:100%; 3 | width: 100%; 4 | background-color:rgba(0, 0, 0, 0.8); 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | display:flex; 9 | justify-content: center; 10 | align-items: center; 11 | } -------------------------------------------------------------------------------- /src/components/video/ModalItemTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ModalItemTitle = ({ children, single }) => ( 4 | 5 | {children} 6 | 7 | ); 8 | 9 | export default ModalItemTitle; 10 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/video/VideoModalItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoModalItem = ({ children, onClick }) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | }; 10 | 11 | export default VideoModalItem; 12 | -------------------------------------------------------------------------------- /src/credentials/credentialsExample.js: -------------------------------------------------------------------------------- 1 | // firebase credentials 2 | export const credentials = { 3 | apiKey: '', 4 | authDomain: '', 5 | projectId: '', 6 | storageBucket: '', 7 | messagingSenderId: '', 8 | }; 9 | export const host = ''; // API endpoint (http://127.0.0.1:5500 | https://www.mywebsite.com); 10 | -------------------------------------------------------------------------------- /src/components/video/VideoModalHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoModalHeader = ({ single, children }) => { 4 | let className = 'video-modal-item-header'; 5 | if (single) className += ' single-item'; 6 | return
{children}
; 7 | }; 8 | 9 | export default VideoModalHeader; 10 | -------------------------------------------------------------------------------- /src/components/navbar/OverflowSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const OverflowSection = ({ children }) => { 4 | return ( 5 |
6 |
{children}
7 |
8 | ); 9 | }; 10 | 11 | export default OverflowSection; 12 | -------------------------------------------------------------------------------- /src/components/relatedvideos/VideoCardContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoCardContainer = (props) => { 4 | return ( 5 |
6 | {props.children} 7 |
8 | ); 9 | }; 10 | 11 | export default VideoCardContainer; 12 | -------------------------------------------------------------------------------- /src/components/video/UserCommentImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserCommentImage = ({ imageSource }) => { 4 | return ( 5 |
6 | User profile thumbnail 7 |
8 | ); 9 | }; 10 | 11 | export default UserCommentImage; 12 | -------------------------------------------------------------------------------- /src/components/video/VideoModalHeaderButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoModalHeaderButton = ({ children, onClick }) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | }; 10 | 11 | export default VideoModalHeaderButton; 12 | -------------------------------------------------------------------------------- /src/components/trending/trending.css: -------------------------------------------------------------------------------- 1 | .trending-container { 2 | display: flex; 3 | flex-direction:column; 4 | justify-content: flex-start; 5 | align-items: flex-start; 6 | width: 100%; 7 | min-height: 100vh; 8 | height: auto; 9 | padding-left: 19%; 10 | padding-top: 12vh; 11 | background-color:rgba(0, 0, 0, 0.92); 12 | } -------------------------------------------------------------------------------- /src/components/video/VideoModalHeaderSecondaryButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoModalHeaderSecondaryButton = ({ children, onClick }) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | }; 10 | 11 | export default VideoModalHeaderSecondaryButton; 12 | -------------------------------------------------------------------------------- /public/opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Search videos 4 | Search on CloneTube 5 | 7 | -------------------------------------------------------------------------------- /src/components/video/UserData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserData = ({ username, date }) => { 4 | return ( 5 |
6 | {username} 7 | {date} 8 |
9 | ); 10 | }; 11 | 12 | export default UserData; 13 | -------------------------------------------------------------------------------- /src/components/navbar/SidenavLinkSub.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SidenavLinkSub = ({ channel_name, channel_thumbnail }) => ( 4 |
5 | 6 | Channel thumbnail 7 | {channel_name} 8 | 9 |
10 | ); 11 | 12 | export default SidenavLinkSub; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | #credentials 15 | /src/credentials/credentials.js 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/components/video/VideoModalContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoModalContainer = ({ videoModal, children, isCogActive }) => { 4 | let className = 'video-options-modal'; 5 | if (!isCogActive) { 6 | className += ' video-options-modal--unactive'; 7 | } 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | export default VideoModalContainer; 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | * { 10 | outline: none; 11 | box-sizing: border-box; 12 | } 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/video/DescriptionText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getURLAndTimestamps } from './helpers/helpers'; 3 | 4 | const DescriptionText = ({ text }) => { 5 | return ( 6 | <> 7 | {text && ( 8 | 9 | {getURLAndTimestamps(text).map((g) => g)} 10 | 11 | )} 12 | 13 | ); 14 | }; 15 | 16 | export default DescriptionText; 17 | -------------------------------------------------------------------------------- /src/helpers/languages.js: -------------------------------------------------------------------------------- 1 | const availableLanguages = [ 2 | 'ar', 3 | 'ca', 4 | 'de', 5 | 'en', 6 | 'es', 7 | 'fr', 8 | 'it', 9 | 'nl', 10 | 'pt', 11 | ]; 12 | let language = localStorage.getItem('language'); 13 | language = JSON.parse(language); 14 | language = language ? language : navigator.languages; 15 | let languageCode = language[1]; 16 | if (!availableLanguages.includes(languageCode)) { 17 | languageCode = 'en'; 18 | language = ['en', 'en']; 19 | } 20 | 21 | export default language; 22 | -------------------------------------------------------------------------------- /src/components/relatedvideos/ButtonContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ButtonContainer = (props) => { 4 | return ( 5 |
14 | {props.children} 15 |
16 | ); 17 | }; 18 | 19 | export default ButtonContainer; 20 | -------------------------------------------------------------------------------- /src/components/videothumbnail/MiniatureContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MiniatureContainer = (props) => { 4 | const { title, uploader } = props; 5 | return ( 6 |
props._goToVideo(e)} 9 | > 10 | {title} 11 | {uploader} 12 |
13 | ); 14 | }; 15 | 16 | export default MiniatureContainer; 17 | -------------------------------------------------------------------------------- /src/components/shared/ButtonContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ButtonContainer = ({ children, className = '' }) => { 4 | return ( 5 |
14 | {children} 15 |
16 | ); 17 | }; 18 | 19 | export default ButtonContainer; 20 | -------------------------------------------------------------------------------- /src/locales/trending.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | title: 'Tendències', 6 | }, 7 | en: { 8 | title: 'Trending', 9 | }, 10 | es: { 11 | title: 'Tendencias', 12 | }, 13 | fr: { 14 | title: 'Tendances', 15 | }, 16 | de: { 17 | title: 'Trends', 18 | }, 19 | it: { 20 | title: 'Tendenze', 21 | }, 22 | }; 23 | 24 | const currentLanguage = languagesArray[1]; 25 | 26 | export default locales[currentLanguage]; 27 | -------------------------------------------------------------------------------- /src/components/videothumbnail/MiniaturePlayerContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MiniaturePlayerContainer = (props) => { 4 | const { darkTheme, isThumbnailActive } = props; 5 | return ( 6 |
12 | {props.children} 13 |
14 | ); 15 | }; 16 | 17 | export default MiniaturePlayerContainer; 18 | -------------------------------------------------------------------------------- /src/components/subscriptions/ChannelMetadata.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChannelMetadata = ({ channel }) => { 4 | const { channel_name, channel_thumbnail } = channel; 5 | return ( 6 |
7 | {channel_name} 12 | {channel_name} 13 |
14 | ); 15 | }; 16 | 17 | export default ChannelMetadata; 18 | -------------------------------------------------------------------------------- /src/components/shared/searchthumbnail/Badges.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Badges = ({ badges }) => { 4 | return ( 5 |
6 | {badges.map((badge, index) => { 7 | const { label } = badge.metadataBadgeRenderer; 8 | return ( 9 | 10 | {label} 11 | 12 | ); 13 | })} 14 |
15 | ); 16 | }; 17 | 18 | export default Badges; 19 | -------------------------------------------------------------------------------- /src/components/video/VideoPlayerContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoPlayerContainer = ({ 4 | children, 5 | videoContainer, 6 | className, 7 | onMouseOut, 8 | onMouseMove, 9 | id, 10 | }) => { 11 | return ( 12 |
19 | {children} 20 |
21 | ); 22 | }; 23 | 24 | export default VideoPlayerContainer; 25 | -------------------------------------------------------------------------------- /src/components/video/SaveButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Save } from '../../assets/Icons'; 3 | import locales from '../../locales/video'; 4 | 5 | const SaveButton = ({ user, updateVisitorModal, modalPlaylist }) => { 6 | return ( 7 | modalPlaylist(false)} 9 | className="video-actions-container" 10 | > 11 | 12 | {locales.uploaderInfo.tooltips.save} 13 | 14 | ); 15 | }; 16 | 17 | export default SaveButton; 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/video/ShareButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Share } from '../../assets/Icons'; 3 | import locales from '../../locales/video'; 4 | 5 | // FEATURE NOT WORKING 6 | const ShareButton = ({ user, updateVisitorModal, modalPlaylist }) => { 7 | return ( 8 | modalPlaylist(false)} 10 | className="video-actions-container" 11 | > 12 | 13 | {locales.uploaderInfo.tooltips.share} 14 | 15 | ); 16 | }; 17 | 18 | export default ShareButton; 19 | -------------------------------------------------------------------------------- /src/components/shared/PlaylistNameForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/playlistmodal'; 3 | 4 | const PlaylistNameForm = (props) => { 5 | return ( 6 |
7 | 8 | props.updatePlaylistName(e.target.value)} 13 | /> 14 |
15 | ); 16 | }; 17 | 18 | export default PlaylistNameForm; 19 | -------------------------------------------------------------------------------- /src/components/video/VideoComments.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Comment from './Comment'; 3 | 4 | const VideoComments = ({ comments, updateComments }) => { 5 | return ( 6 |
7 | {comments.map((comment, index) => ( 8 | 15 | ))} 16 |
17 | ); 18 | }; 19 | 20 | export default VideoComments; 21 | -------------------------------------------------------------------------------- /src/assets/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin responsivePhoneSize { 2 | @media screen and (max-width: 587px) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin responsiveNavSize { 8 | @media screen and (max-width: 1024px) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin responsivePortraitSize { 14 | @media screen and (max-width: 873px) { 15 | @content; 16 | } 17 | } 18 | 19 | @mixin responsiveTabletPortraitSize { 20 | @media screen and (min-width: 588px) and (max-width: 873px) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin responsiveTabletLandscapeSize { 26 | @media screen and (min-width: 874px) and (max-width: 1024px) { 27 | @content; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/shared/PrivacySelection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PrivacySelection = (props) => { 4 | return ( 5 |
6 | {props.playlist.map((play, i) => { 7 | return ( 8 |
props.selectPrivacy(i)} 11 | className="privacy-setting" 12 | > 13 | {play.title} 14 |
15 | ); 16 | })} 17 |
18 | ); 19 | }; 20 | 21 | export default PrivacySelection; 22 | -------------------------------------------------------------------------------- /src/components/video/UserComment.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getURLAndTimestamps } from './helpers/helpers'; 3 | 4 | const UserComment = ({ comment }) => { 5 | return ( 6 | 7 | {getURLAndTimestamps(comment).map((g, index) => { 8 | if (!g.props) return {g}; 9 | let key = ''; 10 | const { children, time } = g.props; 11 | if (children) key = children + index; 12 | else if (time) key = time + index; 13 | return {g}; 14 | })} 15 | 16 | ); 17 | }; 18 | 19 | export default UserComment; 20 | -------------------------------------------------------------------------------- /src/components/navbar/LoginSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/navbar'; 3 | 4 | const LoginSection = ({ login }) => { 5 | return ( 6 |
7 | {locales.nav.signInMessage} 8 | 18 |
19 | ); 20 | }; 21 | 22 | export default LoginSection; 23 | -------------------------------------------------------------------------------- /src/components/shared/Overlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/miniatureplayer'; 3 | import './css/overlay.scss'; 4 | 5 | const Overlay = () => { 6 | return ( 7 |
8 | 19 | {locales.overlay.text} 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default Overlay; 26 | -------------------------------------------------------------------------------- /src/components/shared/searchthumbnail/Thumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Thumbnail = (props) => { 4 | const { imageURI, time, current_length, length } = props; 5 | return ( 6 |
7 | {imageURI} 8 | {time} 9 | {current_length !== undefined && ( 10 |
11 |
14 |
15 | )} 16 | {props.children} 17 |
18 | ); 19 | }; 20 | 21 | export default Thumbnail; 22 | -------------------------------------------------------------------------------- /src/components/video/MobileVolumeControl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Muted } from '../../assets/Icons'; 3 | 4 | const MobileVolumeControl = ({ controlVolume }) => { 5 | return ( 6 |
17 |
18 | 19 | Listen to this video 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default MobileVolumeControl; 26 | -------------------------------------------------------------------------------- /src/locales/dates.js: -------------------------------------------------------------------------------- 1 | export default { 2 | seconds: { 3 | es: 'segundos', 4 | }, 5 | second: { 6 | es: 'segundo', 7 | }, 8 | minutes: { 9 | es: 'minutos', 10 | }, 11 | minute: { 12 | es: 'minuto', 13 | }, 14 | hours: { 15 | es: 'horas', 16 | }, 17 | hour: { 18 | es: 'hora', 19 | }, 20 | days: { 21 | es: 'días', 22 | }, 23 | day: { 24 | es: 'día', 25 | }, 26 | week: { 27 | es: 'semanas', 28 | }, 29 | weeks: { 30 | es: 'semanas', 31 | }, 32 | month: { 33 | es: 'mes', 34 | }, 35 | months: { 36 | es: 'meses', 37 | }, 38 | year: { 39 | es: 'año', 40 | }, 41 | years: { 42 | es: 'años', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/videothumbnail/MiniaturePlayerControls.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BufferedTime from '../video/BufferedTime'; 3 | 4 | const MiniaturePlayerControls = (props) => { 5 | const { bufferedTime } = props; 6 | 7 | return ( 8 |
12 | {props.children} 13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default MiniaturePlayerControls; 24 | -------------------------------------------------------------------------------- /src/components/subscriptions/SubscribeButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/subscriptions'; 3 | 4 | const SubscribeButton = ({ 5 | isSubscribed, 6 | openModal, 7 | changeSubscriptionState, 8 | }) => { 9 | return ( 10 |
18 | 19 | {isSubscribed 20 | ? locales.subscribeButton.subscribed 21 | : locales.subscribeButton.unsubscribed} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default SubscribeButton; 28 | -------------------------------------------------------------------------------- /src/components/video/LoadingSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingSkeleton = () => { 4 | return ( 5 | 6 |
7 |
8 |
9 |
10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default LoadingSkeleton; 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import { Provider } from 'react-redux'; 7 | import store from './store/store'; 8 | import axios from 'axios'; 9 | import ErrorBoundary from './components/ErrorBoundary'; 10 | import { host } from './credentials/credentials'; 11 | 12 | axios.defaults.baseURL = host + '/'; 13 | 14 | 15 | ReactDOM.render(, document.getElementById('root')); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.unregister(); 21 | -------------------------------------------------------------------------------- /src/components/navbar/Socials.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/navbar'; 3 | import SidenavLink from './SidenavLink'; 4 | import SidenavSectionContainer from './SidenavSectionContainer'; 5 | import SidenavSectionTitle from './SidenavSectionTitle'; 6 | 7 | const Socials = () => { 8 | return ( 9 | 10 | 11 | {locales.sidenav.portfolio} 12 | 13 | LinkedIn 14 | Portfolio 15 | YouTube 16 | GitHub 17 | 18 | ); 19 | }; 20 | 21 | export default Socials; 22 | -------------------------------------------------------------------------------- /src/components/shared/css/ownerbadges.scss: -------------------------------------------------------------------------------- 1 | .trending-container { 2 | display: flex; 3 | flex-direction:column; 4 | justify-content: flex-start; 5 | align-items: flex-start; 6 | width: 100%; 7 | min-height: 100vh; 8 | height: auto; 9 | padding-left: 19%; 10 | padding-top: 12vh; 11 | background-color:rgba(0, 0, 0, 0.92); 12 | } 13 | 14 | .owner-badge-icon { 15 | width:11px; 16 | height: 11px; 17 | fill: white; 18 | opacity: 0.6; 19 | display: flex; 20 | justify-content: center; 21 | margin-left: 6px; 22 | margin-bottom: 3px; 23 | margin-right: 12px; 24 | transform: translateY(12.5%); 25 | &.verified-icon { 26 | width:13px; 27 | height:13px; 28 | margin-left: 4px; 29 | } 30 | } 31 | .owner-badge-icon.light-theme-owner-badge-icon { 32 | fill: black; 33 | } -------------------------------------------------------------------------------- /src/components/video/VideoTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoTitle = (props) => { 4 | const { 5 | isFullScreenActive, 6 | hideControls, 7 | current_video_data, 8 | paused, 9 | } = props; 10 | return ( 11 |
23 | 24 | {current_video_data ? current_video_data.title : ''} 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default VideoTitle; 31 | -------------------------------------------------------------------------------- /src/components/videothumbnail/PlayerInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PlayerInput = (props) => { 4 | const { duration, currentTime, video } = props; 5 | const sliderThumbPosition = String(currentTime); 6 | const rangeOutput = (value = null) => { 7 | const rangeInp = document.getElementById('range'); 8 | if (!isNaN(value) === false) video.currentTime = rangeInp.value; 9 | else video.currentTime = video.currentTime + value; 10 | }; 11 | 12 | return ( 13 | 23 | ); 24 | }; 25 | 26 | export default PlayerInput; 27 | -------------------------------------------------------------------------------- /src/components/shared/SearchThumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SearchThumbnail = (props) => { 4 | const { style, onClick, onClickParams, href, size } = props; 5 | const sizeClass = 6 | size === 'regular' 7 | ? 'video-result-container' 8 | : 'video-result-container video-result-container-big'; 9 | return ( 10 |
11 | {props.children} 12 | { 14 | e.preventDefault(); 15 | onClick(onClickParams); 16 | }} 17 | href={href} 18 | style={style} 19 | > 20 | Video link 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default SearchThumbnail; 27 | -------------------------------------------------------------------------------- /src/components/shared/css/tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip-container { 2 | width: auto; 3 | height: auto; 4 | position: relative; 5 | } 6 | .tooltip-container-absolute { 7 | width: auto; 8 | height: auto; 9 | } 10 | .tooltip { 11 | width: auto; 12 | position: absolute; 13 | left: 50%; 14 | transform: translateX(-50%); 15 | padding: 10px 10px; 16 | color: white !important; 17 | background-color: rgba(83, 83, 83, 0.925); 18 | border-radius: 2px; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | font-size: 0.8rem !important; 23 | font-weight: 400 !important; 24 | white-space: nowrap; 25 | z-index: 15000; 26 | } 27 | .tooltip-top { 28 | top: -50px; 29 | } 30 | .tooltip-down { 31 | bottom: -60px; 32 | } 33 | .tooltip-container-absolute .tooltip { 34 | top: -30px !important; 35 | } -------------------------------------------------------------------------------- /src/components/shared/PrivacySelectionContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/playlistmodal'; 3 | 4 | const PrivacySelectionContainer = (props) => { 5 | return ( 6 |
7 | 8 |
9 |
props.privacyActive(false)} 12 | data-value={props.current_playlist.value} 13 | > 14 | {props.current_playlist.title} 15 |
16 | 17 | {props.children} 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default PrivacySelectionContainer; 24 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { reportBug } from '../helpers/helpers'; 3 | 4 | class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { hasError: false, error: null, info: null }; 8 | } 9 | 10 | async componentDidCatch(error, info) { 11 | const { href } = window.location; 12 | this.setState({ hasError: true, error, info }); 13 | await reportBug(info.componentStack, error.toString(), href); 14 | } 15 | render() { 16 | const { hasError, error, info } = this.state; 17 | if (hasError) { 18 | return ( 19 |
20 |

The app crashed

21 | {error} 22 | {info} 23 |
24 | ); 25 | } 26 | return this.props.children; 27 | } 28 | } 29 | 30 | export default ErrorBoundary; 31 | -------------------------------------------------------------------------------- /src/components/library/components/VideosContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LibraryThumbnail from '../../shared/LibraryThumbnail'; 3 | import PlaylistThumbnail from '../../shared/PlaylistThumbnail'; 4 | 5 | const VideosContainer = ({ videos, pathname, playlist }) => { 6 | if (videos.length === 0 || !videos) { 7 | return ( 8 |
9 | 10 | Save videos to watch later. Your list shows up right here. 11 | 12 |
13 | ); 14 | } 15 | 16 | return ( 17 |
18 | {videos.map((video, i) => { 19 | if (playlist) return ; 20 | return ; 21 | })} 22 |
23 | ); 24 | }; 25 | 26 | export default VideosContainer; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "firebase": "^7.0.0", 8 | "hls.js": "^0.14.16", 9 | "moment": "^2.29.1", 10 | "node-sass": "^4.14.1", 11 | "react": "^16.9.0", 12 | "react-dom": "^16.9.0", 13 | "react-redux": "^7.1.1", 14 | "react-router-dom": "^5.0.1", 15 | "react-scripts": "3.1.1", 16 | "redux": "^4.0.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/home/Skeletons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HomepageSection } from './Section'; 3 | 4 | const Skeletons = () => { 5 | // create an array to map the Skeleton components 6 | const array = new Array(40).fill(0); 7 | return ( 8 | 9 | {array.map((a, index) => { 10 | return ( 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ); 21 | })} 22 | 23 | ); 24 | }; 25 | 26 | export default Skeletons; 27 | -------------------------------------------------------------------------------- /src/components/relatedvideos/LoadingCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingCard = () => { 4 | const arrayMapper = [ 5 | 0, 6 | 0, 7 | 0, 8 | 0, 9 | 0, 10 | 0, 11 | 0, 12 | 0, 13 | 0, 14 | 0, 15 | 0, 16 | 0, 17 | 0, 18 | 0, 19 | 0, 20 | 0, 21 | 0, 22 | 0, 23 | 0, 24 | ]; 25 | return ( 26 | <> 27 |
28 | {arrayMapper.map((a, i) => ( 29 |
34 |
35 |
36 |
37 | ))} 38 | 39 | ); 40 | }; 41 | 42 | export default LoadingCard; 43 | -------------------------------------------------------------------------------- /src/components/shared/searchthumbnail/Author.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ownerBadgeText } from '../../../helpers/helpers'; 3 | import OwnerBadges from '../OwnerBadges'; 4 | import ToolTip from '../ToolTip'; 5 | 6 | const Author = ({ video }) => { 7 | const { channel_thumbnail, channel, owner_badges } = video; 8 | return ( 9 |
10 | {`${channel} 15 | 16 | {channel} 17 | 18 | {owner_badges && ( 19 | } 22 | /> 23 | )} 24 |
25 | ); 26 | }; 27 | 28 | export default Author; 29 | -------------------------------------------------------------------------------- /src/components/shared/PlaylistThumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const PlaylistThumbnail = ({ playlist }) => { 5 | const { thumbnail, playlist_name, playlist_id, display_name } = playlist; 6 | return ( 7 | 13 |
14 | {''} 15 |
16 |
17 |
18 | {playlist_name} 19 | {display_name} 20 | View full playlist 21 |
22 |
23 | 24 | ); 25 | }; 26 | 27 | export default PlaylistThumbnail; 28 | -------------------------------------------------------------------------------- /src/components/shared/OwnerBadges.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Artist, Verified } from '../../assets/Icons'; 4 | import './css/ownerbadges.scss'; 5 | 6 | const OwnerBadges = ({ badges, darkTheme }) => { 7 | const { style } = badges[0].metadataBadgeRenderer; 8 | const text = 9 | style === 'BADGE_STYLE_TYPE_VERIFIED_ARTIST' ? 'music' : 'verified'; 10 | if (text === 'music') 11 | return ( 12 | 17 | ); 18 | return ( 19 | 24 | ); 25 | }; 26 | 27 | const mapStateToProps = (state) => { 28 | return { 29 | darkTheme: state.darkTheme, 30 | }; 31 | }; 32 | 33 | export default connect(mapStateToProps)(OwnerBadges); 34 | -------------------------------------------------------------------------------- /src/assets/sass/_colors.scss: -------------------------------------------------------------------------------- 1 | $red: #FF0000; 2 | $onfocus-border-color: #1565C0; 3 | 4 | // navbar 5 | $dark-navbar-background-color: rgba(33, 33, 33, 0.98); 6 | $dark-navbar-input-background-color: #121212; 7 | $dark-navbar-input-border-color: #303030; 8 | $dark-navbar-input-placeholder-color: #ffffffb3; 9 | $dark-navbar-input-text-color: #FFFFFF; 10 | $dark-navbar-input-button-background-color: #323232; 11 | $dark-navbar-input-button-color: #ffffffb3; 12 | $dark-navbar-input-button-icon-color: #ffffffb3; // pending 13 | $dark-navbar-icons-color: #FFFFFF; 14 | $dark-navbar-burger-menu-color: #FFFFFF; 15 | 16 | $light-navbar-background-color: #FFFFFF; 17 | $light-navbar-input-background-color: #FFFFFF; 18 | $light-navbar-input-border-color: #CCCCCC; 19 | $light-navbar-input-placeholder-color: #7F7F7F; 20 | $light-navbar-input-text-color: #000000; 21 | $light-navbar-input-button-background-color: #F8F8F8; 22 | $light-navbar-input-button-color: #ffffffb3; 23 | $light-navbar-input-button-icon-color: #22222fb3; // pending 24 | $light-navbar-icons-color: #606060; 25 | $light-navbar-burger-menu-color: #000000; 26 | 27 | // main app 28 | -------------------------------------------------------------------------------- /src/components/relatedvideos/VideoThumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { convertMinsSecs } from '../../helpers/helpers'; 3 | 4 | const VideoThumbnail = ({ info }) => { 5 | const { thumbnails, current_length, length_seconds } = info; 6 | const thumbnail = thumbnails[thumbnails.length - 1].url; 7 | return ( 8 |
9 | {info} 15 | 16 | {convertMinsSecs(length_seconds * 1000, length_seconds)} 17 | 18 | {current_length !== undefined && ( 19 |
20 |
26 |
27 | )} 28 |
29 | ); 30 | }; 31 | 32 | export default VideoThumbnail; 33 | -------------------------------------------------------------------------------- /src/components/relatedvideos/VideoInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ownerBadgeText } from '../../helpers/helpers'; 3 | import OwnerBadges from '../shared/OwnerBadges'; 4 | import ToolTip from '../shared/ToolTip'; 5 | 6 | const VideoInfo = (props) => { 7 | const { info } = props; 8 | const { title, author, short_view_count_text, ownerBadges, date } = info; 9 | return ( 10 |
11 | {title} 12 | 13 | 14 | {author.name} 15 | 16 | {ownerBadges && ( 17 | 18 | 19 | 20 | )} 21 | 22 | 23 | {short_view_count_text} {date && ` • ${date}`} 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default VideoInfo; 30 | -------------------------------------------------------------------------------- /src/components/toast/ToastMessage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | 3 | class ToastMessage extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | toastContainer: createRef(), 8 | }; 9 | } 10 | componentDidMount() { 11 | const { addFadeoutStyle } = this; 12 | setTimeout(function () { 13 | addFadeoutStyle(); 14 | }, 3000); 15 | } 16 | 17 | addFadeoutStyle = () => { 18 | const toastContainer = this.state.toastContainer.current; 19 | if (!toastContainer) return; 20 | const { removeLastToastNotification } = this.props; 21 | toastContainer.classList.add('toast-message-container-fade-out'); 22 | setTimeout(function () { 23 | removeLastToastNotification(); 24 | }, 1000); 25 | }; 26 | 27 | render() { 28 | const { toastContainer } = this.state; 29 | return ( 30 |
31 | {this.props.message} 32 |
33 | ); 34 | } 35 | } 36 | 37 | export default ToastMessage; 38 | -------------------------------------------------------------------------------- /src/components/video/VideoDescriptions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/video'; 3 | import DescriptionText from './DescriptionText'; 4 | 5 | const VideoDescriptions = ({ description, active, toggle }) => { 6 | return ( 7 | <> 8 |
15 | 16 |
17 | {description && ( 18 | { 23 | e.preventDefault(); 24 | toggle(); 25 | }} 26 | > 27 | {active 28 | ? locales.uploaderInfo.description.showLess 29 | : locales.uploaderInfo.description.showMore} 30 | 31 | )} 32 | 33 | ); 34 | }; 35 | 36 | export default VideoDescriptions; 37 | -------------------------------------------------------------------------------- /src/components/library/components/UserInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../../locales/library'; 3 | 4 | const UserInfo = ({ user }) => { 5 | if (!user) return <>; 6 | const { photo_url, display_name, videos_liked, user_subscriptions } = user; 7 | return ( 8 |
9 | User thumbnail 14 | {display_name} 15 |
16 |
17 | {locales.user.data.subscriptions} 18 | {user_subscriptions} 19 |
20 |
21 | {locales.user.data.uploads} 22 | 0 23 |
24 |
25 | {locales.user.data.likes} 26 | {videos_liked} 27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default UserInfo; 34 | -------------------------------------------------------------------------------- /src/components/shared/NewPlaylistForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/playlistmodal'; 3 | import PlaylistNameForm from './PlaylistNameForm'; 4 | import PrivacySelection from './PrivacySelection'; 5 | import PrivacySelectionContainer from './PrivacySelectionContainer'; 6 | 7 | const NewPlaylistForm = (props) => { 8 | return ( 9 |
10 | 14 | 18 | {props.isPrivacyOptionActive && ( 19 | 23 | )} 24 | 25 | 26 | {locales.newPlaylistModal.confirmButton} 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default NewPlaylistForm; 33 | -------------------------------------------------------------------------------- /src/components/video/VideoMiddleLayer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const VideoMiddleLayer = (props) => { 4 | const { 5 | openFullscreen, 6 | videoContainer, 7 | playAndPauseVideo, 8 | controlVideoWithKeys, 9 | animationLogo, 10 | volumeValue, 11 | } = props; 12 | return ( 13 |
openFullscreen(videoContainer)} 15 | onClick={(e) => playAndPauseVideo(e)} 16 | className="video-middlelayer" 17 | id="video-middlelayer" 18 | onKeyDown={(e) => controlVideoWithKeys(e)} 19 | tabIndex="0" 20 | > 21 |
22 |
26 | 27 | {volumeValue + '%'} 28 | 29 |
30 | 31 | 32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default VideoMiddleLayer; 39 | -------------------------------------------------------------------------------- /src/components/shared/css/tooltip.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/sass/mixins'; 2 | .tooltip-container { 3 | width: auto; 4 | height: auto; 5 | position: relative; 6 | } 7 | .tooltip-container-absolute { 8 | width: auto; 9 | height: auto; 10 | .tooltip { 11 | top: -30px !important; 12 | } 13 | } 14 | .tooltip-container, .tooltip-container-absolute { 15 | .tooltip { 16 | width: auto; 17 | position: absolute; 18 | left: 50%; 19 | transform: translateX(-50%); 20 | padding: 10px 10px; 21 | color: white !important; 22 | background-color: rgba(83, 83, 83, 0.925); 23 | opacity: 1 !important; 24 | border-radius: 2px; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | font-size: 0.8rem !important; 29 | font-weight: 400 !important; 30 | white-space: nowrap; 31 | &.videocontrol-tooltip { 32 | padding: 5px 9px; 33 | background-color: rgba(43, 43, 43, 0.925); 34 | transform: translateY(40%) translateX(-50%); 35 | font-weight: 500 !important; 36 | } 37 | @include responsivePhoneSize { 38 | display: none; 39 | } 40 | } 41 | } 42 | .tooltip-top { 43 | top: -50px; 44 | } 45 | .tooltip-down { 46 | bottom: -60px; 47 | } -------------------------------------------------------------------------------- /src/components/subscriptions/subscriptions.css: -------------------------------------------------------------------------------- 1 | .feed-channel-container { 2 | display: flex; 3 | justify-content: flex-start; 4 | align-items: flex-start; 5 | width: 100%; 6 | min-height: 100vh; 7 | height: auto; 8 | padding-left: 28%; 9 | padding-top: 12vh; 10 | background-color:rgba(0, 0, 0, 0.92); 11 | } 12 | .channels-container { 13 | width: 100%; 14 | height: 100%; 15 | padding-right: 40px; 16 | } 17 | .channel-item-container { 18 | margin-bottom: 20px; 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | } 23 | .channel-data-container { 24 | display: flex; 25 | justify-content: flex-start; 26 | align-items: flex-start; 27 | } 28 | .channel-image { 29 | width: 136px; 30 | height: 136px; 31 | border-radius: 136px; 32 | } 33 | .channel-name { 34 | text-align: left; 35 | margin-top: 36px; 36 | font-size: 1rem; 37 | font-weight: 500; 38 | margin-left: 120px; 39 | } 40 | .subscribe-button { 41 | padding: 10px 16px; 42 | background-color:rgb(219, 8, 8); 43 | border-radius: 3px; 44 | cursor: pointer; 45 | } 46 | .subscribe-button span { 47 | text-transform: uppercase; 48 | font-size: 0.85rem; 49 | font-weight: 500; 50 | opacity: 1; 51 | } 52 | .subscribed { 53 | background-color: rgb(65, 65, 65); 54 | } 55 | .subscribed span { 56 | opacity: 0.8; 57 | } -------------------------------------------------------------------------------- /src/components/shared/ToggleButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // workaround for input checkbox fix this 3 | const change = () => { 4 | return; 5 | }; 6 | const ToggleButton = ({ color = false, active, onChange = change }) => { 7 | if (color) { 8 | const backgroundColor = active ? color : '#313131'; 9 | const backgroundColorRound = active ? '#d4d4d4' : '#909090'; 10 | return ( 11 |
15 | 21 | 25 |
26 | ); 27 | } 28 | return ( 29 |
30 | 31 | 34 |
35 | ); 36 | }; 37 | 38 | export default ToggleButton; 39 | -------------------------------------------------------------------------------- /src/components/shared/ToolTip.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './css/tooltip.scss'; 3 | 4 | const ToolTip = (props) => { 5 | const { 6 | component, 7 | message, 8 | childrenIsAbsolute, 9 | onTop = true, 10 | style = {}, 11 | toolTipStyle = {}, 12 | show = true, 13 | videoControl = false, 14 | } = props; 15 | const [tooltipVisible, setTooltipVisibe] = useState(false); 16 | const positionClass = onTop ? 'tooltip-top' : 'tooltip-down'; 17 | let classNameTooltip = `tooltip ${positionClass}`; 18 | if (videoControl) classNameTooltip += ' videocontrol-tooltip'; 19 | return ( 20 |
setTooltipVisibe(true)} 22 | onMouseOut={() => setTooltipVisibe(false)} 23 | className={ 24 | childrenIsAbsolute 25 | ? 'tooltip-container-absolute' 26 | : 'tooltip-container' 27 | } 28 | style={style} 29 | > 30 | {tooltipVisible && show && ( 31 | 32 | {message} 33 | 34 | )} 35 | {props.children === undefined 36 | ? React.cloneElement(component) 37 | : props.children} 38 |
39 | ); 40 | }; 41 | 42 | export default ToolTip; 43 | -------------------------------------------------------------------------------- /src/locales/visitorsmodal.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | title: 'Heu d’iniciar sessió per utilitzar aquesta opció.', 6 | buttons: { 7 | cancel: 'Tanca', 8 | confirm: 'Inicieu la sessió', 9 | }, 10 | }, 11 | de: { 12 | title: 'Sie müssen sich anmelden, um diese Option nutzen zu können.', 13 | buttons: { 14 | cancel: 'Schließen', 15 | confirm: 'Einloggen', 16 | }, 17 | }, 18 | en: { 19 | title: 'You need to log in to use this option.', 20 | buttons: { 21 | cancel: 'Close', 22 | confirm: 'Sign in', 23 | }, 24 | }, 25 | es: { 26 | title: 'Necesita iniciar sesión para usar esta opción.', 27 | buttons: { 28 | cancel: 'Cerrar', 29 | confirm: 'Inicia sesión', 30 | }, 31 | }, 32 | it: { 33 | title: 'Devi accedere per utilizzare questa opzione.', 34 | buttons: { 35 | cancel: 'Vicino', 36 | confirm: 'Registrati', 37 | }, 38 | }, 39 | fr: { 40 | title: 'Vous devez vous connecter pour utiliser cette option.', 41 | buttons: { 42 | cancel: 'Fermer', 43 | confirm: "S'identifier", 44 | }, 45 | }, 46 | }; 47 | 48 | const currentLanguage = languagesArray[1]; 49 | export default locales[currentLanguage]; 50 | -------------------------------------------------------------------------------- /src/components/video/CommentSection.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { getVideoComments } from '../../helpers/helpers'; 3 | import locales from '../../locales/comments'; 4 | import CommentForm from './CommentForm'; 5 | import VideoComments from './VideoComments'; 6 | 7 | const CommentSection = ({ videoId }) => { 8 | const [comments, setComments] = useState([]); 9 | const [totalComments, setTotalComments] = useState([{ total: 0 }]); 10 | useEffect(() => { 11 | (async () => { 12 | const response = await getVideoComments(videoId); 13 | const { comments, totalComments } = response.data; 14 | setComments(comments); 15 | setTotalComments(totalComments); 16 | })(); 17 | }, [videoId]); 18 | return ( 19 | <> 20 | 21 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | const VideoCommentsData = ({ numberOfComments }) => { 32 | return ( 33 |
34 | 35 | {locales.title.totalComments(numberOfComments[0].total)} 36 | 37 |
38 | ); 39 | }; 40 | 41 | export default CommentSection; 42 | -------------------------------------------------------------------------------- /src/components/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import '../App.scss'; 4 | import RelatedVideos from './relatedvideos/RelatedVideos'; 5 | import Video from './video/Video'; 6 | const AppContainer = (props) => { 7 | const { darkTheme, theater_mode } = props; 8 | let rootClass = darkTheme ? 'root' : 'root root--light'; 9 | rootClass = theater_mode ? rootClass + ' root-theater' : rootClass; 10 | let rightContainer = 'right-container'; 11 | rightContainer = theater_mode 12 | ? rightContainer + ' right-container-theater' 13 | : rightContainer; 14 | 15 | return ( 16 | 17 |
18 |
19 |
24 |
25 | 29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | const mapStateToProps = (state) => { 36 | return { 37 | darkTheme: state.darkTheme, 38 | theater_mode: state.theater_mode, 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps)(AppContainer); 43 | -------------------------------------------------------------------------------- /src/components/toast/Toast.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { removeLastToastNotification } from '../../actions/reduxActions'; 4 | import './toast.scss'; 5 | import ToastMessage from './ToastMessage'; 6 | 7 | class Toast extends Component { 8 | render() { 9 | const { toast_notifications, removeLastToastNotification } = this.props; 10 | return ( 11 |
12 | {toast_notifications.map((toast, index) => { 13 | const { toast_message, id } = toast; 14 | return ( 15 | 24 | ); 25 | })} 26 |
27 | ); 28 | } 29 | } 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | toast_notifications: state.toast_notifications, 34 | }; 35 | }; 36 | 37 | const mapDispatchToProps = (dispatch) => { 38 | return { 39 | removeLastToastNotification: () => 40 | dispatch(removeLastToastNotification()), 41 | }; 42 | }; 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(Toast); 45 | -------------------------------------------------------------------------------- /src/components/navbar/SidenavLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Home, Subscriptions, Trending } from '../../assets/Icons'; 4 | import locales from '../../locales/navbar'; 5 | import domains from '../../locales/socials'; 6 | 7 | const activeClass = 'sidenav-link sidenav-path-active'; 8 | 9 | const SidenavLink = ({ children, icon = false, pathname, to, customIcon }) => { 10 | if (!icon) { 11 | let linkIcon; 12 | if (customIcon === 'home') linkIcon = ; 13 | else if (customIcon === 'subscriptions') 14 | linkIcon = ; 15 | else linkIcon = ; 16 | return ( 17 | 21 | 22 | {linkIcon} 23 | {locales.sidenav[customIcon]} 24 | 25 | 26 | ); 27 | } 28 | const className = `sidenav-link-icon-fa fa ${icon}`; 29 | return ( 30 | 36 | 37 | 38 | {children} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default SidenavLink; 45 | -------------------------------------------------------------------------------- /src/locales/miniatureplayer.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | overlay: { 6 | text: "S'està reproduint", 7 | }, 8 | tooltip: { 9 | verified: 'Verificat', 10 | music: "Canal oficial d'artista", 11 | }, 12 | }, 13 | en: { 14 | overlay: { 15 | text: 'Now playing', 16 | }, 17 | tooltip: { 18 | verified: 'Verified', 19 | music: 'Official Artist Channel', 20 | }, 21 | }, 22 | de: { 23 | overlay: { 24 | text: 'Läuft gerade', 25 | }, 26 | tooltip: { 27 | verified: 'Bestätigt', 28 | music: 'Offizieller Künstlerkanal', 29 | }, 30 | }, 31 | fr: { 32 | overlay: { 33 | text: 'En course de lecture', 34 | }, 35 | tooltip: { 36 | verified: 'Validé', 37 | music: "Chaîne d'artiste officielle", 38 | }, 39 | }, 40 | es: { 41 | overlay: { 42 | text: 'Reproduciendo', 43 | }, 44 | tooltip: { 45 | verified: 'Verificada', 46 | music: 'Canal oficial de artista', 47 | }, 48 | }, 49 | it: { 50 | overlay: { 51 | text: 'Ora in riproduzione', 52 | }, 53 | tooltip: { 54 | verified: 'Verificato', 55 | music: "Canalle ufficiale dell'artista", 56 | }, 57 | }, 58 | }; 59 | 60 | const currentLanguage = languagesArray[1]; 61 | 62 | export default locales[currentLanguage]; 63 | -------------------------------------------------------------------------------- /src/components/shared/NotLoggedInScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { History, Library, Subscriptions } from '../../assets/Icons'; 4 | import navLocales from '../../locales/navbar'; 5 | import './css/notloggedscreen.scss'; 6 | 7 | const NotLoggedInScreen = ({ darkTheme, icon, title, description }) => { 8 | const MainIcon = (iconName) => { 9 | iconName = iconName.toLowerCase(); 10 | const className = 'not-logged-main-icon'; 11 | if (iconName === 'subscriptions') { 12 | return ; 13 | } else if (iconName === 'history') { 14 | return ; 15 | } 16 | return ; 17 | }; 18 | let containerClass = 'not-logged-screen-container'; 19 | if (!darkTheme) 20 | containerClass += ' not-logged-screen-container--light-theme'; 21 | return ( 22 |
23 | {MainIcon(icon)} 24 |
{title}
25 | {description} 26 | 32 |
33 | ); 34 | }; 35 | 36 | const mapStateToProps = (state) => { 37 | return { 38 | darkTheme: state.darkTheme, 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps)(NotLoggedInScreen); 43 | -------------------------------------------------------------------------------- /src/components/trending/Trending.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import React, { useEffect, useState } from "react"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { LOADING_STATES } from "../../helpers/helpers"; 5 | import language from "../../helpers/languages"; 6 | import locales from "../../locales/trending"; 7 | import "./trending.scss"; 8 | import TrendingItems from "./TrendingItems"; 9 | 10 | const hl = language[1]; 11 | 12 | const Trending = (props) => { 13 | const { history } = props; 14 | const [videos, setVideos] = useState([]); 15 | const dispatch = useDispatch(); 16 | const darkTheme = useSelector((state) => state.darkTheme); 17 | useEffect(() => { 18 | document.title = locales.title + " - CloneTube"; 19 | const token = localStorage.getItem("jwt_token"); 20 | const options = token 21 | ? { headers: { Authorization: `Bearer ${token}`, hl } } 22 | : { hl }; 23 | const { LOADING_COMPLETE, LOADING } = LOADING_STATES; 24 | dispatch({ type: "UPDATE_LOADING_STATE", payload: LOADING }); 25 | axios 26 | .get("/trending", options) 27 | .then((response) => { 28 | setVideos(response.data); 29 | dispatch({ type: "UPDATE_LOADING_STATE", payload: LOADING_COMPLETE }); 30 | }) 31 | .catch((e) => console.error(e)); 32 | }, []); 33 | 34 | return ( 35 |
42 | 43 |
44 | ); 45 | }; 46 | 47 | export default Trending; 48 | -------------------------------------------------------------------------------- /src/components/subscriptions/ChannelItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { subscribeToAChannel } from '../../helpers/helpers'; 3 | import ChannelItemContainer from './ChannelItemContainer'; 4 | import ChannelMetadata from './ChannelMetadata'; 5 | import SubscribeButton from './SubscribeButton'; 6 | 7 | const ChannelItem = ({ 8 | channel, 9 | index, 10 | modifySubscriptionState, 11 | user, 12 | openConfirmModal, 13 | }) => { 14 | const { 15 | channel_name, 16 | channel_thumbnail, 17 | channel_id, 18 | is_subscribed, 19 | } = channel; 20 | const isSubscribed = is_subscribed === 1; 21 | const changeSubscriptionState = async () => { 22 | const isSubscribed = is_subscribed === 1 ? 0 : 1; 23 | const { uid } = user; 24 | const response = await subscribeToAChannel( 25 | uid, 26 | channel_id, 27 | channel_name, 28 | channel_thumbnail, 29 | isSubscribed 30 | ); 31 | const { error } = response.data; 32 | if (!error) { 33 | modifySubscriptionState(index, isSubscribed); 34 | } 35 | }; 36 | 37 | const openModal = () => { 38 | openConfirmModal(channel, index); 39 | }; 40 | return ( 41 | 42 | 43 | 48 | 49 | ); 50 | }; 51 | 52 | export default ChannelItem; 53 | -------------------------------------------------------------------------------- /src/components/relatedvideos/NextVideoSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/relatedvideos'; 3 | import ToggleButton from '../shared/ToggleButton'; 4 | import ToolTip from '../shared/ToolTip'; 5 | 6 | const NextVideoSection = (props) => { 7 | const { _onCheckboxClick, autoplay } = props; 8 | return ( 9 |
10 |

18 | {locales.next} 19 |

20 |
21 | {locales.autoplay} 22 | 34 | 39 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default NextVideoSection; 46 | -------------------------------------------------------------------------------- /src/components/video/VoteButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { ThumbDown, ThumbUp } from '../../assets/Icons'; 4 | 5 | const VoteButton = ({ upvote, number, voteFunction, voted }) => { 6 | const user = useSelector((state) => state.user); 7 | const dispatch = useDispatch(); 8 | const vote = () => { 9 | if (user) { 10 | voteFunction(); 11 | return; 12 | } 13 | dispatch({ type: 'UPDATE_VISITOR_MODAL', payload: true }); 14 | }; 15 | return ( 16 |
17 | {upvote ? ( 18 |
19 | 26 |
27 | ) : ( 28 |
29 | 36 |
37 | )} 38 | {upvote && number > 0 && ( 39 | {number} 40 | )} 41 |
42 | ); 43 | }; 44 | 45 | export default VoteButton; 46 | -------------------------------------------------------------------------------- /src/components/video/BufferedTime.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const BufferedTime = (props) => { 4 | let timeRanges = props.bufferedTime; 5 | let timeArr = []; 6 | 7 | if (timeRanges !== 0) { 8 | for (let i = 0; i < timeRanges.length; i++) { 9 | timeArr.push(0); 10 | } 11 | const video = document.getElementsByTagName('video')[0]; 12 | return ( 13 | 14 | {timeArr.map((time, index) => { 15 | return ( 16 |
36 | ); 37 | })} 38 | 39 | ); 40 | } 41 | return
; 42 | }; 43 | 44 | export default BufferedTime; 45 | -------------------------------------------------------------------------------- /src/components/video/VideoLayers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VideoAlert } from '../../assets/Icons'; 3 | 4 | export const VideoWaiting = () => { 5 | return ( 6 |
7 | Waiting for video... 12 |
13 | ); 14 | }; 15 | 16 | export const VideoBlocked = () => { 17 | return ( 18 |
19 | 20 |
21 | 22 | Video unavailable 23 | 24 | 25 | This video is no longer available. 26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export const VideoGeoBlocked = () => { 33 | return ( 34 |
35 | 36 |
37 | 38 | Video unavailable 39 | 40 | 41 | The uploader has not made this video available in your 42 | country. 43 | 44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/shared/ConfirmActionModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/subscriptions'; 3 | import './css/confirmactionmodal.scss'; 4 | 5 | const ConfirmActionModal = ({ 6 | title, 7 | description, 8 | actionButtonTitle, 9 | action, 10 | modalSize, 11 | closeModal, 12 | }) => { 13 | const doAction = () => { 14 | action(); 15 | closeModal(); 16 | }; 17 | 18 | return ( 19 |
20 |
21 |
28 | {title && ( 29 |
30 |

{title}

31 |
32 | )} 33 |
34 | 38 |
39 |
40 | 41 | {locales.confirmModal.cancelButton} 42 | 43 | {actionButtonTitle} 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default ConfirmActionModal; 51 | -------------------------------------------------------------------------------- /src/locales/relatedvideos.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | autoplay: 'Reproducció automática', 6 | next: 'A continuació', 7 | modal: { 8 | saveToWatchLater: 'Desa a Visualitza més tard', 9 | saveToPlaylist: 'Desa en una llista de reproducció', 10 | }, 11 | }, 12 | en: { 13 | autoplay: 'Autoplay', 14 | next: 'Up next', 15 | modal: { 16 | saveToWatchLater: 'Save to Watch later', 17 | saveToPlaylist: 'Save to playlist', 18 | }, 19 | }, 20 | de: { 21 | autoplay: 'Autoplay', 22 | next: 'Nächste Titel', 23 | modal: { 24 | saveToWatchLater: 'Zu "Später ansehen" hinzufügen', 25 | saveToPlaylist: 'Zu Playlist hinzufügen', 26 | }, 27 | }, 28 | fr: { 29 | autoplay: 'Lecture automatique', 30 | next: 'À suivre', 31 | modal: { 32 | saveToWatchLater: 33 | 'Enregistrer dans la playlist "À regarder plus tard"', 34 | saveToPlaylist: 'Enregistrer dans une playlist', 35 | }, 36 | }, 37 | es: { 38 | autoplay: 'Reproducción automática', 39 | next: 'A continuación', 40 | modal: { 41 | saveToWatchLater: 'Guardar en Ver más tarde', 42 | saveToPlaylist: 'Guardar en una lista de reproducción', 43 | }, 44 | }, 45 | it: { 46 | autoplay: 'Riproduzione automatica', 47 | next: 'Prossimi video', 48 | modal: { 49 | saveToWatchLater: 'Salva in Guarda più tardi', 50 | saveToPlaylist: 'Salva in una playlist', 51 | }, 52 | }, 53 | }; 54 | 55 | const currentLanguage = languagesArray[1]; 56 | 57 | export default locales[currentLanguage]; 58 | -------------------------------------------------------------------------------- /src/components/navbar/SearchSuggestionContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SearchSuggestionContainer = (props) => { 4 | const { suggestions, changeCurrentSuggestionIndex } = props; 5 | 6 | // When selecting search suggestion with mouse 7 | const highlightSuggestion = (suggestionIndex) => { 8 | const suggestionsDOM = document.getElementsByClassName( 9 | 'suggestion-item' 10 | ); 11 | // This happens when the mouse leaves 12 | if (suggestionIndex === -1) changeCurrentSuggestionIndex(-1); 13 | // Adding or removing classes depending on the index of 14 | // the search suggestions selected 15 | for (let i = 0; i < suggestionsDOM.length; i++) { 16 | const currentSuggestion = suggestionsDOM[i]; 17 | if (i === suggestionIndex) { 18 | changeCurrentSuggestionIndex(i); 19 | currentSuggestion.classList.add('suggestion-item-highlighted'); 20 | } else 21 | currentSuggestion.classList.remove( 22 | 'suggestion-item-highlighted' 23 | ); 24 | } 25 | }; 26 | 27 | return ( 28 |
29 | {suggestions.map((sug, index) => { 30 | return ( 31 | props.searchSuggestion(e, sug[0])} 34 | className={'suggestion-item'} 35 | onMouseMove={() => highlightSuggestion(index)} 36 | onMouseLeave={() => highlightSuggestion(-1)} 37 | > 38 | {sug[0]} 39 | 40 | ); 41 | })} 42 |
43 | ); 44 | }; 45 | 46 | export default SearchSuggestionContainer; 47 | -------------------------------------------------------------------------------- /src/components/video/Comment.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { agoFormatting } from '../../helpers/helpers'; 4 | import CommentOptionsButton from './CommentOptionsButton'; 5 | import CommentsVotesContainer from './CommentsVotesContainer'; 6 | import UserComment from './UserComment'; 7 | import UserCommentImage from './UserCommentImage'; 8 | import UserData from './UserData'; 9 | 10 | const Comment = ({ comment, updateComments, index, comments }) => { 11 | const user = useSelector((state) => state.user); 12 | const uid = user ? user.uid : null; 13 | const { 14 | commenter_user_uid, 15 | created_at, 16 | id, 17 | image_source, 18 | likes, 19 | user_comment, 20 | display_name, 21 | user_liked_comment_status, 22 | } = comment; 23 | return ( 24 |
25 | 26 |
27 | 31 | 32 | 37 |
38 | {uid === commenter_user_uid && ( 39 | 46 | )} 47 |
48 | ); 49 | }; 50 | 51 | export default Comment; 52 | -------------------------------------------------------------------------------- /src/components/video/VideoWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { convertMinsSecs } from '../../helpers/helpers'; 5 | 6 | const VideoWrapper = (props) => { 7 | let nextVideos = props.relatedVideos.filter((video, index) => index < 12); 8 | 9 | return ( 10 |
11 |
12 | {nextVideos.map((video) => ( 13 | 23 |
24 | {video.title} 25 | 26 | {video.author.name} -{' '} 27 | {video.short_view_count_text} 28 | 29 | 30 | {convertMinsSecs( 31 | video.length_seconds * 1000, 32 | video.length_seconds 33 | )} 34 | 35 |
36 | 37 | ))} 38 |
39 |
40 | ); 41 | }; 42 | 43 | const mapStateToProps = (state) => { 44 | return { 45 | relatedVideos: state.relatedVideos, 46 | }; 47 | }; 48 | 49 | export default connect(mapStateToProps)(VideoWrapper); 50 | -------------------------------------------------------------------------------- /src/components/toast/toast.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/sass/mixins'; 2 | .toast-container { 3 | z-index: 2500; 4 | position: fixed; 5 | bottom: 0; 6 | left: 0; 7 | min-width: 24%; 8 | width: auto; 9 | height: auto; 10 | display: flex; 11 | flex-direction: column-reverse; 12 | justify-content: flex-start; 13 | align-items: center; 14 | z-index: 5000; 15 | transition: 0.25s linear height !important; 16 | padding-left: 12px; 17 | @include responsivePhoneSize { 18 | width:100%; 19 | font-size: 0.85rem; 20 | padding: 0; 21 | } 22 | } 23 | .toast-message-container { 24 | z-index: 2500; 25 | width: 100%; 26 | background-color: rgb(53, 52, 52); 27 | margin-bottom: 4%; 28 | border-radius: 3px; 29 | display: flex; 30 | justify-content: flex-start; 31 | align-items: center; 32 | padding: 18px 12px 18px 24px; 33 | animation-name: toastactivation; 34 | animation-duration: 1s; 35 | animation-timing-function: ease-in-out; 36 | animation-iteration-count: initial; 37 | animation-fill-mode: forwards; 38 | transition: 0.3s linear all !important; 39 | @include responsivePhoneSize { 40 | width: 100%; 41 | margin-bottom: 0%; 42 | border-radius: 2px; 43 | } 44 | } 45 | .toast-container div span { 46 | font-size: 0.9rem; 47 | } 48 | 49 | .toast-message-container-fade-out { 50 | animation-name: toastdeactivation; 51 | animation-duration: 1s; 52 | animation-timing-function: ease-in-out; 53 | animation-iteration-count: initial; 54 | animation-fill-mode: forwards; 55 | } 56 | 57 | @keyframes toastactivation { 58 | 0% { 59 | transform: translateY(120vh); 60 | } 61 | 100% { 62 | transform: translateY(0px); 63 | } 64 | } 65 | @keyframes toastdeactivation { 66 | 0% { 67 | transform: translateY(0px); 68 | } 69 | 100% { 70 | transform: translateY(120vh); 71 | } 72 | } -------------------------------------------------------------------------------- /src/components/playlists/searchhistory.scss: -------------------------------------------------------------------------------- 1 | .search_item-container { 2 | width: 100%; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: flex-start; 6 | padding-left: 20px; 7 | padding-top: 5px; 8 | margin-top: 5px; 9 | padding-bottom: 8px; 10 | margin-bottom: 8px; 11 | text-decoration: none; 12 | color: inherit; 13 | cursor: pointer; 14 | &:nth-child(1) { 15 | padding-top: 30px; 16 | } 17 | .search_data-container { 18 | display: flex; 19 | flex-direction: column; 20 | .search_term { 21 | font-weight: 600; 22 | padding-bottom: 6px; 23 | } 24 | .search_date { 25 | font-size: 0.8rem; 26 | opacity: 0.6; 27 | } 28 | } 29 | .tooltip-container { 30 | z-index: 500; 31 | .close-btn { 32 | fill: rgba(255, 255, 255, 0.4); 33 | width: 18px; 34 | height: 18px; 35 | margin-right: 20px; 36 | } 37 | } 38 | } 39 | .deleted_item-container { 40 | display: flex; justify-content: center; 41 | align-items: center; 42 | padding-top: 28px; 43 | padding-bottom: 28px; 44 | } 45 | 46 | .search_term_deleted { 47 | font-weight: 400; 48 | font-size: 0.8rem; 49 | text-align: center; 50 | opacity: 0.55; 51 | } 52 | 53 | .playlist-container--light { 54 | .search_item-container { 55 | .search_data-container { 56 | .search_term { 57 | color: #222222; 58 | } 59 | .search_date { 60 | color: #222222; 61 | } 62 | } 63 | .tooltip-container { 64 | .close-btn { 65 | fill: rgba(0, 0, 0, 0.4); 66 | } 67 | } 68 | } 69 | .search_term_deleted { 70 | color: #222222; 71 | } 72 | .activity_manager-container { 73 | color: #222222; 74 | } 75 | } 76 | 77 | .delete-playlist-modal { 78 | cursor: pointer; 79 | position: relative; 80 | } -------------------------------------------------------------------------------- /src/components/navbar/SidenavUnactiveItems.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Home, Library, Subscriptions, Trending } from '../../assets/Icons'; 4 | import locales from '../../locales/navbar'; 5 | 6 | const SideNavUnactiveItems = ({ history }) => { 7 | const { pathname } = history.location; 8 | return ( 9 | <> 10 | 16 | 17 | {locales.sidenav.home} 18 | 19 | 25 | 26 | {locales.sidenav.trending} 27 | 28 | 36 | 37 | {locales.sidenav.subscriptions} 38 | 39 | 45 | 46 | {locales.sidenav.library} 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default SideNavUnactiveItems; 53 | -------------------------------------------------------------------------------- /src/components/videothumbnail/VideoContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CloseButton, Expand, Pause, Play } from '../../assets/Icons'; 3 | 4 | const VideoContainer = (props) => { 5 | const { video, playPause, _goToVideo, close, videoNotAvailable } = props; 6 | let containerClassName = 'miniature-player-content'; 7 | if (videoNotAvailable) 8 | containerClassName += ' miniature-player-content-not-available'; 9 | return ( 10 |
props.controlVideoWithKeys(e)} 13 | className={containerClassName} 14 | > 15 | {props.children} 16 |
17 |
27 | {!video ? ( 28 | 29 | ) : video.paused ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 |
35 |
_goToVideo(e)} 38 | style={{ position: 'absolute', top: 8, left: 8 }} 39 | > 40 | 41 |
42 |
47 | 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default VideoContainer; 55 | -------------------------------------------------------------------------------- /src/components/navbar/BottomNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link, withRouter } from "react-router-dom"; 3 | import { Home, Library, Subscriptions, Trending } from "../../assets/Icons"; 4 | import locales from "../../locales/navbar"; 5 | 6 | // Paths where the bottom navbar should be active 7 | const paths = [ 8 | "/", 9 | "/feed/trending", 10 | "/feed/channels", 11 | "/feed/library", 12 | "/playlist", 13 | ]; 14 | 15 | // Bottom Navbar for mobile devices 16 | class BottomNavbar extends Component { 17 | render() { 18 | const { pathname } = this.props.location; 19 | // If the current pathname is included in paths 20 | const pathExists = paths.includes(pathname); 21 | // If current path is not included, do not render anything 22 | if (!pathExists) return <>; 23 | return ( 24 |
25 | 29 | 30 | {locales.bottomNav.home} 31 | 32 | 38 | 39 | {locales.bottomNav.trending} 40 | 41 | 47 | 48 | {locales.bottomNav.subscriptions} 49 | 50 | 56 | 57 | {locales.bottomNav.library} 58 | 59 |
60 | ); 61 | } 62 | } 63 | 64 | export default withRouter(BottomNavbar); 65 | -------------------------------------------------------------------------------- /src/components/shared/css/notloggedscreen.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/sass/mixins'; 2 | 3 | .not-logged-screen-container { 4 | width: 100%; 5 | min-height: 100vh; 6 | padding-left: 17.5vw; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | flex-direction: column; 11 | background-color:rgba(0, 0, 0, 0.92); 12 | @include responsivePhoneSize { 13 | padding: 0px 18px; 14 | } 15 | .not-logged-main-icon { 16 | width:105px; 17 | fill: white; 18 | opacity: 0.4; 19 | margin-bottom: 28px; 20 | } 21 | .title { 22 | font-size: 1.5rem; 23 | font-weight: 400; 24 | margin-bottom: 16px; 25 | text-align: center; 26 | } 27 | .description { 28 | font-size: 0.9rem; 29 | font-weight: 400; 30 | margin-bottom: 24px; 31 | text-align: center; 32 | width: 80%; 33 | } 34 | .login-btn { 35 | position:relative; 36 | border:1pt solid #0660d7; 37 | background-color:transparent; 38 | width: 9.5%; 39 | height: 38px !important; 40 | display:flex; 41 | justify-content: space-between !important; 42 | align-items: center; 43 | cursor: pointer; 44 | @include responsivePhoneSize { 45 | width: 30%; 46 | justify-content: center !important; 47 | } 48 | span { 49 | font-size:0.9rem; 50 | text-transform: uppercase; 51 | color: #0660d7; 52 | font-weight: 500; 53 | @include responsivePhoneSize { 54 | text-align: center; 55 | } 56 | } 57 | div { 58 | width: 25px; 59 | height:25px; 60 | border-radius: 30px; 61 | background-color: #0660d7; 62 | display:flex; 63 | justify-content: center; 64 | align-items: center; 65 | i { 66 | color: white !important; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .not-logged-screen-container--light-theme { 73 | background-color: #F9F9F9; 74 | .title, .description { 75 | color: black; 76 | } 77 | .not-logged-main-icon { 78 | fill: black; 79 | } 80 | } -------------------------------------------------------------------------------- /src/components/shared/HomeVideoThumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ownerBadgeText } from '../../helpers/helpers'; 3 | import OwnerBadges from './OwnerBadges'; 4 | import ToolTip from './ToolTip'; 5 | 6 | const HomeVideoThumbnail = (props) => { 7 | const { video, pathname, index, onClick, onClickParams } = props; 8 | const { date, owner_badges } = video; 9 | return ( 10 | { 12 | e.preventDefault(); 13 | onClick(onClickParams); 14 | }} 15 | key={video.uri + index} 16 | title={video.title} 17 | className="home-link" 18 | href={pathname} 19 | > 20 | {props.children} 21 |
22 |
23 | Channe thumbnail 28 |
29 |
30 | {video.title} 31 | 32 | 33 | 34 | {video.channel} 35 | 36 | 37 | {owner_badges && ( 38 | 42 | } 43 | /> 44 | )} 45 | • {video.views} 46 | 47 | 48 | {video.views} • {date && date} 49 | 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default HomeVideoThumbnail; 57 | -------------------------------------------------------------------------------- /src/components/shared/searchthumbnail/Metadata.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ownerBadgeText } from '../../../helpers/helpers'; 3 | import OwnerBadges from '../OwnerBadges'; 4 | import ToolTip from '../ToolTip'; 5 | import Author from './Author'; 6 | import Badges from './Badges'; 7 | 8 | const Metadata = (props) => { 9 | const { video, trending } = props; 10 | const { title, badges, owner_badges, channel } = video; 11 | 12 | if (trending) { 13 | let video_metadata = 14 | props.video.date === props.video.views 15 | ? ` ${props.video.views}` 16 | : ` ${props.video.views} • ${props.video.date}`; 17 | return ( 18 |
19 | {title} 20 | 21 | 22 | {channel} 23 | 24 |   25 | {owner_badges && ( 26 | } 29 | /> 30 | )} 31 | {video_metadata} 32 | 33 | 37 |
38 | ); 39 | } 40 | 41 | let video_metadata = 42 | props.video.date === props.video.views 43 | ? ` ${props.video.views}` 44 | : ` ${props.video.views} • ${props.video.date}`; 45 | return ( 46 |
47 | {title} 48 | {video_metadata} 49 | 50 | 54 | {badges && } 55 |
56 | ); 57 | }; 58 | 59 | export default Metadata; 60 | -------------------------------------------------------------------------------- /src/components/video/PlaybackSpeedContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from '../../locales/video'; 3 | import ModalItemTitle from './ModalItemTitle'; 4 | import VideoModalHeader from './VideoModalHeader'; 5 | import VideoModalHeaderButton from './VideoModalHeaderButton'; 6 | import VideoModalHeaderSecondaryButton from './VideoModalHeaderSecondaryButton'; 7 | import VideoModalItem from './VideoModalItem'; 8 | 9 | const PlaybackSpeedContainer = ({ 10 | onClickMainButton, 11 | onClickSecondaryButton, 12 | rate, 13 | slowMotion, 14 | }) => { 15 | const rates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; 16 | return ( 17 | <> 18 | 19 | 20 | * {locales.videoPlayer.modal.playbackSpeed} 21 | 22 | 25 | {locales.videoPlayer.modal.custom} 26 | 27 | 28 | {rates.map((rateItem) => { 29 | return ( 30 | { 32 | slowMotion(rateItem); 33 | onClickMainButton(); 34 | }} 35 | > 36 | {rateItem === rate ? ( 37 | 38 | *{' '} 39 | {rateItem === 1 40 | ? locales.videoPlayer.modal.normal 41 | : rateItem} 42 | 43 | ) : ( 44 | 45 | {rateItem === 1 46 | ? locales.videoPlayer.modal.normal 47 | : rateItem} 48 | 49 | )} 50 | 51 | ); 52 | })} 53 | 54 | ); 55 | }; 56 | 57 | export default PlaybackSpeedContainer; 58 | -------------------------------------------------------------------------------- /src/components/video/CommentOptionsButton.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Delete } from '../../assets/Icons'; 4 | import { deleteComment } from '../../helpers/helpers'; 5 | import locales from '../../locales/comments'; 6 | import ButtonContainer from '../relatedvideos/ButtonContainer'; 7 | import SmallModal from '../shared/SmallModal'; 8 | 9 | const CommentOptionsButton = ({ 10 | updateComments, 11 | index, 12 | commentId, 13 | comments, 14 | }) => { 15 | const [isModalActive, setIsModalActive] = useState(false); 16 | const dispatch = useDispatch(); 17 | const deleteUserComment = async () => { 18 | const response = await deleteComment(commentId); 19 | const { error } = response.data; 20 | if (!error) { 21 | let newComments = [...comments]; 22 | newComments.splice(index, 1); 23 | updateComments([...newComments]); 24 | const payload = { 25 | toast_message: 'Comment deleted', 26 | id: String(new Date()) + 'deleted comment', 27 | }; 28 | dispatch({ type: 'ADD_TOAST_NOTIFICATION', payload }); 29 | } 30 | }; 31 | return ( 32 | 33 | {isModalActive && ( 34 | setIsModalActive(false)} 41 | autoWidth 42 | > 43 |
44 | 45 | {locales.modal.options.delete} 46 |
47 |
48 | )} 49 | { 51 | e.preventDefault(); 52 | setIsModalActive(true); 53 | }} 54 | className="btn-options" 55 | href="/" 56 | > 57 | 58 | 59 |
60 | ); 61 | }; 62 | 63 | export default CommentOptionsButton; 64 | -------------------------------------------------------------------------------- /src/components/shared/css/confirmactionmodal.css: -------------------------------------------------------------------------------- 1 | .confirm-action-modal { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | z-index: 10000; 11 | } 12 | .confirm-action-backdrop { 13 | position: absolute; 14 | top:0; 15 | left:0; 16 | width: 100%; 17 | height: 100%; 18 | background-color: rgba(0, 0, 0, 0.75); 19 | z-index: 0; 20 | } 21 | .title-info-container { 22 | padding: 24px 30px 0px 24px; 23 | } 24 | .title-info-container h3 { 25 | font-size: 1rem; 26 | font-weight: 400; 27 | } 28 | .confirm-action-info { 29 | width: auto; 30 | height: auto; 31 | background-color: rgb(39, 39, 39); 32 | border-radius: 2px; 33 | display:flex; 34 | flex-direction: column; 35 | justify-content: flex-start; 36 | align-items: flex-start; 37 | z-index: 1; 38 | -webkit-box-shadow: 0px 0px 18px 0px rgba(0,0,0,1); 39 | -moz-box-shadow: 0px 0px 18px 0px rgba(0,0,0,1); 40 | box-shadow: 0px 0px 18px 0px rgba(0,0,0,1); 41 | } 42 | .playlist-container--light .confirm-action-info { 43 | background-color: #FFFFFF; 44 | color: #222222; 45 | } 46 | .confirm-action-info--big { 47 | width: 50%; 48 | } 49 | .confirm-action-info div { 50 | width:100%; 51 | flex: 1; 52 | } 53 | .description-info-container { 54 | display: flex; 55 | justify-content: flex-start; 56 | align-items: center; 57 | padding: 28px 40px 28px 24px; 58 | } 59 | .description-info-container span { 60 | font-size: 0.85rem; 61 | opacity: 0.5; 62 | line-height: 1.3rem; 63 | } 64 | .confirm-action-options { 65 | display: flex; 66 | justify-content: flex-end; 67 | align-items: center; 68 | padding-right: 26px; 69 | border-top: 1pt solid rgba(255, 255, 255, 0.1); 70 | padding:20px 22px 20px 30px; 71 | } 72 | .playlist-container--light .confirm-action-options { 73 | border-top-color: rgba(0, 0, 0, 0.1); 74 | } 75 | .confirm-action-options span { 76 | font-size: 0.9rem; 77 | font-weight: 500; 78 | text-transform: uppercase; 79 | color: rgb(33, 119, 199); 80 | cursor: pointer; 81 | } 82 | .confirm-action-options span:first-of-type { 83 | margin-right: 30px; 84 | opacity: 0.5; 85 | color:white; 86 | } 87 | .playlist-container--light .confirm-action-modal span:first-of-type { 88 | color: #222222; 89 | opacity: 0.7; 90 | } -------------------------------------------------------------------------------- /src/components/playlists/OptionsButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import PlaylistModal from './PlaylistModal'; 3 | 4 | class OptionsButton extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | isModalActive: false, 9 | modal_position_up: false, 10 | }; 11 | this.button = createRef(); 12 | } 13 | 14 | closeModal = () => { 15 | this.setState({ 16 | isModalActive: false, 17 | modal_position_up: false, 18 | }); 19 | }; 20 | 21 | openModal = () => { 22 | const button = this.button.current; 23 | const space = window.innerHeight - button.getBoundingClientRect().top; 24 | let modal_position_up = space < 213 ? true : false; 25 | this.setState({ 26 | isModalActive: !this.state.isModalActive, 27 | modal_position_up, 28 | }); 29 | }; 30 | 31 | render() { 32 | const { isModalActive } = this.state; 33 | const { 34 | playlistName = '', 35 | addToastNotification, 36 | changeModalStatus, 37 | playlistData, 38 | getLikedVideos, 39 | } = this.props; 40 | return ( 41 |
42 | {isModalActive && ( 43 | 57 | )} 58 | 64 | 65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default OptionsButton; 72 | -------------------------------------------------------------------------------- /src/components/VisitorsModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { updateVisitorModal } from '../actions/reduxActions'; 4 | import { login } from '../helpers/helpers'; 5 | import locales from '../locales/visitorsmodal'; 6 | 7 | class VisitorsModal extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = {}; 11 | } 12 | 13 | componentDidMount() { 14 | document.body.style = 'overflow:hidden'; 15 | } 16 | 17 | closeModal = () => { 18 | document.body.style = 'overflow:auto'; 19 | this.props.updateVisitorModal(); 20 | }; 21 | 22 | render() { 23 | const msg = this.props.download_limit 24 | ? "You've reached the limits of download tests!" 25 | : locales.title; 26 | return ( 27 |
28 | 58 | ); 59 | } 60 | } 61 | 62 | const mapDispatchToProps = (dispatch) => { 63 | return { 64 | updateVisitorModal: () => dispatch(updateVisitorModal()), 65 | }; 66 | }; 67 | 68 | const mapStateToProps = (state) => { 69 | return { 70 | download_limit: state.download_limit, 71 | }; 72 | }; 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(VisitorsModal); 75 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/actions/reduxActions.js: -------------------------------------------------------------------------------- 1 | export const toggleTheme = () => { 2 | return { 3 | type: 'TOGGLE_DARKTHEME', 4 | }; 5 | }; 6 | 7 | export const mutateRelatedVideos = (payload) => { 8 | return { 9 | type: 'GET_RELATEDVIDEOS', 10 | payload, 11 | }; 12 | }; 13 | 14 | export const toggleAutoplay = () => { 15 | return { 16 | type: 'TOGGLE_AUTOPLAY', 17 | }; 18 | }; 19 | 20 | export const updateAuthUserData = (payload) => { 21 | return { 22 | type: 'UPDATE_USERDATA', 23 | payload, 24 | }; 25 | }; 26 | 27 | export const toggleSidenav = (payload = null) => { 28 | return { 29 | type: 'TOGGLE_SIDENAV', 30 | payload, 31 | }; 32 | }; 33 | 34 | export const toggleSidenavHome = () => { 35 | return { 36 | type: 'TOGGLE_SIDENAV-HOME', 37 | }; 38 | }; 39 | 40 | export const updateVideoEnded = (payload) => { 41 | return { 42 | type: 'UPDATE_VIDEOENDED', 43 | payload, 44 | }; 45 | }; 46 | 47 | export const updateHomeSearch = (payload) => { 48 | return { 49 | type: 'UPDATE_HOMESEARCH', 50 | payload, 51 | }; 52 | }; 53 | 54 | export const updateVideoThumb = (payload) => { 55 | return { 56 | type: 'UPDATE_THUMBACTIVE', 57 | payload, 58 | }; 59 | }; 60 | 61 | export const updateCurrentVideoData = (payload) => { 62 | return { 63 | type: 'UPDATE_CURRENT_VIDEO_DATA', 64 | payload, 65 | }; 66 | }; 67 | 68 | export const updateLoadingState = (payload) => { 69 | return { 70 | type: 'UPDATE_LOADING_STATE', 71 | payload, 72 | }; 73 | }; 74 | 75 | export const updateVisitorModal = (payload) => { 76 | return { 77 | type: 'UPDATE_VISITOR_MODAL', 78 | payload, 79 | }; 80 | }; 81 | 82 | export const updatePlaylists = (payload) => { 83 | return { 84 | type: 'UPDATE_PLAYLISTS', 85 | payload, 86 | }; 87 | }; 88 | 89 | export const addToastNotification = (payload) => { 90 | return { 91 | type: 'ADD_TOAST_NOTIFICATION', 92 | payload, 93 | }; 94 | }; 95 | export const removeLastToastNotification = () => { 96 | return { 97 | type: 'REMOVE_TOAST_NOTIFICATION', 98 | }; 99 | }; 100 | export const updateSearchResults = (payload) => { 101 | return { 102 | type: 'UPDATE_SEARCH_RESULTS', 103 | payload, 104 | }; 105 | }; 106 | export const toggleTheaterMode = () => { 107 | return { 108 | type: 'UPDATE_THEATER_MODE', 109 | }; 110 | }; 111 | export const updateVideoChapters = () => { 112 | return { 113 | type: 'UPDATE_VIDEO_CHAPTERS', 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/shared/HistoryVideoThumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Overlay from './Overlay'; 3 | import ToolTip from './ToolTip'; 4 | 5 | const HistoryVideoThumbnail = ({ 6 | video, 7 | search, 8 | getVideos, 9 | user, 10 | pathname, 11 | OptionsButton, 12 | onClick, 13 | onClickParams, 14 | isMiniplayerActive, 15 | currentVideo, 16 | }) => { 17 | return ( 18 |
23 | { 25 | e.preventDefault(); 26 | onClick(onClickParams); 27 | }} 28 | href={pathname} 29 | className="video-basic-data" 30 | > 31 | 72 | ); 73 | }; 74 | 75 | export default HistoryVideoThumbnail; 76 | -------------------------------------------------------------------------------- /src/components/shared/LibraryThumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { 5 | mutateRelatedVideos, 6 | updateCurrentVideoData, 7 | updateLoadingState, 8 | } from '../../actions/reduxActions'; 9 | import { _goToVideo } from '../../helpers/navigation'; 10 | 11 | const LibraryThumbnail = ({ 12 | video, 13 | updateLoadingState, 14 | updateCurrentVideoData, 15 | mutateRelatedVideos, 16 | history, 17 | }) => { 18 | const { video_id, video_title, video_channel, video_duration } = video; 19 | 20 | const goToVideo = (e, video_data) => { 21 | e.preventDefault(); 22 | video_data['uri'] = video.video_id; 23 | _goToVideo( 24 | video_data, 25 | updateLoadingState, 26 | updateCurrentVideoData, 27 | mutateRelatedVideos, 28 | history 29 | ); 30 | }; 31 | 32 | return ( 33 | goToVideo(e, video)} 35 | key={video_id} 36 | title={video_title} 37 | className="library-thumbnail" 38 | href={'/watch?v=' + video.video_id} 39 | > 40 |
41 | {''} 42 | {video_duration} 43 | {video.current_length !== undefined && ( 44 |
45 |
53 |
54 | )} 55 |
56 |
57 |
58 | {video.video_title} 59 | 60 | {video_channel} • {video.views} 61 | 62 | {video.views} 63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | const mapDispatchToProps = (dispatch) => { 70 | return { 71 | updateLoadingState: (payload) => dispatch(updateLoadingState(payload)), 72 | mutateRelatedVideos: (payload) => 73 | dispatch(mutateRelatedVideos(payload)), 74 | updateCurrentVideoData: (payload) => 75 | dispatch(updateCurrentVideoData(payload)), 76 | }; 77 | }; 78 | 79 | export default withRouter(connect(null, mapDispatchToProps)(LibraryThumbnail)); 80 | -------------------------------------------------------------------------------- /src/components/library/components/SectionContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { History, Playlist, ThumbUp, WatchLater } from '../../../assets/Icons'; 4 | import locales from '../../../locales/library'; 5 | import VideosContainer from './VideosContainer'; 6 | 7 | const SectionContainer = ({ 8 | title, 9 | videos, 10 | hasVideoCount, 11 | videoCount, 12 | pathname, 13 | iconName, 14 | }) => { 15 | const playlist = title === locales.headers.playlists; 16 | const className = 17 | videos.length === 0 || !videos 18 | ? 'section-container empty' 19 | : 'section-container'; 20 | return ( 21 |
22 | 31 | {} 32 |
33 | ); 34 | }; 35 | 36 | const SectionHeader = ({ 37 | title, 38 | hasVideoCount, 39 | pathname, 40 | videoCount, 41 | playlist, 42 | iconName, 43 | videos, 44 | }) => { 45 | const getIcon = () => { 46 | const iconClassName = 'header-container-icon'; 47 | if (iconName === 'history') { 48 | return ; 49 | } else if (iconName === 'likedvideos') { 50 | return ; 51 | } else if (iconName === 'watchlater') { 52 | return ; 53 | } 54 | return ; 55 | }; 56 | 57 | return ( 58 |
59 | 60 | {getIcon()} 61 | 62 | {title} 63 | {hasVideoCount && ( 64 | {videoCount} 65 | )} 66 | 67 | 68 | 69 | 77 | {locales.headers.buttons.seeAll} 78 | 79 | 80 |
81 | ); 82 | }; 83 | 84 | export default SectionContainer; 85 | -------------------------------------------------------------------------------- /src/components/relatedvideos/RelatedVideoModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Playlist, WatchLater } from '../../assets/Icons'; 3 | import { addVideoToPlaylist } from '../../helpers/helpers'; 4 | import locales from '../../locales/relatedvideos'; 5 | import SmallModal from '../shared/SmallModal'; 6 | 7 | class RelatedVideoModal extends Component { 8 | addToPlaylist = async () => { 9 | const { user, closeDropdown, addToastNotification } = this.props; 10 | 11 | if (user === null) return false; 12 | const { uid } = user; 13 | const { 14 | title, 15 | author, 16 | id, 17 | length_seconds, 18 | thumbnails, 19 | } = this.props.video; 20 | const thumbnail = thumbnails[thumbnails.length - 1].url; 21 | const playlistName = 'WatchLater'; 22 | const video_object = { 23 | uid, 24 | videoid: id, 25 | title, 26 | uploader: author.name, 27 | length_seconds, 28 | playlistName, 29 | type: 1, 30 | thumbnail, 31 | }; 32 | const response = await addVideoToPlaylist(video_object); 33 | if (response.status === 200) { 34 | addToastNotification({ 35 | toast_message: 'Saved to Watch later', 36 | id: String(new Date()) + 'added_to_wl', 37 | }); 38 | closeDropdown(); 39 | } 40 | }; 41 | 42 | openPlaylistModal = async () => { 43 | this.props.openPlaylistModal(false); 44 | this.props.closeDropdown(); 45 | }; 46 | 47 | render() { 48 | const { 49 | is_modal_up, 50 | closeDropdown, 51 | user, 52 | updateVisitorModal, 53 | } = this.props; 54 | return ( 55 | 56 |
57 |
63 | 64 | {locales.modal.saveToWatchLater} 65 |
66 |
72 | 73 | {locales.modal.saveToPlaylist} 74 |
75 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | export default RelatedVideoModal; 82 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 17 | 25 | 29 | 30 | 35 | 40 | 45 | 50 | 55 | 59 | 60 | 69 | CloneTube 70 | 71 | 72 | 73 |
74 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/components/shared/css/confirmactionmodal.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/sass/mixins'; 2 | 3 | .confirm-action-modal { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100vw; 8 | height: 100vh; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | z-index: 10000; 13 | .confirm-action-backdrop { 14 | position: absolute; 15 | top:0; 16 | left:0; 17 | width: 100%; 18 | height: 100%; 19 | background-color: rgba(0, 0, 0, 0.8); 20 | z-index: 0; 21 | } 22 | .confirm-action-info { 23 | width: auto; 24 | height: auto; 25 | background-color: rgb(39, 39, 39); 26 | border-radius: 2px; 27 | display:flex; 28 | flex-direction: column; 29 | justify-content: flex-start; 30 | align-items: flex-start; 31 | z-index: 1; 32 | -webkit-box-shadow: 0px 0px 4px 0px rgba(0,0,0,1); 33 | -moz-box-shadow: 0px 0px 4px 0px rgba(0,0,0,1); 34 | box-shadow: 0px 0px 4px 0px rgba(0,0,0,1); 35 | @include responsivePhoneSize { 36 | width: 90% !important; 37 | } 38 | div { 39 | width:100%; 40 | flex: 1; 41 | } 42 | .title-info-container { 43 | padding: 24px 30px 0px 24px; 44 | h3 { 45 | font-size: 1rem; 46 | font-weight: 400; 47 | } 48 | } 49 | .description-info-container { 50 | display: flex; 51 | justify-content: flex-start; 52 | align-items: center; 53 | padding: 28px 40px 28px 24px; 54 | span { 55 | font-size: 0.85rem; 56 | opacity: 0.5; 57 | line-height: 1.3rem; 58 | } 59 | } 60 | .confirm-action-options { 61 | display: flex; 62 | justify-content: flex-end; 63 | align-items: center; 64 | padding-right: 26px; 65 | border-top: 1pt solid rgba(255, 255, 255, 0.1); 66 | padding:20px 22px 20px 30px; 67 | span { 68 | font-size: 0.9rem; 69 | font-weight: 500; 70 | text-transform: uppercase; 71 | color: rgb(33, 119, 199); 72 | cursor: pointer; 73 | &:first-of-type { 74 | margin-right: 30px; 75 | opacity: 0.5; 76 | color:white; 77 | } 78 | } 79 | } 80 | &.confirm-action-info--big { 81 | width: 50%; 82 | } 83 | } 84 | } 85 | 86 | .playlist-container--light, .feed-channel-container--light-theme { 87 | .confirm-action-info { 88 | background-color: #FFFFFF; 89 | color: #222222; 90 | .confirm-action-options { 91 | border-top-color: rgba(0, 0, 0, 0.1); 92 | span:first-of-type { 93 | color: #222222; 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloneTube FrontEnd (ReactJS) 1.0.0 2 | 3 | CloneTube is a [YouTube](https://www.youtube.com) clone web app made with ReactJS, NodeJS, MySQL. **_It's use is merely educative and to show technical skills, each user is responsible for the use of this application._** 4 | Videos are blocked geologically depending of the location of the server. 5 | 6 | ## Features 7 | 8 | **Elements with a red text color are upcoming features** 9 | 10 | The app count with the next sections on it: 11 | 12 | ### Sections 13 | 14 | **Home:** This view shows the recommended videos of the _Youtube_ home page. 15 | 16 | **Trending:** The page of trending videos show the videos that are more popular at the moment. 17 | 18 | **Video:** It plays the video in a Vanilla made Youtube Video Player Clone, and recommends a group of recommended videos, you also can like or dislike the video, create a playlist or/and add the video to a playlist (you can also do this with the related videos) or you can download the video in a series of formats. 19 | 20 | **Search:** You can search a video in the youtube database, it only return 20 search results as it uses web scrapping (***not *Youtube* API***). 21 | 22 | **Playlist:** You can manage your created playlists as well as your _Favorites_ and _Watch Later_ playlists. 23 | 24 | **History:** All the activity you've done in the app, see when you watched videos, or what you searched, you can delete one term or one video, or you can delete it all. 25 | 26 | **Library:** All your general data about your app activity, how many liked videos, subscriptions, and your more recent activity in your playlists, last watched videos, and videos on your playlists. 27 | 28 | ### Most significant features 29 | 30 | - **Youtube Top Loading Bar** 31 | - **Dark and Light Mode** 32 | - **Search autocomplete** The search bar sugggests you search terms as you type in the search form. 33 | - **Miniature Video Playing** (navigate through the app while the video is playing in a miniature fixed player) 34 | - **Modals** that position up or down from their parents element depending on the offset space below them. 35 | - **Formatting of numbers**, i.e. current time of a video in seconds to a formatted _mm:ss_ or _hh:mm:ss_ depending of the duration of the video; number of suscribers or likes from _100234_ to _100,200 suscribers_ and _100K_ respectively. 36 | - **Video Player** made from scratch with JS and ReactJS with most of the Youtube Original Video Player. 37 | - **Google User Authentication** implemented with Firebase and Google Sign In. 38 | - **Creation and management of Playlists**, CRUD operations to control your playlists, managed by a MySQL database. 39 | - **Comments and video descriptions**: comments and video descripton section with CRUD operations as well as regex functions to identify timestamps and links. 40 | - **Video player settings modal** 41 | 42 | ## Upcoming Features 43 | 44 | - **Channel Page**: a page to navigate through **YouTube** channels and see all their videos. 45 | - **Drag to reorder playlists** 46 | - **Video queueing** 47 | - **Options modal in search page** 48 | -------------------------------------------------------------------------------- /src/components/video/CommentsVotesContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { voteComment } from '../../helpers/helpers'; 3 | import VoteButton from './VoteButton'; 4 | 5 | const CommentsVotesContainer = ({ 6 | likes, 7 | commentId, 8 | userCommentLikedStatus, 9 | }) => { 10 | const [isLiked, setIsLiked] = useState(false); 11 | const [isDisliked, setIsDisliked] = useState(false); 12 | const [isLikedSinceMounted, setIsLikedSinceMounted] = useState(false); 13 | 14 | const upvoteComment = async () => { 15 | let is_liked = 0; 16 | let is_disliked = 0; 17 | if (isDisliked) { 18 | setIsDisliked(false); 19 | is_disliked = 0; 20 | setIsLiked(true); 21 | is_liked = 1; 22 | } else if (isLiked) { 23 | setIsLiked(false); 24 | is_liked = 0; 25 | } else { 26 | setIsLiked(true); 27 | is_liked = 1; 28 | } 29 | await voteComment({ 30 | uid: 0, 31 | comment_id: commentId, 32 | is_liked, 33 | is_disliked, 34 | }); 35 | }; 36 | const downvoteComment = async () => { 37 | let is_liked = 0; 38 | let is_disliked = 0; 39 | if (isLiked) { 40 | setIsLiked(false); 41 | is_liked = 0; 42 | setIsDisliked(true); 43 | is_disliked = 1; 44 | } else if (isDisliked) { 45 | setIsDisliked(false); 46 | is_disliked = 0; 47 | } else { 48 | is_disliked = 1; 49 | setIsDisliked(true); 50 | } 51 | await voteComment({ 52 | uid: 0, 53 | comment_id: commentId, 54 | is_liked, 55 | is_disliked, 56 | }); 57 | }; 58 | 59 | useEffect(() => { 60 | if (userCommentLikedStatus === 'liked') { 61 | setIsLiked(true); 62 | setIsDisliked(false); 63 | setIsLikedSinceMounted(true); 64 | } else if (userCommentLikedStatus === 'disliked') { 65 | setIsLiked(false); 66 | setIsDisliked(true); 67 | } else { 68 | setIsDisliked(false); 69 | setIsLiked(false); 70 | } 71 | }, [userCommentLikedStatus]); // check this hook effect 72 | 73 | const numberOfLikes = (num) => { 74 | if (isLikedSinceMounted) num = num - 1; 75 | if (isDisliked) return num; 76 | if (isLiked) return num + 1; 77 | return num; 78 | }; 79 | 80 | return ( 81 |
82 | 88 | 93 |
94 | ); 95 | }; 96 | 97 | export default CommentsVotesContainer; 98 | -------------------------------------------------------------------------------- /src/locales/comments.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | title: { 6 | totalComments: (total) => total + ' comentaris', 7 | }, 8 | form: { 9 | placeholder: 'Afegeix un comentari públic…', 10 | buttons: { 11 | cancel: 'Cancel·la', 12 | confirm: 'Comenta', 13 | }, 14 | }, 15 | modal: { 16 | options: { 17 | delete: 'Suprimeix', 18 | }, 19 | }, 20 | }, 21 | de: { 22 | title: { 23 | totalComments: (total) => total + ' Kommentare', 24 | }, 25 | form: { 26 | placeholder: 'Öffentlich kommentieren…', 27 | buttons: { 28 | cancel: 'Abbrechen', 29 | confirm: 'Kommentieren', 30 | }, 31 | }, 32 | modal: { 33 | options: { 34 | delete: 'Löschen', 35 | }, 36 | }, 37 | }, 38 | en: { 39 | title: { 40 | totalComments: (total) => total + ' comments', 41 | }, 42 | form: { 43 | placeholder: 'Add a public comment...', 44 | buttons: { 45 | cancel: 'Cancel', 46 | confirm: 'Comment', 47 | }, 48 | }, 49 | modal: { 50 | options: { 51 | delete: 'Delete', 52 | }, 53 | }, 54 | }, 55 | es: { 56 | title: { 57 | totalComments: (total) => total + ' comentarios', 58 | }, 59 | form: { 60 | placeholder: 'Agrega un comentario público…', 61 | buttons: { 62 | cancel: 'Cancelar', 63 | confirm: 'Comentar', 64 | }, 65 | }, 66 | modal: { 67 | options: { 68 | delete: 'Borrar', 69 | }, 70 | }, 71 | }, 72 | it: { 73 | title: { 74 | totalComments: (total) => total + ' commenti', 75 | }, 76 | form: { 77 | placeholder: 'Aggiungi un commento pubblico...', 78 | buttons: { 79 | cancel: 'Annulla', 80 | confirm: 'Commenta', 81 | }, 82 | }, 83 | modal: { 84 | options: { 85 | delete: 'Elimina', 86 | }, 87 | }, 88 | }, 89 | fr: { 90 | title: { 91 | totalComments: (total) => total + ' commentaires', 92 | }, 93 | form: { 94 | placeholder: 'Ajouter un commentaire public...', 95 | buttons: { 96 | cancel: 'Annuler', 97 | confirm: 'Ajouter un commentaire', 98 | }, 99 | }, 100 | modal: { 101 | options: { 102 | delete: 'Supprimer', 103 | }, 104 | }, 105 | }, 106 | }; 107 | 108 | const currentLanguage = languagesArray[1]; 109 | export default locales[currentLanguage]; 110 | -------------------------------------------------------------------------------- /src/components/playlists/PlaylistVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | _goToVideo, 4 | _navigateToMiniPlayerVideo, 5 | _playOnMiniPlayer, 6 | } from '../../helpers/navigation'; 7 | import locales from '../../locales/playlist'; 8 | import PlaylistCard from './PlaylistCard'; 9 | 10 | const PlaylistVideos = (props) => { 11 | const { 12 | updateLoadingState, 13 | updateCurrentVideoData, 14 | mutateRelatedVideos, 15 | updateVideoThumb, 16 | miniPlayerInfo, 17 | history, 18 | currentVideo, 19 | videos, 20 | search, 21 | getVideos, 22 | user, 23 | addToastNotification, 24 | getLikedVideos, 25 | } = props; 26 | const GoToVideo = async (video) => { 27 | let video_object = video; 28 | video_object['uri'] = video.video_id; 29 | await _goToVideo( 30 | video_object, 31 | updateLoadingState, 32 | updateCurrentVideoData, 33 | mutateRelatedVideos, 34 | history 35 | ); 36 | }; 37 | 38 | const PlayOnMiniaturePlayer = async (video) => { 39 | let video_object = video; 40 | video_object['uri'] = video.video_id; 41 | await _playOnMiniPlayer( 42 | video_object, 43 | updateLoadingState, 44 | updateCurrentVideoData, 45 | mutateRelatedVideos, 46 | updateVideoThumb 47 | ); 48 | }; 49 | 50 | const GoToMiniaturePlayerVideo = async (video) => { 51 | let video_object = video; 52 | video_object['uri'] = video.video_id; 53 | await _navigateToMiniPlayerVideo( 54 | video_object, 55 | updateLoadingState, 56 | updateCurrentVideoData, 57 | mutateRelatedVideos, 58 | history 59 | ); 60 | }; 61 | 62 | const isMiniPlayerActive = miniPlayerInfo 63 | ? miniPlayerInfo.thumbnail 64 | : false; 65 | 66 | if (videos.length === 0) { 67 | return {locales.noVideos}; 68 | } 69 | 70 | return ( 71 | <> 72 | {videos.map((video, index) => { 73 | return ( 74 | 91 | ); 92 | })} 93 | 94 | ); 95 | }; 96 | 97 | export default PlaylistVideos; 98 | -------------------------------------------------------------------------------- /src/components/searchresults/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | updateLoadingState, 5 | updateSearchResults, 6 | } from '../../actions/reduxActions'; 7 | import { LOADING_STATES, searchVideos } from '../../helpers/helpers'; 8 | import './search.scss'; 9 | import VideoResults from './VideoResults'; 10 | 11 | const { LOADING, LOADING_COMPLETE } = LOADING_STATES; 12 | 13 | class SearchResults extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = {}; 17 | } 18 | 19 | async componentDidMount() { 20 | const { 21 | updateLoadingState, 22 | updateSearchResults, 23 | search_results, 24 | } = this.props; 25 | if (search_results.length > 0) return; 26 | updateLoadingState(LOADING); 27 | document.body.style = 'overflow:auto'; 28 | const search_query = this.props.location.search.substring(14); 29 | const title_search_query = search_query.split('+').join(' '); 30 | document.title = title_search_query + ' - CloneTube'; 31 | const response = await searchVideos(search_query); 32 | updateSearchResults(response.data); 33 | updateLoadingState(LOADING_COMPLETE); 34 | } 35 | 36 | render() { 37 | const { darkTheme, history } = this.props; 38 | const searchcontainerClass = darkTheme 39 | ? 'search-container' 40 | : 'search-container search-container--light'; 41 | return ( 42 |
43 |
44 | {this.props.search_results.length > 0 45 | ? this.props.search_results.map((video, i) => { 46 | const { uri, current_length, length } = video; 47 | const pathname = current_length 48 | ? current_length === length 49 | ? `/watch?v=${uri}` 50 | : `/watch?v=${uri}&t=${current_length}s` 51 | : `/watch?v=${uri}`; 52 | return ( 53 | 59 | ); 60 | }) 61 | : null} 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | const mapStateToProps = (state) => { 69 | return { 70 | darkTheme: state.darkTheme, 71 | search_results: state.search_results, 72 | }; 73 | }; 74 | 75 | const mapDispatchToProps = (dispatch) => { 76 | return { 77 | updateSearchResults: (payload) => 78 | dispatch(updateSearchResults(payload)), 79 | updateLoadingState: (payload) => dispatch(updateLoadingState(payload)), 80 | }; 81 | }; 82 | 83 | export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); 84 | -------------------------------------------------------------------------------- /src/components/subscriptions/subscriptions.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/sass/mixins"; 2 | 3 | .feed-channel-container { 4 | display: flex; 5 | justify-content: flex-start; 6 | align-items: flex-start; 7 | width: 100%; 8 | min-height: 100vh; 9 | height: auto; 10 | padding-left: 28%; 11 | padding-top: 12vh; 12 | background-color: rgba(0, 0, 0, 0.92); 13 | @include responsivePortraitSize { 14 | padding: 0; 15 | padding-top: 8.5vh; 16 | } 17 | @include responsiveTabletLandscapeSize { 18 | padding-left: 4%; 19 | } 20 | .channels-container { 21 | width: 100%; 22 | height: 100%; 23 | padding-right: 40px; 24 | .channel-item-container { 25 | margin-bottom: 20px; 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | .channel-data-container { 30 | display: flex; 31 | justify-content: flex-start; 32 | align-items: flex-start; 33 | @include responsivePortraitSize { 34 | padding-left: 24px; 35 | align-items: center; 36 | } 37 | @include responsiveTabletLandscapeSize { 38 | align-items: center; 39 | } 40 | .channel-image { 41 | width: 136px; 42 | height: 136px; 43 | border-radius: 136px; 44 | @include responsivePortraitSize { 45 | width: 48px; 46 | height: 48px; 47 | } 48 | } 49 | .channel-name { 50 | text-align: left; 51 | margin-top: 36px; 52 | font-size: 1rem; 53 | font-weight: 500; 54 | margin-left: 120px; 55 | @include responsivePortraitSize { 56 | margin: 0; 57 | margin-left: 18px; 58 | } 59 | @include responsiveTabletLandscapeSize { 60 | margin-left: 28px; 61 | margin-top: 0; 62 | } 63 | } 64 | } 65 | .subscribe-button { 66 | padding: 10px 16px; 67 | background-color: rgb(219, 8, 8); 68 | border-radius: 3px; 69 | cursor: pointer; 70 | @include responsivePortraitSize { 71 | display: none; 72 | } 73 | span { 74 | text-transform: uppercase; 75 | font-size: 0.85rem; 76 | font-weight: 500; 77 | opacity: 1; 78 | color: white; 79 | } 80 | &.subscribed { 81 | background-color: rgb(65, 65, 65); 82 | span { 83 | opacity: 0.6; 84 | color: white; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | .feed-channel-container--light-theme { 93 | background-color: #f9f9f9; 94 | .channels-container { 95 | .channel-item-container { 96 | .channel-data-container { 97 | .channel-name { 98 | color: black; 99 | opacity: 1 !important; 100 | } 101 | } 102 | .subscribe-button { 103 | span { 104 | color: white; 105 | } 106 | &.subscribed { 107 | background-color: #ececec; 108 | span { 109 | color: black; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/components/video/VideoModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import locales from '../../locales/video'; 3 | import ToggleButton from '../shared/ToggleButton'; 4 | import './css/videomodal.scss'; 5 | import ModalItemTitle from './ModalItemTitle'; 6 | import PlaybackSpeedContainer from './PlaybackSpeedContainer'; 7 | import VideoModalContainer from './VideoModalContainer'; 8 | import VideoModalItem from './VideoModalItem'; 9 | 10 | const VideoModal = (props) => { 11 | const { isCogActive, rate, slowMotion } = props; 12 | const videoModal = useRef(null); 13 | const [currentModal, setCurrentModal] = useState('main'); 14 | useEffect(() => { 15 | document.addEventListener('click', handleClickOutside, true); 16 | return () => { 17 | document.removeEventListener('click', handleClickOutside, true); 18 | }; 19 | }, []); 20 | 21 | const handleClickOutside = (event) => { 22 | if (videoModal.current && !videoModal.current.contains(event.target)) { 23 | setCurrentModal('main'); 24 | if ( 25 | props.controlModal && 26 | !event.target.classList.contains('fa-cog') 27 | ) { 28 | setCurrentModal('main'); 29 | props.controlModal(false); 30 | } 31 | } 32 | }; 33 | 34 | if (currentModal === 'playback') { 35 | return ( 36 | 40 | setCurrentModal('main')} 44 | onClickSecondaryButton={() => setCurrentModal('main')} 45 | /> 46 | 47 | ); 48 | } 49 | 50 | return ( 51 | 52 | 53 | 54 | {locales.videoPlayer.modal.autoplay} 55 | 56 | 57 | 58 | 59 | {props.watermark && ( 60 | 64 | 65 | {locales.videoPlayer.modal.annotations} 66 | 67 | 68 | 69 | )} 70 | 71 | setCurrentModal('playback')}> 72 | 73 | {locales.videoPlayer.modal.playbackSpeed} 74 | 75 | 76 | {rate === 1 ? locales.videoPlayer.modal.normal : rate} 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default VideoModal; 84 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .loading-bar--container { 2 | width:100%; 3 | height: 3px; 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | z-index: 6000; 8 | } 9 | .loading-bar { 10 | width:100%; 11 | height: 100%; 12 | background-color: grey; 13 | display: flex; 14 | justify-content: flex-start; 15 | align-items: center; 16 | } 17 | .loading-indicator { 18 | height: 100%; 19 | background-color: rgb(230, 23, 23); 20 | transition: 1s linear all; 21 | } 22 | 23 | 24 | 25 | 26 | 27 | /* MINIATURE PLAYER */ 28 | .miniature-player { 29 | width: 29.6%; 30 | height:44%; 31 | position:fixed; 32 | bottom:0px; 33 | right:12px; 34 | background-color: #272727; 35 | z-index: 999999; 36 | /* box-shadow: ; */ 37 | } 38 | .miniature-player-content { 39 | width: 100%; 40 | height:78%; 41 | position: relative; 42 | } 43 | .miniature-player div div span { 44 | cursor: pointer; 45 | opacity: 0; 46 | transition: 0.3s linear all; 47 | } 48 | .miniature-player div:hover div span { 49 | opacity: 1; 50 | } 51 | .miniature-player video { 52 | background-color: black; 53 | } 54 | .miniature-player--light { 55 | background-color: #FFFFFF; 56 | } 57 | .miniature-middle-layer { 58 | position:absolute; 59 | background-color:rgba(0, 0, 0, 0.33); 60 | top:0; 61 | left:0; 62 | width:100%; 63 | height:100%; 64 | display:flex; 65 | justify-content:center; 66 | align-items: center; 67 | opacity:0; 68 | transition: 0.4s linear opacity; 69 | } 70 | .miniature-middle-layer:hover { 71 | opacity: 1; 72 | } 73 | .miniatureplayer-info-container { 74 | cursor:pointer; 75 | padding: 8px 15px; 76 | display:flex; 77 | height:22%; 78 | flex-direction:column; 79 | justify-content:space-around; 80 | } 81 | .miniatureplayer-info-container:hover .video-uploader { 82 | color: white; 83 | } 84 | .miniature-player-btn { 85 | cursor:pointer; 86 | } 87 | .miniature-player-btn div svg { 88 | fill:white; 89 | } 90 | .video-title { 91 | text-overflow: ellipsis; 92 | white-space: nowrap; 93 | font-size:0.9rem; 94 | font-weight: 600; 95 | width: 100%; 96 | overflow: hidden; 97 | color:#FFFFFF; 98 | } 99 | .miniature-player--light .video-title { 100 | color: #222222; 101 | } 102 | .video-uploader { 103 | font-size: 0.75rem; 104 | color:#a6a6a6; 105 | } 106 | .miniature-player--light .video-uploader { 107 | color: #7c7c7c; 108 | } 109 | svg { width:100%;} 110 | 111 | /* VISITORS MODAL */ 112 | .visitors-modal { 113 | position:fixed; 114 | top:0; left:0; 115 | width:100%; 116 | height:100%; 117 | z-index: 7000; 118 | display:flex; 119 | justify-content:center; 120 | align-items:center; 121 | } 122 | .visitors-modal-backdrop { 123 | position: absolute; 124 | top:0; 125 | left:0; 126 | width:100%; 127 | height:100%; 128 | background-color: rgba(0, 0, 0, 0.6); 129 | } 130 | .modal-box { 131 | width:25%; 132 | z-index:7000; 133 | height:30%; 134 | background-color:white; 135 | border-radius: 8px; 136 | display: flex; 137 | flex-direction:column; 138 | justify-content:space-between; 139 | align-items:center; 140 | padding: 30px 20px; 141 | } 142 | .modal-box div { 143 | display: flex; 144 | justify-content: space-around; 145 | align-items: center; 146 | width:50%; 147 | } 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/components/LoadingBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { updateLoadingState } from '../actions/reduxActions'; 4 | import { LOADING_STATES } from '../helpers/helpers'; 5 | 6 | class LoadingBar extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | percentage: 0, 11 | interval: null, 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | const { LOADING_STATE } = this.props; 17 | const { LOADING, LOADING_COMPLETE } = LOADING_STATES; 18 | if (LOADING_STATE === LOADING_COMPLETE) { 19 | clearInterval(this.state.interval); 20 | this.loadingComplete(); 21 | } else if (LOADING_STATE === LOADING) { 22 | clearInterval(this.state.interval); 23 | this.loading(); 24 | } 25 | } 26 | 27 | UNSAFE_componentWillReceiveProps(newProps) { 28 | const { LOADING_STATE } = newProps; 29 | const { LOADING, LOADING_COMPLETE } = LOADING_STATES; 30 | if (LOADING_STATE === LOADING_COMPLETE) { 31 | clearInterval(this.state.interval); 32 | this.loadingComplete(); 33 | } else if (LOADING_STATE === LOADING) { 34 | clearInterval(this.state.interval); 35 | this.loading(); 36 | } 37 | } 38 | 39 | loading = () => { 40 | const full_seconds = 4; 41 | let current_second = 0; 42 | const self = this; 43 | this.setState({ 44 | interval: setInterval(function () { 45 | current_second++; 46 | const percentage = current_second * 19; 47 | self.setState({ percentage }); 48 | if (current_second === full_seconds) { 49 | self.slowLoading(); 50 | } 51 | }, 800), 52 | }); 53 | }; 54 | 55 | slowLoading = () => { 56 | const self = this; 57 | clearInterval(this.state.interval); 58 | this.setState({ 59 | interval: setInterval(function () { 60 | const percentage = self.state.percentage + 4; 61 | self.setState({ percentage }); 62 | if (percentage === 100) clearInterval(self.state.interval); 63 | }, 1000), 64 | }); 65 | }; 66 | 67 | loadingComplete = () => { 68 | const { updateLoadingState } = this.props; 69 | const { NOT_LOADING } = LOADING_STATES; 70 | 71 | this.setState( 72 | { 73 | percentage: 120, 74 | }, 75 | () => { 76 | setTimeout(function () { 77 | updateLoadingState(NOT_LOADING); 78 | }, 1000); 79 | } 80 | ); 81 | }; 82 | 83 | render() { 84 | return ( 85 |
86 |
87 |
91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | const mapStateToProps = (state) => { 98 | return { 99 | LOADING_STATE: state.loading_state, 100 | }; 101 | }; 102 | 103 | const mapDispatchToProps = (dispatch) => { 104 | return { 105 | updateLoadingState: (payload) => dispatch(updateLoadingState(payload)), 106 | }; 107 | }; 108 | 109 | export default connect(mapStateToProps, mapDispatchToProps)(LoadingBar); 110 | -------------------------------------------------------------------------------- /src/locales/subscriptions.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | title: 'Llista de canals', 6 | subscribeButton: { 7 | subscribed: 'Suscrit', 8 | unsubscribed: 'Subscriu-me', 9 | }, 10 | confirmModal: { 11 | confirmButton: 'Cancel·la la subscripció', 12 | cancelButton: 'Cancel·la', 13 | description: (channel_name) => 14 | `Vols cancel·lar la subscripció a ${channel_name} ?`, 15 | }, 16 | toast: { 17 | subscribed: "S'ha afegit la subscripció.", 18 | unsubscribed: "S'ha eliminat la subscripció", 19 | }, 20 | }, 21 | en: { 22 | title: 'Channel list', 23 | subscribeButton: { 24 | subscribed: 'Subscribed', 25 | unsubscribed: 'Subscribe', 26 | }, 27 | confirmModal: { 28 | confirmButton: 'Unsubscribe', 29 | cancelButton: 'Cancel', 30 | description: (channel_name) => `Unsubscribe from ${channel_name}?`, 31 | }, 32 | toast: { 33 | subscribed: 'Subscription added', 34 | unsubscribed: 'Subscription removed', 35 | }, 36 | }, 37 | es: { 38 | title: 'Lista de canales', 39 | subscribeButton: { 40 | subscribed: 'Suscrito', 41 | unsubscribed: 'Suscribirse', 42 | }, 43 | confirmModal: { 44 | confirmButton: 'Anular suscripción', 45 | cancelButton: 'Cancelar', 46 | description: (channel_name) => 47 | `¿Deseas anular tu suscripción a ${channel_name}?`, 48 | }, 49 | toast: { 50 | subscribed: 'Suscripción agregada', 51 | unsubscribed: 'Se eliminó la suscripción', 52 | }, 53 | }, 54 | fr: { 55 | title: 'Liste des chaînes', 56 | subscribeButton: { 57 | subscribed: 'Abonné', 58 | unsubscribed: `S'abonner`, 59 | }, 60 | confirmModal: { 61 | confirmButton: 'Se désabonner', 62 | cancelButton: 'Annuler', 63 | description: (channel_name) => `Se désabonner de ${channel_name} ?`, 64 | }, 65 | toast: { 66 | subscribed: 'Abonnement ajouté', 67 | unsubscribed: 'Abonnement supprimé', 68 | }, 69 | }, 70 | de: { 71 | title: 'Kanalliste', 72 | subscribeButton: { 73 | subscribed: 'Abonniert', 74 | unsubscribed: 'Suscribirse', 75 | }, 76 | confirmModal: { 77 | confirmButton: 'Abo beenden', 78 | cancelButton: 'Abbrechen', 79 | description: (channel_name) => `Abo für ${channel_name} beenden?`, 80 | }, 81 | toast: { 82 | subscribed: 'Abo hinzugefügt', 83 | unsubscribed: 'Abo entfernt', 84 | }, 85 | }, 86 | it: { 87 | title: 'Elenco canali', 88 | subscribeButton: { 89 | subscribed: 'Iscritto', 90 | unsubscribed: 'Iscriviti', 91 | }, 92 | confirmModal: { 93 | confirmButton: 'Anulla iscrizione', 94 | cancelButton: 'Anulla', 95 | description: (channel_name) => 96 | `Vuoi annullare l'iscrizione a ${channel_name}?`, 97 | }, 98 | toast: { 99 | subscribed: 'Iscrizione aggiunta', 100 | unsubscribed: 'Iscrizione rimossa', 101 | }, 102 | }, 103 | }; 104 | 105 | const currentLanguage = languagesArray[1]; 106 | 107 | export default locales[currentLanguage]; 108 | -------------------------------------------------------------------------------- /src/locales/library.js: -------------------------------------------------------------------------------- 1 | import languagesArray from '../helpers/languages'; 2 | 3 | const locales = { 4 | ca: { 5 | title: 'Biblioteca', 6 | headers: { 7 | history: 'Historial', 8 | watchLater: 'Visualitza més tard', 9 | playlists: 'Llistes de reproducció', 10 | likedVideos: "Vídeos que m'agraden", 11 | buttons: { 12 | seeAll: 'Mostra-ho tot', 13 | }, 14 | }, 15 | user: { 16 | data: { 17 | subscriptions: 'Subscripcions', 18 | uploads: 'Pujades', 19 | likes: "M'agrada", 20 | }, 21 | }, 22 | }, 23 | en: { 24 | title: 'Library', 25 | headers: { 26 | history: 'History', 27 | watchLater: 'Watch later', 28 | playlists: 'Playlists', 29 | likedVideos: 'Liked videos', 30 | buttons: { 31 | seeAll: 'See all', 32 | }, 33 | }, 34 | user: { 35 | data: { 36 | subscriptions: 'Subscriptions', 37 | uploads: 'Uploads', 38 | likes: 'Likes', 39 | }, 40 | }, 41 | }, 42 | de: { 43 | title: 'Mediathek', 44 | headers: { 45 | history: 'Verlauf', 46 | watchLater: 'Später ansehen', 47 | playlists: 'Playlists', 48 | likedVideos: 'Videos, die ich mag', 49 | buttons: { 50 | seeAll: 'Alle', 51 | }, 52 | }, 53 | user: { 54 | data: { 55 | subscriptions: 'Abos', 56 | uploads: 'Uploads', 57 | likes: '"Mag ich"-Bewertungen', 58 | }, 59 | }, 60 | }, 61 | fr: { 62 | title: 'Bibliothèque', 63 | headers: { 64 | history: 'Historique', 65 | watchLater: 'À regarder plus tard', 66 | playlists: 'Playlists', 67 | likedVideos: `Vidéos "J'aime"`, 68 | buttons: { 69 | seeAll: 'Tout voir', 70 | }, 71 | }, 72 | user: { 73 | data: { 74 | subscriptions: 'Abonnements', 75 | uploads: 'Vidéos mises en ligne', 76 | likes: "J'aime", 77 | }, 78 | }, 79 | }, 80 | es: { 81 | title: 'Biblioteca', 82 | headers: { 83 | history: 'Historial', 84 | watchLater: 'Ver más tarde', 85 | playlists: 'Listas de reproducción', 86 | likedVideos: 'Videos que me gustan', 87 | buttons: { 88 | seeAll: 'Ver todo', 89 | }, 90 | }, 91 | user: { 92 | data: { 93 | subscriptions: 'Suscripciones', 94 | uploads: 'Videos subidos', 95 | likes: 'Me gusta', 96 | }, 97 | }, 98 | }, 99 | it: { 100 | title: 'Raccolta', 101 | headers: { 102 | history: 'Cronologia', 103 | watchLater: 'Guarda più tardi', 104 | playlists: 'Playlist', 105 | likedVideos: 'Video piaciuti', 106 | buttons: { 107 | seeAll: 'Vedi tutto', 108 | }, 109 | }, 110 | user: { 111 | data: { 112 | subscriptions: 'Iscrizioni', 113 | uploads: 'Video caricati', 114 | likes: 'Mi piace', 115 | }, 116 | }, 117 | }, 118 | }; 119 | 120 | const currentLanguage = languagesArray[1]; 121 | 122 | export default locales[currentLanguage]; 123 | -------------------------------------------------------------------------------- /src/components/video/css/videomodal.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/sass/mixins'; 2 | .video-options-modal { 3 | position:absolute; 4 | top:-10px; 5 | right:1.5%; 6 | min-width: 250px; 7 | height: auto; 8 | max-height: 51vh; 9 | overflow-y: auto; 10 | width:auto; 11 | background-color: #212121ee; 12 | border-radius: 3px; 13 | transform: translateY(-100%); 14 | padding: 8px 0px; 15 | cursor: pointer; 16 | transition: 1s linear all; 17 | animation-name: modalanimation; 18 | animation-duration: 0.15s; 19 | animation-fill-mode: forwards; 20 | animation-timing-function: ease-in-out; 21 | z-index: 99999999999999; 22 | &.video-options-modal--unactive { 23 | transition: 1s linear all; 24 | animation-name: modalanimationreverse; 25 | animation-duration: 0.15s; 26 | animation-fill-mode: forwards; 27 | animation-timing-function: ease-in-out; 28 | } 29 | .video-modal-item-header { 30 | width: 100%; 31 | height: 50px; 32 | padding: 0px 20px; 33 | display: flex; 34 | justify-content: space-between; 35 | align-items: center; 36 | font-size: 0.8rem; 37 | border-bottom: 1pt solid rgba(255, 255, 255, 0.2); 38 | &.single-item { 39 | justify-content: flex-start; 40 | } 41 | .modal-item-header-button { 42 | font-weight: 500; 43 | } 44 | .modal-item-header-secondary-button { 45 | font-weight: 300; 46 | text-decoration: underline; 47 | } 48 | } 49 | .video-options-modal-item { 50 | width: 100%; 51 | height: 40px !important; 52 | display: flex; 53 | justify-content: space-between; 54 | align-items: center; 55 | padding: 0px 20px; 56 | &:hover { 57 | background-color: rgba(128, 128, 128, 0.25); 58 | } 59 | &.single-item { 60 | justify-content: flex-start; 61 | } 62 | .video-modal-item-title { 63 | font-size: 0.8rem; 64 | font-weight: 500; 65 | } 66 | } 67 | } 68 | 69 | .watermark { 70 | position: absolute; 71 | right: 0; 72 | width: 43px; 73 | height: 43px; 74 | margin-right: 14px; 75 | opacity: 0.6; 76 | overflow: hidden; 77 | z-index: 4; 78 | transition: 0.2s ease-in-out all; 79 | @include responsivePhoneSize { 80 | display: none; 81 | } 82 | &:hover { 83 | opacity: 1; 84 | cursor: pointer; 85 | } 86 | img { 87 | width: 100%; 88 | height: 100%; 89 | object-fit: contain; 90 | transition: 0.15s linear all; 91 | } 92 | &.watermark-controls-unactive { 93 | bottom: 20px; 94 | } 95 | &.watermark-controls-active { 96 | bottom: 56px; 97 | } 98 | &.watermark-hide { 99 | animation-name: hidewatermark; 100 | animation-duration: 0.1s; 101 | animation-delay: 0.15s; 102 | animation-fill-mode: forwards; 103 | img { 104 | transform: translateX(100%); 105 | } 106 | } 107 | } 108 | 109 | @keyframes hidewatermark { 110 | from { 111 | width:54px; 112 | height: 54px; 113 | } 114 | to { 115 | width: 0px; 116 | height: 0px; 117 | } 118 | } 119 | 120 | @keyframes modalanimation { 121 | from { 122 | opacity: 0; 123 | visibility: hidden; 124 | } 125 | to { 126 | opacity: 1; 127 | visibility: visible; 128 | } 129 | } 130 | 131 | @keyframes modalanimationreverse { 132 | from { 133 | opacity: 1; 134 | visibility: visible; 135 | } 136 | to { 137 | opacity: 0; 138 | visibility: hidden; 139 | } 140 | } -------------------------------------------------------------------------------- /src/components/playlists/PlaylistCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import AddToPlaylistModal from '../shared/AddToPlaylistModal'; 3 | import Overlay from '../shared/Overlay'; 4 | import OptionsButton from './OptionsButton'; 5 | 6 | const PlaylistCard = ({ 7 | video, 8 | index, 9 | isMiniPlayerActive, 10 | currentVideo, 11 | search, 12 | getVideos, 13 | user, 14 | playlistName, 15 | addToastNotification, 16 | GoToMiniaturePlayerVideo, 17 | PlayOnMiniaturePlayer, 18 | GoToVideo, 19 | playlistData, 20 | getLikedVideos, 21 | }) => { 22 | const [playlistModal, setPlaylistModal] = useState(false); 23 | const { video_id, current_length, length } = video; 24 | const pathname = current_length 25 | ? current_length === length 26 | ? `/watch?v=${video_id}` 27 | : `/watch?v=${video_id}&t=${current_length}s` 28 | : `/watch?v=${video_id}`; 29 | return ( 30 |
31 | {index + 1} 32 | { 36 | e.preventDefault(); 37 | isMiniPlayerActive 38 | ? video_id === currentVideo.id 39 | ? GoToMiniaturePlayerVideo(video) 40 | : PlayOnMiniaturePlayer(video) 41 | : GoToVideo(video); 42 | }} 43 | > 44 |
48 | {video.video_duration} 49 | {video.current_length !== undefined && ( 50 |
51 |
59 |
60 | )} 61 | {currentVideo && isMiniPlayerActive 62 | ? video_id === currentVideo.id && 63 | : null} 64 |
65 |
66 | {video.video_title} 67 | {video.video_channel} 68 |
69 |
70 | setPlaylistModal(true)} 77 | changeModalStatus={setPlaylistModal} 78 | addToastNotification={addToastNotification} 79 | playlistData={playlistData} 80 | getLikedVideos={getLikedVideos} 81 | /> 82 | {playlistModal && ( 83 | setPlaylistModal(false)} 86 | uri={video.video_id} 87 | current_video_data={{ 88 | title: video.video_title, 89 | author: video.video_channel, 90 | video_thumbnail: video.thumbnail, 91 | length_seconds: video.video_duration, 92 | }} 93 | /> 94 | )} 95 |
96 | ); 97 | }; 98 | 99 | export default PlaylistCard; 100 | -------------------------------------------------------------------------------- /src/helpers/navigation.js: -------------------------------------------------------------------------------- 1 | import { LOADING_STATES, _getVideoSource } from './helpers'; 2 | const { LOADING_COMPLETE, LOADING } = LOADING_STATES; 3 | 4 | export const _navigateToMiniPlayerVideo = async ( 5 | video, 6 | updateLoadingState, 7 | updateCurrentVideoData, 8 | mutateRelatedVideos, 9 | history 10 | ) => { 11 | let { uri } = video; 12 | uri = uri.length > 11 ? uri.substr(0, 11) : uri; 13 | updateLoadingState(LOADING); 14 | const thumbvideo = document.getElementById('thumb-video'); 15 | const { src, currentTime } = thumbvideo; 16 | thumbvideo.src = ''; 17 | updateLoadingState(LOADING_COMPLETE); 18 | history.push({ 19 | pathname: `/watch`, 20 | search: `?v=${uri}`, 21 | state: { videoSource: src, currentTime, comingFromMiniature: true }, 22 | }); 23 | }; 24 | 25 | export const _playOnMiniPlayer = async ( 26 | video, 27 | updateLoadingState, 28 | updateCurrentVideoData, 29 | mutateRelatedVideos, 30 | updateVideoThumb 31 | ) => { 32 | let { uri, length } = video; 33 | uri = uri.length > 11 ? uri.substr(0, 11) : uri; 34 | updateLoadingState(LOADING); 35 | let response = await _getVideoSource(uri); 36 | const { data } = response; 37 | if (data.isError) { 38 | updateCurrentVideoData(data); 39 | return; 40 | } else { 41 | if (!data.formats) { 42 | updateCurrentVideoData({ isError: true }); 43 | return; 44 | } 45 | 46 | updateCurrentVideoData(data.current_video_data); 47 | mutateRelatedVideos(data.related_videos); 48 | const { formats } = response.data; 49 | updateVideoThumb({ 50 | uri, 51 | currentTime: length ? parseInt(length) : 0, 52 | videoURL: formats[1].url, 53 | thumbnail: true, 54 | }); 55 | const thumbVideo = document.getElementById('thumb-video'); 56 | thumbVideo.src = response.data.formats[1].url; 57 | thumbVideo.currentTime = video.current_length 58 | ? video.current_length === video.length 59 | ? 0 60 | : video.current_length 61 | : 0; 62 | thumbVideo.onloadedmetadata = undefined; 63 | thumbVideo.oncanplay = undefined; 64 | thumbVideo.play(); 65 | updateLoadingState(LOADING_COMPLETE); 66 | } 67 | }; 68 | 69 | export const _goToVideo = async ( 70 | video, 71 | updateLoadingState, 72 | updateCurrentVideoData, 73 | mutateRelatedVideos, 74 | history 75 | ) => { 76 | let { uri } = video; 77 | uri = uri.length > 11 ? uri.substr(0, 11) : uri; 78 | updateLoadingState(LOADING); 79 | let response = await _getVideoSource(uri); 80 | const { data } = response; 81 | if (data.isError) { 82 | updateCurrentVideoData(data); 83 | mutateRelatedVideos([]); 84 | const thumbvideo = document.getElementById('thumb-video'); 85 | if (thumbvideo) thumbvideo.src = ''; 86 | const currentTime = video.current_length ? video.current_length : 0; 87 | updateLoadingState(LOADING_COMPLETE); 88 | history.push({ 89 | pathname: `/watch`, 90 | search: video.current_length 91 | ? video.current_length === video.length 92 | ? `?v=${uri}` 93 | : `?v=${uri}&t=${video.current_length}s` 94 | : `?v=${uri}`, 95 | state: { currentTime }, 96 | }); 97 | return; 98 | } else { 99 | updateCurrentVideoData(data.current_video_data); 100 | mutateRelatedVideos(data.related_videos); 101 | const thumbvideo = document.getElementById('thumb-video'); 102 | if (thumbvideo) thumbvideo.src = ''; 103 | const currentTime = video.current_length ? video.current_length : 0; 104 | updateLoadingState(LOADING_COMPLETE); 105 | history.push({ 106 | pathname: `/watch`, 107 | search: video.current_length 108 | ? video.current_length === video.length 109 | ? `?v=${uri}` 110 | : `?v=${uri}&t=${video.current_length}s` 111 | : `?v=${uri}`, 112 | state: { currentTime }, 113 | }); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/shared/SmallModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import language from '../../helpers/languages'; 4 | 5 | const smallModalTranslateStyleRelatedVideos = { 6 | ca: { transform: 'translate(-95%, 20px)' }, 7 | de: { transform: 'translate(-92%, 20px)' }, 8 | en: { transform: 'translate(-80%, 20px)' }, 9 | es: { transform: 'translate(-98%, 20px)' }, 10 | fr: { transform: 'translate(-108%, 20px)' }, 11 | it: { transform: 'translate(-85%, 20px)' }, 12 | }; 13 | 14 | const smallModalTranslateStylePlaylist = { 15 | ca: { transform: 'translate(-100%, 28px)' }, 16 | de: { transform: 'translate(-100%, 28px)' }, 17 | en: { transform: 'translate(-99%, 28px)' }, 18 | es: { transform: 'translate(-100%, 28px)' }, 19 | fr: { transform: 'translate(-100%, 28px)' }, 20 | it: { transform: 'translate(-99%, 28px)' }, 21 | }; 22 | 23 | class SmallModal extends Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = {}; 27 | this.component = createRef(); 28 | this.currentThreePointsOptions = null; 29 | } 30 | 31 | componentDidMount() { 32 | document.addEventListener('click', this.handleClickOutside, true); 33 | const { overlaping = true } = this.props; 34 | if (!overlaping) return; 35 | const optionButtons = document.getElementsByClassName('btn-container'); 36 | let i = this.props.is_modal_up ? optionButtons.length : 0; 37 | for (i; i < optionButtons.length; i++) { 38 | const currentOptionIteration = optionButtons[i]; 39 | if ( 40 | currentOptionIteration.childNodes.length > 1 && 41 | i < optionButtons.length - 1 42 | ) { 43 | this.currentThreePointsOptions = optionButtons[i].children[1]; 44 | this.currentThreePointsOptions.style.opacity = 1; 45 | currentOptionIteration.style.opacity = 1; 46 | optionButtons[i + 1].style.display = 'none'; 47 | } 48 | } 49 | } 50 | 51 | componentWillUnmount() { 52 | document.removeEventListener('click', this.handleClickOutside, true); 53 | const { overlaping = true } = this.props; 54 | if (!overlaping) return; 55 | if (this.currentThreePointsOptions) 56 | this.currentThreePointsOptions.style.opacity = ''; 57 | const optionButtons = document.getElementsByClassName('btn-container'); 58 | let i = this.props.is_modal_up ? optionButtons.length : 0; 59 | for (i; i < optionButtons.length; i++) { 60 | const currentOptionIteration = optionButtons[i]; 61 | currentOptionIteration.style.display = 'flex'; 62 | } 63 | } 64 | 65 | handleClickOutside = (event) => { 66 | if ( 67 | this.component.current && 68 | !this.component.current.contains(event.target) 69 | ) { 70 | if (this.props.closeDropdown) this.props.closeDropdown(); 71 | } 72 | }; 73 | 74 | render() { 75 | const { 76 | is_modal_up, 77 | style = {}, 78 | right = false, 79 | autoWidth = false, 80 | history, 81 | } = this.props; 82 | const { pathname } = history.location; 83 | let className = 'playlist-modal'; 84 | className += is_modal_up ? ' playlist-modal--up-position' : ''; 85 | className += right ? ' playlist-modal--right-position' : ''; 86 | className += autoWidth ? ' playlist-modal--autowidth' : ''; 87 | const containerStyle = 88 | pathname === '/playlist' 89 | ? smallModalTranslateStylePlaylist[language[0]] 90 | : smallModalTranslateStyleRelatedVideos[language[0]]; 91 | return ( 92 |
96 |
101 | {this.props.children} 102 |
103 |
104 | ); 105 | } 106 | } 107 | 108 | export default withRouter(SmallModal); 109 | -------------------------------------------------------------------------------- /src/components/playlists/SearchHistory.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { withRouter } from "react-router-dom"; 4 | import { updateLoadingState } from "../../actions/reduxActions"; 5 | import { CloseButton } from "../../assets/Icons"; 6 | import { 7 | agoFormatting, 8 | deleteTerm, 9 | getSearchHistory, 10 | LOADING_STATES, 11 | } from "../../helpers/helpers"; 12 | import locales from "../../locales/historial"; 13 | import ToolTip from "../shared/ToolTip"; 14 | import "./searchhistory.scss"; 15 | 16 | class SearchHistory extends Component { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | terms: [], 21 | terms_first_call: [], 22 | isModalActive: false, 23 | gotSearchTerms: false, 24 | }; 25 | } 26 | 27 | componentDidMount() { 28 | this.getSearchHistory(); 29 | } 30 | 31 | getSearchHistory = async () => { 32 | const response = await getSearchHistory(); 33 | const { terms } = response.data; 34 | this.props.updateLoadingState(LOADING_STATES.LOADING_COMPLETE); 35 | this.setState({ terms_first_call: terms, terms, gotSearchTerms: true }); 36 | }; 37 | 38 | deleteTerm = async (e, term, term_id) => { 39 | const { user } = this.props; 40 | const { uid } = user; 41 | const response = await deleteTerm(uid, term, term_id); 42 | const { error } = response; 43 | if (!error) { 44 | let { terms } = this.state; 45 | for (let i = 0; i < terms.length; i++) { 46 | let currentTerm = terms[i]; 47 | const currentTermId = currentTerm.id; 48 | if (currentTermId === term_id) { 49 | terms[i] = false; 50 | break; 51 | } 52 | } 53 | this.setState({ terms }); 54 | } 55 | }; 56 | 57 | goToSearch = (e, term) => { 58 | const { target } = e; 59 | const clickableClasses = [ 60 | "search_item-container", 61 | "search_data-container", 62 | "search_term", 63 | "search_date", 64 | ]; 65 | if (!clickableClasses.includes(target.className)) return; 66 | const search_query = term.split(" ").join("+"); 67 | this.props.history.push({ 68 | pathname: `/results`, 69 | search: `?search_query=${search_query}`, 70 | }); 71 | }; 72 | 73 | render() { 74 | const { terms } = this.state; 75 | 76 | if (terms.length === 0) { 77 | return ( 78 | {locales.search.noVideos} 79 | ); 80 | } 81 | 82 | return ( 83 |
84 | {terms.map((term) => { 85 | if (!term) { 86 | return ( 87 |
91 | 92 | {locales.itemRemoved} 93 | 94 |
95 | ); 96 | } 97 | const { search_term, updated_at, id } = term; 98 | let date_difference_formated = agoFormatting(updated_at); 99 | return ( 100 |
this.goToSearch(e, search_term)} 103 | className="search_item-container" 104 | > 105 |
106 | {search_term} 107 | 108 | {date_difference_formated} ago 109 | 110 |
111 |
this.deleteTerm(e, search_term, id)}> 112 | 113 | 114 | 115 |
116 |
117 | ); 118 | })} 119 |
120 | ); 121 | } 122 | } 123 | 124 | const mapStateToProps = (state) => { 125 | return { 126 | user: state.user, 127 | darkTheme: state.darkTheme, 128 | }; 129 | }; 130 | 131 | const mapDispatchToProps = (dispatch) => { 132 | return { 133 | updateLoadingState: (payload) => dispatch(updateLoadingState(payload)), 134 | }; 135 | }; 136 | 137 | export default withRouter( 138 | connect(mapStateToProps, mapDispatchToProps)(SearchHistory) 139 | ); 140 | -------------------------------------------------------------------------------- /src/components/searchresults/VideoResults.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | mutateRelatedVideos, 5 | updateCurrentVideoData, 6 | updateLoadingState, 7 | updateVideoThumb, 8 | } from '../../actions/reduxActions'; 9 | import { 10 | _goToVideo, 11 | _navigateToMiniPlayerVideo, 12 | _playOnMiniPlayer, 13 | } from '../../helpers/navigation'; 14 | import Overlay from '../shared/Overlay'; 15 | import SearchThumbnail from '../shared/SearchThumbnail'; 16 | import Metadata from '../shared/searchthumbnail/Metadata'; 17 | import Thumbnail from '../shared/searchthumbnail/Thumbnail'; 18 | 19 | const VideoResults = (props) => { 20 | const { 21 | updateLoadingState, 22 | updateCurrentVideoData, 23 | mutateRelatedVideos, 24 | updateVideoThumb, 25 | } = props; 26 | 27 | const PlayOnMiniPlayerVideo = async (video) => { 28 | await _playOnMiniPlayer( 29 | video, 30 | updateLoadingState, 31 | updateCurrentVideoData, 32 | mutateRelatedVideos, 33 | updateVideoThumb 34 | ); 35 | }; 36 | 37 | const GoToVideo = async (video) => { 38 | await _goToVideo( 39 | video, 40 | updateLoadingState, 41 | updateCurrentVideoData, 42 | mutateRelatedVideos, 43 | props.history 44 | ); 45 | }; 46 | 47 | const GoToMiniplayerVideo = async (video) => { 48 | await _navigateToMiniPlayerVideo( 49 | video, 50 | updateLoadingState, 51 | updateCurrentVideoData, 52 | mutateRelatedVideos, 53 | props.history 54 | ); 55 | }; 56 | 57 | if (!props.thumbnail || (!props.thumbnail.thumbnail && props.video_data)) { 58 | return ( 59 | 72 | 78 | 79 | 80 | ); 81 | } else { 82 | const id = props.video_data ? props.video_data.id : ''; 83 | 84 | return ( 85 | 102 | 108 | {id === props.video.uri && } 109 | 110 | 111 | 112 | ); 113 | } 114 | }; 115 | 116 | const mapDispatchToProps = (dispatch) => { 117 | return { 118 | updateVideoThumb: (payload) => dispatch(updateVideoThumb(payload)), 119 | updateCurrentVideoData: (payload) => 120 | dispatch(updateCurrentVideoData(payload)), 121 | mutateRelatedVideos: (payload) => 122 | dispatch(mutateRelatedVideos(payload)), 123 | updateLoadingState: (payload) => dispatch(updateLoadingState(payload)), 124 | }; 125 | }; 126 | 127 | const mapStateToProps = (state) => { 128 | return { 129 | thumbnail: state.thumbnailVideoActive, 130 | video_data: state.current_video_data, 131 | }; 132 | }; 133 | 134 | export default connect(mapStateToProps, mapDispatchToProps)(VideoResults); 135 | -------------------------------------------------------------------------------- /src/components/video/LikesWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThumbDown, ThumbUp } from '../../assets/Icons'; 3 | import { commaFormatting, KiloFormatting } from '../../helpers/helpers'; 4 | import locales from '../../locales/video'; 5 | import ToolTip from '../shared/ToolTip'; 6 | 7 | const LikesWrapper = ({ 8 | user, 9 | updateVisitorModal, 10 | voteVideo, 11 | isVideoLiked, 12 | isVideoDisliked, 13 | likes, 14 | dislikes, 15 | }) => { 16 | let likesWidth = 0; 17 | let dislikesWidth = 0; 18 | const intLikes = Number(likes.split(',').join('')); 19 | const intDislikes = Number(dislikes.split(',').join('')); 20 | let newLikes = intLikes + 1; 21 | let newDislikes = intDislikes + 1; 22 | 23 | if (likes !== undefined) { 24 | const percentage_likes = isVideoLiked ? newLikes : intLikes; 25 | const percentage_dislikes = isVideoDisliked ? newDislikes : intDislikes; 26 | const percentage = percentage_likes + percentage_dislikes; 27 | likesWidth = (100 / percentage) * percentage_likes; 28 | dislikesWidth = (100 / percentage) * percentage_dislikes; 29 | } 30 | return ( 31 |
32 | 39 | voteVideo(e)} 41 | className="like-btn" 42 | href="#" 43 | > 44 | 49 | 52 | {isVideoLiked 53 | ? KiloFormatting(newLikes) 54 | : KiloFormatting(likes)} 55 | 56 | 57 | 58 | 59 | voteVideo(e, false) 62 | } 63 | className="like-btn dislike-btn" 64 | href="#" 65 | > 66 | 71 | 76 | {isVideoDisliked 77 | ? KiloFormatting(newDislikes) 78 | : KiloFormatting(dislikes)} 79 | 80 | 81 | 82 | 88 |
89 |
90 |
102 |
109 |
110 |
111 |
112 |
113 | ); 114 | }; 115 | 116 | export default LikesWrapper; 117 | -------------------------------------------------------------------------------- /src/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | const darkTheme = 2 | localStorage.getItem('darkTheme') === null 3 | ? false 4 | : JSON.parse(localStorage.getItem('darkTheme')); 5 | const autoplay = 6 | localStorage.getItem('autoplay') === null 7 | ? false 8 | : JSON.parse(localStorage.getItem('autoplay')); 9 | const theater_mode = 10 | localStorage.getItem('theater_mode') === null 11 | ? false 12 | : JSON.parse(localStorage.getItem('theater_mode')); 13 | // localStorage.setItem('language', '["es", "es"]'); 14 | const language = JSON.parse(localStorage.getItem('language')); 15 | 16 | const initState = { 17 | darkTheme, 18 | relatedVideos: [], 19 | autoplay, 20 | user: null, 21 | sidenav: false, 22 | sidenavHome: true, 23 | videoEnded: false, 24 | homeSearchVideos: [], 25 | buffering: false, 26 | thumbnailVideoActive: false, 27 | current_video_data: null, 28 | loading_state: 0, 29 | visitor_modal_active: false, 30 | download_limit: false, 31 | playlists: [], 32 | toast_notifications: [], 33 | search_results: [], 34 | language, 35 | theater_mode, 36 | video_chapters: [], 37 | }; 38 | 39 | const rootReducer = (state = initState, action) => { 40 | if (action.type === 'TOGGLE_DARKTHEME') { 41 | localStorage.setItem('darkTheme', !state.darkTheme); 42 | return { 43 | ...state, 44 | darkTheme: !state.darkTheme, 45 | }; 46 | } else if (action.type === 'UPDATE_VISITOR_MODAL') { 47 | return { 48 | ...state, 49 | visitor_modal_active: !state.visitor_modal_active, 50 | }; 51 | } else if (action.type === 'UPDATE_LOADING_STATE') { 52 | const download_limit = action.payload ? true : false; 53 | return { 54 | ...state, 55 | loading_state: action.payload, 56 | download_limit, 57 | }; 58 | } else if (action.type === 'UPDATE_CURRENT_VIDEO_DATA') { 59 | return { 60 | ...state, 61 | current_video_data: action.payload, 62 | }; 63 | } else if (action.type === 'GET_RELATEDVIDEOS') { 64 | return { 65 | ...state, 66 | relatedVideos: action.payload, 67 | }; 68 | } else if (action.type === 'TOGGLE_AUTOPLAY') { 69 | localStorage.setItem('autoplay', !state.autoplay); 70 | return { 71 | ...state, 72 | autoplay: !state.autoplay, 73 | }; 74 | } else if (action.type === 'UPDATE_USERDATA') { 75 | return { 76 | ...state, 77 | user: action.payload, 78 | }; 79 | } else if (action.type === 'TOGGLE_SIDENAV-HOME') { 80 | return { 81 | ...state, 82 | sidenavHome: !state.sidenavHome, 83 | }; 84 | } else if (action.type === 'TOGGLE_SIDENAV') { 85 | return { 86 | ...state, 87 | sidenav: action.payload === null ? !state.sidenav : false, 88 | }; 89 | } else if (action.type === 'UPDATE_VIDEOENDED') { 90 | return { 91 | ...state, 92 | videoEnded: action.payload, 93 | }; 94 | } else if (action.type === 'UPDATE_HOMESEARCH') { 95 | return { 96 | ...state, 97 | homeSearchVideos: action.payload, 98 | }; 99 | } else if (action.type === 'UPDATE_THUMBACTIVE') { 100 | return { 101 | ...state, 102 | thumbnailVideoActive: action.payload, 103 | }; 104 | } else if (action.type === 'UPDATE_PLAYLISTS') { 105 | return { 106 | ...state, 107 | playlists: action.payload, 108 | }; 109 | } else if (action.type === 'ADD_TOAST_NOTIFICATION') { 110 | let toast_notifications = state.toast_notifications; 111 | toast_notifications.push(action.payload); 112 | return { 113 | ...state, 114 | toast_notifications: [...toast_notifications], 115 | }; 116 | } else if (action.type === 'REMOVE_TOAST_NOTIFICATION') { 117 | let toast_notifications = state.toast_notifications; 118 | toast_notifications.shift(); 119 | return { 120 | ...state, 121 | toast_notifications: [...toast_notifications], 122 | }; 123 | } else if (action.type === 'UPDATE_SEARCH_RESULTS') { 124 | return { 125 | ...state, 126 | search_results: [...action.payload], 127 | }; 128 | } else if (action.type === 'UPDATE_THEATER_MODE') { 129 | localStorage.setItem('theater_mode', !state.theater_mode); 130 | return { 131 | ...state, 132 | theater_mode: !state.theater_mode, 133 | }; 134 | } 135 | 136 | return state; 137 | }; 138 | 139 | export default rootReducer; 140 | --------------------------------------------------------------------------------