├── src ├── components │ ├── instructions │ │ ├── index.css │ │ └── index.jsx │ ├── commands │ │ ├── user-input-list.jsx │ │ ├── command-input.jsx │ │ └── search-list.jsx │ ├── navbar.jsx │ ├── toggle │ │ ├── index.jsx │ │ ├── styled.js │ │ ├── bulboff.svg │ │ └── bulbon.svg │ ├── command.jsx │ ├── player.jsx │ ├── VideoDetail.jsx │ └── player.scss ├── services │ ├── global.js │ ├── theme.js │ └── youtube.js ├── setupTests.js ├── App.test.js ├── App.css ├── index.js ├── index.css ├── serviceWorker.js └── App.js ├── .env.sample ├── public ├── favicon.ico ├── robots.txt ├── assets │ ├── webpod.png │ └── mchammer.gif ├── manifest.json └── index.html ├── .gitignore ├── package.json ├── LICENSE └── README.md /src/components/instructions/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_API_KEY="Your API Key" 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anuragbhattacharjee/webpod/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/assets/webpod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anuragbhattacharjee/webpod/HEAD/public/assets/webpod.png -------------------------------------------------------------------------------- /public/assets/mchammer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anuragbhattacharjee/webpod/HEAD/public/assets/mchammer.gif -------------------------------------------------------------------------------- /src/services/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | body { 5 | background: ${({ theme }) => theme.body}; 6 | color: ${({ theme }) => theme.text}; 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/services/theme.js: -------------------------------------------------------------------------------- 1 | export const lightTheme = { 2 | body: "#f5f5f5", 3 | text: "#212529", 4 | }; 5 | 6 | export const darkTheme = { 7 | body: `linear-gradient( 8 | 159deg, 9 | rgba(37, 37, 50, 0.98) 0%, 10 | rgba(35, 35, 45, 0.98) 100% 11 | )`, 12 | text: "#fafafc", 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/youtube.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const KEY = process.env.REACT_APP_API_KEY; 3 | 4 | export const baseTerms = { 5 | part: "snippet", 6 | maxResults: 5, 7 | key: KEY, 8 | }; 9 | 10 | export default axios.create({ 11 | baseURL: "https://www.googleapis.com/youtube/v3/", 12 | }); 13 | -------------------------------------------------------------------------------- /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 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .container-fluid { 2 | text-align: center; 3 | } 4 | .flex-fill { 5 | flex: 1; 6 | } 7 | 8 | .main-container { 9 | border: 12px solid #333642; 10 | /* height: inherit; */ 11 | } 12 | 13 | .main-instructions-column { 14 | border-right: 18px solid #333642; 15 | padding: 20px; 16 | } 17 | 18 | .main-player-column { 19 | /* border: 1px solid rgba(0, 0, 0, 0.4); */ 20 | padding: 20px; 21 | } 22 | 23 | .main-command-column { 24 | border-left: 18px solid #333642; 25 | padding: 20px; 26 | } 27 | -------------------------------------------------------------------------------- /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 "bootstrap/dist/css/bootstrap.css"; 7 | // import "font-awesome/css/font-awesome.css"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: https://bit.ly/CRA-PWA 19 | serviceWorker.unregister(); 20 | -------------------------------------------------------------------------------- /src/components/commands/user-input-list.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const UserInputList = (props) => { 4 | return ( 5 | 22 | ); 23 | }; 24 | 25 | export default UserInputList; 26 | -------------------------------------------------------------------------------- /src/components/commands/command-input.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CommandInput = (props) => { 4 | const { inputTerm, onInputChange, onCommandSubmit } = props; 5 | 6 | return ( 7 |
8 |
9 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default CommandInput; 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | WebPod by Anurag 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class Navbar extends Component { 4 | navbarStyle = { 5 | margin: "auto", 6 | }; 7 | render() { 8 | return ( 9 |
10 | {/*
11 | Total Users using: 2000 12 |
*/} 13 |

14 | 📻 15 | WebPod 16 |

17 | 18 | {/*
19 | 20 |
*/} 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default Navbar; 27 | -------------------------------------------------------------------------------- /src/components/toggle/index.jsx: -------------------------------------------------------------------------------- 1 | // Toggle.js 2 | import React from "react"; 3 | import { func, string } from "prop-types"; 4 | import { ToggleContainer } from "./styled"; 5 | // import styled from "styled-components"; 6 | 7 | // Import a couple of SVG files we'll use in the design: https://www.flaticon.com 8 | import { ReactComponent as BulbOnIcon } from "./bulbon.svg"; 9 | import { ReactComponent as BulbOffIcon } from "./bulboff.svg"; 10 | 11 | const Toggle = ({ theme, toggleTheme }) => { 12 | const isLight = theme === "light"; 13 | return ( 14 | 15 | 16 | 17 | {/*

{theme}

*/} 18 |
19 | ); 20 | }; 21 | 22 | Toggle.propTypes = { 23 | theme: string.isRequired, 24 | toggleTheme: func.isRequired, 25 | }; 26 | 27 | export default Toggle; 28 | -------------------------------------------------------------------------------- /src/components/toggle/styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ToggleContainer = styled.a` 4 | // position: relative; 5 | // width: 8rem; 6 | // height: 4rem; 7 | cursor: pointer; 8 | width: 54px; 9 | height: 54px; 10 | padding-top: 5px; 11 | display: inline-block; 12 | text-align: center; 13 | border-radius: 50%; 14 | transition: all 0.4s ease-in-out; 15 | background-color: #fff; 16 | margin-bottom: 10px; 17 | svg { 18 | height: auto; 19 | width: 2.5rem; 20 | transition: all 0.3s linear; 21 | 22 | // sun icon 23 | &:first-child { 24 | display: ${({ lightTheme }) => (lightTheme ? "none" : "")}; 25 | transition: ${({ lightTheme }) => 26 | lightTheme ? "" : "opacity 1s ease-in-out"}; 27 | } 28 | 29 | // moon icon 30 | &:nth-child(2) { 31 | display: ${({ lightTheme }) => (lightTheme ? "" : "none")}; 32 | transition: ${({ lightTheme }) => 33 | lightTheme ? "opacity 1s ease-in-out" : ""}; 34 | } 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/command.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import CommandInput from "./commands/command-input"; 4 | import UserInputList from "./commands/user-input-list"; 5 | import SearchList from "./commands/search-list"; 6 | 7 | const Command = (props) => { 8 | const { videos, commands, inputTerm, onInputChange, onCommandSubmit } = props; 9 | 10 | return ( 11 |
12 |
13 |
14 |
15 | 20 |
21 |
22 |
23 |
24 | {videos.length > 0 && } 25 | 26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | export default Command; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpod", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "bootstrap": "^4.5.0", 11 | "dotenv": "^8.2.0", 12 | "lodash": "^4.17.15", 13 | "node-sass": "^4.14.1", 14 | "react": "^16.13.1", 15 | "react-bootstrap": "^1.0.1", 16 | "react-dom": "^16.13.1", 17 | "react-scripts": "3.4.1", 18 | "styled-components": "^5.1.1" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anurag Bhattacharjee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | html { 12 | font-family: "MuseoModerno", cursive; 13 | line-height: 1.15; 14 | -webkit-text-size-adjust: 100%; 15 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 16 | } 17 | 18 | body { 19 | /* background: linear-gradient( 20 | 159deg, 21 | rgba(37, 37, 50, 0.98) 0%, 22 | rgba(35, 35, 45, 0.98) 100% 23 | ); */ 24 | background-image: linear-gradient( 25 | 159deg, 26 | rgba(37, 37, 50, 0.98) 0%, 27 | rgba(35, 35, 45, 0.98) 100% 28 | ); 29 | background-position-x: initial; 30 | background-position-y: initial; 31 | background-size: initial; 32 | background-repeat-x: initial; 33 | background-repeat-y: initial; 34 | background-attachment: initial; 35 | background-origin: initial; 36 | background-clip: initial; 37 | background-color: initial; 38 | /* color: #fafafc; */ 39 | margin: 0; 40 | font-family: "MuseoModerno"; 41 | font-size: 1rem; 42 | font-weight: 400; 43 | line-height: 1.5; 44 | overflow-x: hidden; 45 | } 46 | code { 47 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 48 | monospace; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/commands/search-list.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SearchList = (props) => { 4 | const styles = { 5 | codeStyle: { 6 | color: "orangered", 7 | fontFamily: "roboto", 8 | borderRadius: "5px", 9 | padding: "2px", 10 | background: "#333655", 11 | }, 12 | searchDiv: { 13 | fontFamily: "monospace", 14 | textAlign: "left", 15 | marginTop: "10px", 16 | }, 17 | list: { 18 | listStyleType: "none", 19 | border: "2px solid darkgoldenrod", 20 | borderRadius: "5px", 21 | marginTop: "10px", 22 | padding: "5px 10px", 23 | background: "#333655", 24 | }, 25 | listElement: { 26 | padding: "5px 10px", 27 | fontSize: "14px", 28 | }, 29 | }; 30 | return ( 31 |
32 |
33 |
34 | Please type the number of the song you want to play,{" "} 35 | !dismiss to dismiss. 36 |
37 |
    38 | {props.videos.map((video, index) => ( 39 |
  • 40 | {index + 1}. {video.snippet.title} 41 |
  • 42 | ))} 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default SearchList; 50 | -------------------------------------------------------------------------------- /src/components/toggle/bulboff.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/player.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import VideoDetail from "./VideoDetail"; 3 | import _ from "lodash"; 4 | 5 | import "./player.scss"; 6 | 7 | class Player extends Component { 8 | state = {}; 9 | render() { 10 | const { video, onVideoEvent } = this.props; 11 | const { 12 | channelId, 13 | publishedAt, 14 | title, 15 | description, 16 | thumbnails, 17 | channelTitle, 18 | } = !_.isEmpty(video) ? video.snippet : {}; 19 | const imageURl = !_.isEmpty(video) ? thumbnails.medium.url : ""; 20 | const publishedDate = !_.isEmpty(video) ? publishedAt.split("T")[0] : ""; 21 | return ( 22 |
23 |
24 |
25 |
Now Playing
26 |
27 |
28 | 29 |
30 |
31 |
32 |
{title}
33 | {/*
{description}
*/} 34 |
35 |
36 |
37 |
38 |
39 |
40 | {/* */} 41 | 42 |
43 |
44 |
45 |
{publishedDate}
46 |
{channelTitle}
47 |
48 | {!_.isEmpty(video) && ( 49 | onVideoEvent(c)} /> 50 | )} 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default Player; 57 | -------------------------------------------------------------------------------- /src/components/VideoDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class VideoDetail extends Component { 4 | componentDidMount = () => { 5 | // On mount, check to see if the API script is already loaded 6 | if (!window.YT) { 7 | // If not, load the script asynchronously 8 | const tag = document.createElement("script"); 9 | tag.src = "https://www.youtube.com/iframe_api"; 10 | 11 | // onYouTubeIframeAPIReady will load the video after the script is loaded 12 | window.onYouTubeIframeAPIReady = this.loadVideo; 13 | 14 | const firstScriptTag = document.getElementsByTagName("script")[0]; 15 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 16 | } else { 17 | // If script is already there, load the video directly 18 | this.loadVideo(); 19 | } 20 | }; 21 | 22 | loadVideo = () => { 23 | const { video } = this.props; 24 | const id = video.id.videoId; 25 | 26 | // the Player object is created uniquely based on the id in props 27 | this.player = new window.YT.Player(`youtube-player`, { 28 | videoId: id, 29 | events: { 30 | onReady: this.onPlayerReady, 31 | // onStateChange: this.onPlayerStateChange, 32 | }, 33 | }); 34 | }; 35 | 36 | onPlayerStateChange = (event) => {}; 37 | 38 | onPlayerReady = (event) => { 39 | event.target.playVideo(); 40 | this.props.onVideoEvent(event); 41 | }; 42 | 43 | render() { 44 | const { video } = this.props; 45 | 46 | if (!video) { 47 | return
Loading ...
; 48 | } 49 | 50 | // const videoSrc = `https://www.youtube.com/embed/${video.id.videoId}`; 51 | return ( 52 |
53 |
54 | {/*