├── _config.yml ├── .env ├── .firebaserc ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── App.css ├── components │ ├── SideBar │ │ ├── SideBar.css │ │ └── SideBar.js │ ├── Video │ │ └── Video.js │ ├── SideBarRow │ │ ├── SideBarRow.js │ │ └── SideBarRow.css │ ├── RecommendedVideos │ │ ├── RecommendedVideos.css │ │ └── RecommendedVideos.js │ ├── SearchPage │ │ ├── SearchPage.css │ │ └── SearchPage.js │ ├── VideoPlayer │ │ ├── VideoPlayer.css │ │ └── VideoPlayer.js │ ├── ChannelRow │ │ ├── ChannelRow.css │ │ └── ChannelRow.js │ ├── VideoRow │ │ ├── VideoRow.css │ │ └── VideoRow.js │ ├── VideoCard │ │ ├── VideoCard.css │ │ └── VideoCard.js │ ├── Header │ │ ├── Header.css │ │ └── Header.js │ └── VideoInfo │ │ ├── VideoInfo.css │ │ └── VideoInfo.js ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── App.js ├── logo.svg └── serviceWorker.js ├── firebase.json ├── .gitignore ├── README.md ├── package.json └── .firebase └── hosting.YnVpbGQ.cache /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-merlot -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_YOUTUBE_API_KEY=YOUR_YOUTUBE_DATA_API_KEY -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "alanbinu-642df" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanBinu007/YouTube-Clone-ReactJs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanBinu007/YouTube-Clone-ReactJs/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanBinu007/YouTube-Clone-ReactJs/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .app__mainpage { 2 | display: flex; 3 | } 4 | 5 | a { 6 | text-decoration: inherit; 7 | color: inherit; 8 | } 9 | 10 | .loading { 11 | display: block !important; 12 | width: 50%; 13 | margin: auto; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/SideBar/SideBar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | flex: 0.2 3 | } 4 | 5 | .sidebar > hr { 6 | height: 1px; 7 | border: 0; 8 | background-color: lightgray; 9 | margin-top: 10px; 10 | margin-bottom: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Video/Video.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import YouTube from 'react-youtube'; 3 | 4 | const Video = ({videoId}) => { 5 | return ( 6 |
7 | 10 |
11 | ) 12 | } 13 | 14 | export default Video; 15 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /.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 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /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 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/components/SideBarRow/SideBarRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './SideBarRow.css'; 3 | 4 | 5 | const SideBarRow = ({selected, Icon, title}) => { 6 | return ( 7 |
8 | 9 |

{title}

10 |
11 | ) 12 | } 13 | 14 | export default SideBarRow; 15 | -------------------------------------------------------------------------------- /src/components/RecommendedVideos/RecommendedVideos.css: -------------------------------------------------------------------------------- 1 | .recommendedvideos { 2 | flex: 0.8; 3 | background-color: #f9f9f9; 4 | padding: 40px 20px; 5 | padding-bottom: 0; 6 | } 7 | 8 | .recommendedVideos > h2 { 9 | margin-left: 5px; 10 | margin-bottom: 20px; 11 | } 12 | 13 | .recommendedvideos__videos { 14 | display: flex; 15 | flex-wrap: wrap; 16 | } 17 | 18 | .recommendedvideos__videos > * { 19 | margin-left: 5%; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/SearchPage/SearchPage.css: -------------------------------------------------------------------------------- 1 | .searchpage { 2 | flex: 0.8; 3 | background-color: #f9f9f9; 4 | padding: 20px 20px; 5 | } 6 | 7 | .searchpage__filter { 8 | display: flex; 9 | align-items: center; 10 | color: #606060; 11 | font-size: xx-small !important; 12 | } 13 | 14 | .searchpage__filter > h2 { 15 | margin-left: 10px; 16 | } 17 | 18 | .searchpage > hr { 19 | height: 1px; 20 | border: 0; 21 | background-color: lightgray; 22 | margin-top: 10px; 23 | margin-bottom: 10px; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/VideoPlayer/VideoPlayer.css: -------------------------------------------------------------------------------- 1 | .videoplayer { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .videoplayer__videodetails { 7 | flex: 0.8; 8 | } 9 | 10 | 11 | .videoplayer__video, .videoplayer__videoinfo { 12 | margin: 5% 5% 5% 10%; 13 | } 14 | 15 | .videoplayer__suggested { 16 | flex: 0.2; 17 | margin-top: 0 !important; 18 | } 19 | 20 | .videoplayer__videoinfo { 21 | flex-direction: row; 22 | } 23 | 24 | .videoinfo__channeldesc { 25 | color: gray; 26 | font-size: small; 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Youtube Clone 2 | 3 | **Project Link** - ***https://alanbinu-642df.web.app/*** 4 | 5 | ## Tech We Used 6 | 7 | - ReactJs 8 | - Firebase Hosting 9 | - Firebase Storage 10 | - React-Dom 11 | - React Redux 12 | 13 | ## Features 14 | 15 | - Sarch Videos 16 | - Play Videos via Youtube API 17 | - Neat and clean UI 18 | 19 | ## Steps to run in your machine 20 | 21 | #### Run the following commands 22 | ``` 23 | npm i 24 | npm run start 25 | ``` 26 | 27 | 28 | 29 | 30 | #### Hope you liked this project, dont forget to ⭐ the repo. 31 | -------------------------------------------------------------------------------- /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 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 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/ChannelRow/ChannelRow.css: -------------------------------------------------------------------------------- 1 | .channelrow { 2 | display: flex; 3 | align-items: center; 4 | width: 70%; 5 | } 6 | 7 | .channelrow__text { 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | .channelrow__text > p { 13 | color: #606060; 14 | font-size: small !important; 15 | } 16 | 17 | .channelrow__logo { 18 | height: 120px !important; 19 | width: 120px !important; 20 | margin: 10px 60px; 21 | } 22 | 23 | .channelrow__text > * { 24 | margin: 2px 2px; 25 | } 26 | 27 | .channelrow__text > h4 { 28 | display: flex; 29 | align-items: center; 30 | justify-content: space-between; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/VideoRow/VideoRow.css: -------------------------------------------------------------------------------- 1 | .videorow { 2 | display: flex; 3 | margin-bottom: 30px; 4 | max-width: 700px; 5 | } 6 | 7 | .videorow > img { 8 | object-fit: contain; 9 | width: 246px; 10 | height: 138px; 11 | } 12 | 13 | .videorow__text > * { 14 | margin: 0px; 15 | } 16 | 17 | .videorow__text { 18 | margin-left: 14px 19 | } 20 | 21 | .videorow__text > h3 { 22 | margin-top: 8px; 23 | } 24 | 25 | .videorow__description { 26 | margin-top: 10px; 27 | font-size: 12px; 28 | color: #606060 29 | } 30 | 31 | .videorow__headline { 32 | margin-top: 6px; 33 | font-size: 12px; 34 | color: #606060; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/VideoRow/VideoRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './VideoRow.css'; 3 | 4 | const VideoRow = ({views, description, timestamp, channel, title, image}) => { 5 | return ( 6 |
7 | 8 |
9 |

{title}

10 |

11 | {channel} • {views} views • {timestamp} 12 |

13 |

14 | {description} 15 |

16 |
17 |
18 | ) 19 | } 20 | 21 | export default VideoRow; 22 | -------------------------------------------------------------------------------- /src/components/VideoCard/VideoCard.css: -------------------------------------------------------------------------------- 1 | .videocard { 2 | width: 270px; 3 | margin-bottom: 40px; 4 | } 5 | 6 | .videocard__image { 7 | height: 140px; 8 | width: 250px; 9 | } 10 | 11 | .videocard__info { 12 | display: flex; 13 | margin-top: 10px; 14 | padding-right: 30px; 15 | } 16 | 17 | .videocard__text { 18 | margin-left: 15px; 19 | } 20 | 21 | .videocard__text > h4 { 22 | font-size: 14px; 23 | margin-bottom: 5px; 24 | } 25 | 26 | .videocard__text > p { 27 | font-size: 14px; 28 | color: gray; 29 | } 30 | 31 | .videocard__avatar { 32 | height: 30px !important; 33 | width: 30px !important; 34 | } 35 | 36 | 37 | 38 | .videocard__text > * { 39 | margin: 1px; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ChannelRow/ChannelRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './ChannelRow.css'; 3 | import Avatar from '@material-ui/core/Avatar'; 4 | 5 | const ChannelRow = ({image, channel, subs, noOfVideos, description}) => { 6 | return ( 7 |
8 | 13 |
14 |

{channel}

15 |

{subs} subscribers • {noOfVideos} videos

16 |

{description}

17 |
18 |
19 | ) 20 | } 21 | 22 | export default ChannelRow; 23 | -------------------------------------------------------------------------------- /src/components/VideoCard/VideoCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import './VideoCard.css'; 4 | 5 | const VideoCard = ({image, title, channel, views, timestamp, channelImage}) => { 6 | return ( 7 |
8 | 9 |
10 | 15 |
16 |

{title}

17 |

{channel}

18 |

{views} views • {timestamp}

19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | export default VideoCard; 26 | -------------------------------------------------------------------------------- /src/components/SideBarRow/SideBarRow.css: -------------------------------------------------------------------------------- 1 | .sidebarrow { 2 | display: flex; 3 | align-items: center; 4 | padding: 10px 20px; 5 | } 6 | 7 | .sidebarrow__icon { 8 | color: #606060; 9 | font-size: large !important; 10 | } 11 | 12 | .sidebarrow__title { 13 | flex: 1; 14 | margin-left: 20px; 15 | font-size: 12px; 16 | font-weight: 500; 17 | } 18 | 19 | .sidebarrow:hover { 20 | background-color: lightgray; 21 | cursor: pointer 22 | } 23 | 24 | .sidebarrow:hover >.sidebarrow__icon { 25 | color: red 26 | } 27 | 28 | .sidebarrow:hover >.sidebarrow__title { 29 | font-weight: bold; 30 | } 31 | 32 | .sidebarrow.selected { 33 | background-color: lightgray; 34 | } 35 | 36 | .sidebarrow.selected >.sidebarrow__icon { 37 | color: red 38 | } 39 | 40 | .sidebarrow.selected >.sidebarrow__title { 41 | font-weight: bold; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 10px; 6 | position: sticky; 7 | top: 0; 8 | z-index: 100; 9 | background-color: white; 10 | } 11 | 12 | .header__logo { 13 | height: 25px; 14 | object-fit: contain; 15 | margin-left: 10px; 16 | } 17 | 18 | .header__center { 19 | display: flex; 20 | align-items: center; 21 | width: 40%; 22 | border: 1px solid lightgray 23 | } 24 | 25 | .header__searchbutton { 26 | width: 50px !important; 27 | border-color: #fafafa; 28 | border-left: 1px solid lightgray; 29 | color: gray; 30 | cursor: pointer; 31 | } 32 | 33 | .header__right { 34 | display: flex; 35 | align-items: center; 36 | } 37 | 38 | .header__icon { 39 | margin-right: 12px; 40 | } 41 | 42 | .header__center > input { 43 | flex: 1; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/VideoInfo/VideoInfo.css: -------------------------------------------------------------------------------- 1 | .videoinfo__stats, .videoinfo__channel { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | } 6 | 7 | .videoinfo__headline > h1 { 8 | font-weight: 400; 9 | font-size: large; 10 | } 11 | 12 | .videoinfo__stats > p { 13 | font-size: medium; 14 | color: gray; 15 | } 16 | 17 | .videoinfo__stats > * { 18 | margin-top: 2px; 19 | } 20 | 21 | .videoinfo__likes > * { 22 | padding-left: 0; 23 | padding-top: 0; 24 | } 25 | 26 | .videoinfo__likes, .videoinfo__channel > div { 27 | display: flex; 28 | align-items: center; 29 | } 30 | 31 | hr { 32 | height: 1px; 33 | border: 0; 34 | background-color: lightgray; 35 | margin-top: 10px; 36 | margin-bottom: 10px; 37 | } 38 | 39 | .videoinfo__channelinfo { 40 | padding-left: 5px; 41 | } 42 | 43 | .videoinfo__channeltitle { 44 | font-size: medium; 45 | margin: 2px; 46 | } 47 | 48 | .videoinfo__channelsubs { 49 | color: gray; 50 | font-size: small; 51 | margin: 0; 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/lab": "^4.0.0-alpha.57", 9 | "@testing-library/jest-dom": "^5.12.0", 10 | "@testing-library/react": "^11.2.6", 11 | "@testing-library/user-event": "^12.8.3", 12 | "axios": "^0.21.1", 13 | "luxon": "^1.26.0", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-router": "^5.2.0", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "4.0.3", 19 | "react-youtube": "^7.13.1", 20 | "web-vitals": "^1.1.1" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import Header from './components/Header/Header'; 4 | import SideBar from './components/SideBar/SideBar'; 5 | import RecommendedVideos from './components/RecommendedVideos/RecommendedVideos'; 6 | import SearchPage from './components/SearchPage/SearchPage'; 7 | import VideoPlayer from './components/VideoPlayer/VideoPlayer'; 8 | import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'; 9 | 10 | function App() { 11 | 12 | return ( 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 |
28 | 29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/components/SideBar/SideBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SideBarRow from './../SideBarRow/SideBarRow'; 3 | import './SideBar.css'; 4 | import HomeIcon from '@material-ui/icons/Home'; 5 | import WhatshotIcon from '@material-ui/icons/Whatshot'; 6 | import SubscriptionsIcon from '@material-ui/icons/Subscriptions'; 7 | import VideoLibraryIcon from '@material-ui/icons/VideoLibrary'; 8 | import HistoryIcon from '@material-ui/icons/History'; 9 | import OndemandVideoIcon from '@material-ui/icons/OndemandVideo'; 10 | import WatchLaterIcon from '@material-ui/icons/WatchLater'; 11 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'; 12 | 13 | 14 | const SideBar = () => { 15 | return ( 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ) 29 | } 30 | 31 | export default SideBar; 32 | -------------------------------------------------------------------------------- /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1619409866491,0c6d934e8a45bfe6fc73ce10fc7c32f6ee6c17607ef4f62657e47e811238ef46 2 | manifest.json,1619272675692,5c997de1364b8be939319fa9209abd77f2caf7f8844999a9e2e9173f844e7840 3 | index.html,1619409866491,1b279e83ba7a19267227e0153fc92e052b3729c12cde0fb56d10bae9039325d2 4 | robots.txt,1619272675693,b2090cf9761ef60aa06e4fab97679bd43dfa5e5df073701ead5879d7c68f1ec5 5 | static/css/main.a79c03d9.chunk.css,1619409866495,c905ca198c64997edeadfd8f32c1413816d4019ca7d8b364c771e1b088824663 6 | static/css/main.a79c03d9.chunk.css.map,1619409866511,6440ed184f56b5d44448815acefde7aacbac6787a310f9313408b3aadb561050 7 | static/js/2.d9ea9150.chunk.js.LICENSE.txt,1619409866511,bb31c8e2ca219e22de0def9acd9fe2dd1aa3cd9ee1a8562ccf463667c7498606 8 | static/js/main.1a55034c.chunk.js,1619409866499,43a7804668c2ad21284020e2b9c1865e8854a08638027edd8ef60a99e49876ab 9 | static/js/runtime-main.1ed77be7.js,1619409866511,d06ba7f93a56760a2b1ed2b664711b23a71aceda1bcd8941df84052ce88941a7 10 | static/js/runtime-main.1ed77be7.js.map,1619409866511,962de7f374834221dd6aaba5c0125c0068aa738cbb3323d5aea176533608e8bd 11 | static/js/main.1a55034c.chunk.js.map,1619409866511,3bfee13a8d50d04e714a878233ec5c185d3bb434b81a54c3f7673065bb400d4e 12 | favicon.ico,1619272675690,d96ddbc4933b04e12c738ab39f469573143949ca2c39eda0a49d16f83d40c319 13 | logo192.png,1619272675691,3ee59515172ee198f3be375979df15ac5345183e656720a381b8872b2a39dc8b 14 | logo512.png,1619272675692,ee7e2f3fdb8209c4b6fd7bef6ba50d1b9dba30a25bb5c3126df057e1cb6f5331 15 | static/js/2.d9ea9150.chunk.js,1619409866511,6874757bb00d8750a6ba7dc97a01d6c6d64089fa0e2e3437d7a3684083ee4625 16 | static/js/2.d9ea9150.chunk.js.map,1619409866511,6ba6430fcdeb540192430024ed94e1a00096bbefe6ef1492477dae3414632fe8 17 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './Header.css'; 4 | import MenuIcon from '@material-ui/icons/Menu'; 5 | import SearchIcon from '@material-ui/icons/Search'; 6 | import VideoCallIcon from '@material-ui/icons/VideoCall'; 7 | import AppsIcon from '@material-ui/icons/Apps'; 8 | import NotificationsIcon from '@material-ui/icons/Notifications'; 9 | import Avatar from '@material-ui/core/Avatar'; 10 | 11 | function Header () { 12 | 13 | const [inputSearch, setInputSearch] = useState(''); 14 | 15 | return ( 16 |
17 |
18 | 19 | 20 | 25 | 26 |
27 | 28 |
29 | setInputSearch(e.target.value)} value={inputSearch}/> 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | 44 |
45 | 46 |
47 | ) 48 | } 49 | 50 | export default Header; 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/VideoInfo/VideoInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ThumbUpIcon from '@material-ui/icons/ThumbUp'; 3 | import ThumbDownIcon from '@material-ui/icons/ThumbDown'; 4 | import SideBarRow from '../SideBarRow/SideBarRow'; 5 | import ReplyIcon from '@material-ui/icons/Reply'; 6 | import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; 7 | import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd'; 8 | import './VideoInfo.css'; 9 | import { Avatar, Button } from '@material-ui/core'; 10 | 11 | const VideoInfo = ({title, description, publishedDate, channelTitle, channelImage, viewCount, likeCount, dislikeCount, subs}) => { 12 | return ( 13 |
14 |
15 |

{title}

16 |
17 |
18 |

{viewCount} views • {publishedDate}

19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | 35 |
36 |

{channelTitle}

37 |

{subs} subscribers

38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |

{description}

47 |
48 |
49 | ) 50 | } 51 | 52 | export default VideoInfo; 53 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RecommendedVideos/RecommendedVideos.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import VideoCard from './../VideoCard/VideoCard'; 3 | import './RecommendedVideos.css'; 4 | import axios from 'axios'; 5 | import {DateTime} from 'luxon'; 6 | import { Link } from 'react-router-dom'; 7 | import CircularProgress from '@material-ui/core/CircularProgress'; 8 | import Alert from '@material-ui/lab/Alert'; 9 | 10 | 11 | const RecommendedVideos = () => { 12 | 13 | const [videoCards, setVideoCards] = useState([]); 14 | const [isLoading, setIsLoading] = useState(true); 15 | const [isError, setIsError] = useState(false); 16 | 17 | useEffect(() => { 18 | axios 19 | .get(`https://www.googleapis.com/youtube/v3/videos?part=snippet%2CcontentDetails%2Cstatistics&chart=mostPopular&maxResults=9®ionCode=PK&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 20 | .then(response => { 21 | createVideoCards(response.data.items); 22 | }) 23 | .catch(error => { 24 | console.log(error); 25 | setIsError(true); 26 | }) 27 | }, []) 28 | 29 | async function createVideoCards(videoItems) { 30 | let newVideoCards = []; 31 | for (const video of videoItems) { 32 | const videoId = video.id; 33 | const snippet = video.snippet; 34 | const channelId = snippet.channelId; 35 | const response = await axios 36 | .get(`https://www.googleapis.com/youtube/v3/channels?part=snippet&id=${channelId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 37 | const channelImage = response.data.items[0].snippet.thumbnails.medium.url; 38 | 39 | const title = snippet.title; 40 | const image = snippet.thumbnails.medium.url; 41 | const views = video.statistics.viewCount; 42 | const timestamp = DateTime.fromISO(snippet.publishedAt).toRelative(); 43 | const channel = snippet.channelTitle; 44 | 45 | newVideoCards.push({ 46 | videoId, 47 | image, 48 | title, 49 | channel, 50 | views, 51 | timestamp, 52 | channelImage 53 | }); 54 | }; 55 | setVideoCards(newVideoCards); 56 | setIsLoading(false); 57 | } 58 | 59 | if(isError) { 60 | return No Results found! 61 | } 62 | return ( 63 | 64 |
65 | { isLoading ? : null } 66 |
67 | { 68 | videoCards.map(item => { 69 | return ( 70 | 71 | 79 | 80 | ) 81 | }) 82 | } 83 |
84 |
85 | ) 86 | } 87 | 88 | export default RecommendedVideos; 89 | -------------------------------------------------------------------------------- /src/components/VideoPlayer/VideoPlayer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import {useParams} from 'react-router'; 3 | import Video from './../Video/Video' 4 | import './VideoPlayer.css'; 5 | import RecommendedVideos from '../RecommendedVideos/RecommendedVideos'; 6 | import VideoInfo from '../VideoInfo/VideoInfo'; 7 | import axios from 'axios'; 8 | import CircularProgress from '@material-ui/core/CircularProgress'; 9 | import Alert from '@material-ui/lab/Alert'; 10 | 11 | const VideoPlayer = () => { 12 | let { videoId } = useParams(); 13 | 14 | const [videoInfo, setVideoInfo] = useState([]); 15 | const [isLoading, setIsLoading] = useState(true); 16 | const [isError, setIsError] = useState(false); 17 | 18 | useEffect(() => { 19 | setVideoInfo([]); 20 | setIsLoading(true); 21 | axios 22 | .get(`https://www.googleapis.com/youtube/v3/videos?part=snippet%2C%20statistics&id=${videoId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 23 | .then(response => { 24 | createVideoInfo(response.data['items'][0]); 25 | setIsError(false); 26 | }) 27 | .catch(error => { 28 | console.log(error); 29 | setIsError(true); 30 | }) 31 | }, [videoId]) 32 | 33 | async function createVideoInfo (video) { 34 | const snippet = video.snippet; 35 | const stats = video.statistics; 36 | const channelId = snippet.channelId; 37 | const response = await axios 38 | .get(`https://www.googleapis.com/youtube/v3/channels?part=snippet%2C%20statistics&id=${channelId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 39 | 40 | const channelImage = response.data.items[0].snippet.thumbnails.medium.url; 41 | const subs = response.data.items[0].statistics.subscriberCount; 42 | const publishedDate = new Date(snippet.publishedAt).toLocaleDateString('en-GB', { 43 | day : 'numeric', 44 | month : 'short', 45 | year : 'numeric' 46 | }); 47 | const title = snippet.title; 48 | const description = snippet.description; 49 | const channelTitle = snippet.channelTitle; 50 | const viewCount = stats.viewCount; 51 | const likeCount = stats.likeCount; 52 | const dislikeCount = stats.dislikeCount; 53 | 54 | setVideoInfo({ 55 | title, 56 | description, 57 | publishedDate, 58 | channelTitle, 59 | channelImage, 60 | viewCount, 61 | likeCount, 62 | dislikeCount, 63 | subs 64 | }); 65 | setIsLoading(false); 66 | } 67 | if(isError) { 68 | return No Results found! 69 | } 70 | return ( 71 |
72 |
73 |
74 | {isLoading ? :
76 |
77 | {!isLoading ? : null 88 | } 89 |
90 |
91 |
92 | 93 |
94 |
95 | ) 96 | } 97 | 98 | export default VideoPlayer; 99 | -------------------------------------------------------------------------------- /src/components/SearchPage/SearchPage.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import "./SearchPage.css"; 3 | import TuneIcon from '@material-ui/icons/Tune'; 4 | import ChannelRow from './../ChannelRow/ChannelRow'; 5 | import VideoRow from './../VideoRow/VideoRow'; 6 | import {useParams} from 'react-router'; 7 | import axios from 'axios'; 8 | import {DateTime} from 'luxon'; 9 | import { Link } from 'react-router-dom'; 10 | import CircularProgress from '@material-ui/core/CircularProgress'; 11 | import Alert from '@material-ui/lab/Alert'; 12 | 13 | 14 | const SearchPage = (props) => { 15 | let { searchQuery } = useParams(); 16 | 17 | const [channelRow, setChannelRow] = useState(''); 18 | const [videoRows, setVideoRows] = useState([]); 19 | const [isLoading, setIsLoading] = useState(true); 20 | const [isError, setIsError] = useState(false); 21 | 22 | useEffect(() => { 23 | setChannelRow(''); 24 | setVideoRows([]); 25 | axios 26 | .get(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=1&type=channel&q=${searchQuery}&safeSearch=none&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 27 | .then(response => { 28 | createChannelRow(response.data['items'][0]); 29 | }) 30 | 31 | 32 | axios 33 | .get(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=9&type=video&q=${searchQuery}&safeSearch=none&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 34 | .then(response => { 35 | createVideoRows(response.data['items']); 36 | setIsError(false); 37 | }) 38 | .catch(error => { 39 | console.log(error); 40 | setIsError(true); 41 | setIsLoading(false); 42 | }) 43 | 44 | }, [searchQuery]) 45 | 46 | 47 | async function createChannelRow(channel) { 48 | const channelId = channel.id.channelId; 49 | const response = await axios 50 | .get(`https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${channelId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 51 | const noOfVideos = response.data.items[0].statistics.videoCount; 52 | const subs = response.data.items[0].statistics.subscriberCount; 53 | const snippet = channel.snippet; 54 | const title = snippet.title; 55 | const description = snippet.description; 56 | const image = snippet.thumbnails.medium.url; 57 | setChannelRow({ 58 | channelId, 59 | image, 60 | title, 61 | subs, 62 | noOfVideos, 63 | description 64 | }); 65 | } 66 | 67 | async function createVideoRows(videos) { 68 | let newVideoRows = []; 69 | for (const video of videos) { 70 | const videoId = video.id.videoId; 71 | const response = await axios 72 | .get(`https://www.googleapis.com/youtube/v3/videos?part=statistics%2C%20snippet&id=${videoId}&key=${process.env.REACT_APP_YOUTUBE_API_KEY}`) 73 | const views = response.data.items[0].statistics.viewCount; 74 | const snippet = video.snippet; 75 | const title = snippet.title; 76 | const timestamp = DateTime.fromISO(snippet.publishedAt).toRelative(); 77 | const channel = snippet.channelTitle; 78 | const description = snippet.description; 79 | const image = snippet.thumbnails.medium.url; 80 | 81 | newVideoRows.push({ 82 | videoId, 83 | title, 84 | image, 85 | views, 86 | timestamp, 87 | channel, 88 | description 89 | }); 90 | }; 91 | setVideoRows(newVideoRows); 92 | setIsLoading(false); 93 | } 94 | if (isError) { 95 | return No Results found! 96 | } 97 | return ( 98 |
99 |
100 | 101 |

Filter

102 |
103 | { isLoading ? : null } 104 |
105 | { !isLoading ? : null 113 | } 114 |
115 | { 116 | videoRows.map(item => { 117 | return ( 118 | 119 | 127 | 128 | ) 129 | }) 130 | 131 | } 132 | 133 |
134 | ) 135 | } 136 | 137 | export default SearchPage; 138 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | --------------------------------------------------------------------------------