├── 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 |
6 | {props.commands.map((cmd, index) => (
7 | -
18 | {cmd}
19 |
20 | ))}
21 |
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 |
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 |
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 |
27 |
28 |

29 |
30 |
31 |
32 |
{title}
33 | {/*
{description}
*/}
34 |
35 |
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 |
58 | );
59 | }
60 | }
61 |
62 | export default VideoDetail;
63 |
--------------------------------------------------------------------------------
/src/components/instructions/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Toggle from "../toggle";
3 | import "./index.css";
4 |
5 | const Instructions = (props) => {
6 | const styles = {
7 | codeStyle: {
8 | color: "orangered",
9 | fontFamily: "roboto",
10 | borderRadius: "5px",
11 | padding: "2px",
12 | background: "#333655",
13 | },
14 | };
15 |
16 | const { theme, onToggleTheme } = props;
17 |
18 | return (
19 |
20 |
Instructions
21 |
22 |
23 |
24 |
25 | Please follow these commands:
26 | !search music_name:
27 |
28 | - A search list will appear
29 | - Only enter the number of the song you want to listen.
30 | - Example: 3
31 |
32 |
33 | !play music_name: to play a
34 | video
35 |
36 |
37 | !pause: to pause current playing
38 | music
39 |
40 |
41 | !stop: To stop playing
42 |
43 |
44 | !volume desired_volume: To set
45 | volume
Example: !volume 71
46 |
47 |
48 |
49 |
50 |
51 |
52 | {theme} mode
53 |
54 |
55 |
56 |
57 | Made with{" "}
58 | {" "}
62 | by Anurag
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default Instructions;
70 |
--------------------------------------------------------------------------------
/src/components/toggle/bulbon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Webpod
2 |
3 | 
4 |
5 | [](https://github.com/RichardLitt/standard-readme)
6 | 
7 | 
8 | 
9 | 
10 |
11 | A Web App to play distraction free music.
12 |
13 | ## Table of Contents
14 |
15 | - [webpod](#webpod)
16 | - [Table of Contents](#table-of-contents)
17 | - [Instructions](#insuctions)
18 | - [Install](#install)
19 | - [Usage](#usage)
20 | - [`npm start`](#yarn-start)
21 | - [`npm test`](#yarn-test)
22 | - [`npm build`](#yarn-build)
23 | - [API](#api)
24 | - [Maintainers](#maintainers)
25 | - [Contributing](#contributing)
26 | - [License](#license)
27 |
28 | ## Instructions:
29 |
30 | -!search music_name: - A search list will appear - Only enter the number of the song you want to listen. - Example: 3
31 | -!play music_name - to play a music
32 | -!pause - to pause current playing music
33 | -!stop - to stop playing
34 | -!volume desired_volume - To set volume - Example: !volume 71
35 |
36 | ## Background
37 |
38 | ## Install
39 |
40 | In the project directory, run:
41 |
42 | ```
43 | npm install
44 | ```
45 |
46 | ## Usage
47 |
48 | You need to create your own API credential on [Google Developer Console](https://console.developers.google.com/apis/credentials).
49 | Rename .env.sample to .env and replace API_KEY value to your API Key.
50 |
51 | In the project directory, you can run:
52 |
53 | ### `npm start`
54 |
55 | Runs the app in the development mode.
56 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
57 |
58 | The page will reload if you make edits.
59 | You will also see any lint errors in the console.
60 |
61 | ### `npm build`
62 |
63 | Builds the app for production to the `build` folder.
64 | It correctly bundles React in production mode and optimizes the build for the best performance.
65 |
66 | The build is minified and the filenames include the hashes.
67 | Your app is ready to be deployed!
68 |
69 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
70 |
71 | ## Maintainers
72 |
73 | [@anuragbhattacharjee](https://github.com/anuragbhattacharjee)
74 |
75 | ## Contributing
76 |
77 | See [the contributing file](contributing.md)!
78 |
79 | PRs accepted.
80 |
81 | ## License
82 |
83 | MIT © 2020 Anurag Bhattacharjee
84 |
--------------------------------------------------------------------------------
/src/components/player.scss:
--------------------------------------------------------------------------------
1 | // @import "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700";
2 |
3 | // Colors
4 | $black: #121212;
5 | $white: #ffffff;
6 | $orange: #ff4700;
7 | $blue: #1d2d3a;
8 |
9 | // Mixins
10 | @mixin generate-gradient($color) {
11 | background: -moz-linear-gradient(
12 | top,
13 | rgba($color, 0) 0%,
14 | rgba($color, 1) 100%
15 | );
16 | background: -webkit-gradient(
17 | left top,
18 | left bottom,
19 | color-stop(0%, rgba($color, 0)),
20 | color-stop(100%, rgba($color, 1))
21 | );
22 | background: -webkit-linear-gradient(
23 | top,
24 | rgba($color, 0) 0%,
25 | rgba($color, 1) 100%
26 | );
27 | background: -o-linear-gradient(top, rgba($color, 0) 0%, rgba($color, 1) 100%);
28 | background: -ms-linear-gradient(
29 | top,
30 | rgba($color, 0) 0%,
31 | rgba($color, 1) 100%
32 | );
33 | background: linear-gradient(
34 | to bottom,
35 | rgba($color, 0) 0%,
36 | rgba($color, 1) 100%
37 | );
38 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff4800', endColorstr='#ff4800', GradientType=0 );
39 | }
40 |
41 | // Styles
42 | body {
43 | align-items: center;
44 | // background: darken($blue, 10);
45 | // display: flex;
46 | font-family: "Roboto", sans-serif;
47 | color: white;
48 | // height: 100vh;
49 | // justify-content: center;
50 | }
51 |
52 | audio {
53 | display: none;
54 | }
55 |
56 | .Player {
57 | background: darken($blue, 3);
58 | border-radius: 5px;
59 | overflow: hidden;
60 | box-shadow: 0 5px 10px -5px rgba($black, 1);
61 | height: 667px;
62 | position: relative;
63 | width: 375px;
64 | margin: auto;
65 | margin-top: 20px;
66 |
67 | .Background {
68 | width: 150%;
69 | height: 150%;
70 | position: absolute;
71 | top: -25%;
72 | left: -25%;
73 | background: url(https://funkadelphia.files.wordpress.com/2012/09/odesza-summers-gone-lp.jpg);
74 | background-size: cover;
75 | background-position: center center;
76 | opacity: 0.2;
77 | filter: blur(10px);
78 | }
79 |
80 | .Title {
81 | width: 300px;
82 | margin: 50px auto;
83 | font-size: 12px;
84 | text-transform: uppercase;
85 | letter-spacing: 0.1em;
86 | position: relative;
87 | }
88 |
89 | .Artwork {
90 | // max-width: 300px;
91 | height: 250px;
92 | // background: url(https://funkadelphia.files.wordpress.com/2012/09/odesza-summers-gone-lp.jpg);
93 | background-size: cover;
94 | background-position: center center;
95 | border-radius: 4px;
96 | margin: auto;
97 | box-shadow: 0 5px 10px -5px rgba($black, 0.25);
98 | position: relative;
99 | }
100 |
101 | .TrackInformation {
102 | width: 300px;
103 | margin: 30px auto;
104 | text-align: center;
105 | position: relative;
106 |
107 | .Name {
108 | font-size: 24px;
109 | margin-bottom: 10px;
110 | font-weight: 300;
111 | }
112 |
113 | .Artist {
114 | font-size: 16px;
115 | margin-bottom: 5px;
116 | font-weight: 500;
117 | }
118 |
119 | .Album {
120 | font-size: 12px;
121 | opacity: 0.5;
122 | }
123 | }
124 |
125 | .Scrubber {
126 | position: absolute;
127 | bottom: 0;
128 | left: 0;
129 | width: 100%;
130 | height: 20%;
131 | opacity: 0.2;
132 | transition: opacity 0.25s ease;
133 | border-bottom-left-radius: 5px;
134 | border-bottom-right-radius: 5px;
135 |
136 | &:active {
137 | opacity: 1;
138 | }
139 |
140 | .Scrubber-Progress {
141 | @include generate-gradient($orange);
142 | height: 100%;
143 | width: 0%;
144 | transition: width 0.25s ease;
145 | border-bottom-left-radius: 5px;
146 | border-bottom-right-radius: 5px;
147 | }
148 | }
149 |
150 | .Timestamps {
151 | display: flex;
152 | justify-content: space-between;
153 | box-sizing: border-box;
154 | padding: 20px;
155 | position: absolute;
156 | bottom: 0;
157 | left: 0;
158 | pointer-events: none;
159 | z-index: 2;
160 | width: 100%;
161 |
162 | .Time {
163 | font-size: 12px;
164 | }
165 | }
166 |
167 | .Controls {
168 | position: absolute;
169 | bottom: 8%;
170 | pointer-events: none;
171 | margin: auto;
172 | left: 0;
173 | right: 0;
174 |
175 | .Button {
176 | height: 75px;
177 | width: 75px;
178 | border: 2px solid rgba($white, 0.5);
179 | box-sizing: border-box;
180 | display: flex;
181 | align-items: center;
182 | justify-content: center;
183 | border-radius: 75px;
184 | box-shadow: 0 5px 10px 0px rgba($black, 0.125);
185 | margin: auto;
186 | pointer-events: all;
187 | background: $white;
188 |
189 | &:active {
190 | transform: scale(0.98);
191 | background: $white;
192 |
193 | .fa {
194 | color: $orange;
195 | opacity: 1;
196 | }
197 | }
198 |
199 | .fa {
200 | color: $white;
201 | opacity: 0.5;
202 | font-size: 24px;
203 |
204 | &.fa-play {
205 | margin-left: 5px;
206 | }
207 | }
208 |
209 | .hammertime {
210 | height: 50px;
211 | vertical-align: middle;
212 | }
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import "./App.css";
3 | import _ from "lodash";
4 |
5 | // For dark mode
6 | import { ThemeProvider } from "styled-components";
7 | import { lightTheme, darkTheme } from "./services/theme";
8 | import { GlobalStyle } from "./services/global";
9 |
10 | import Navbar from "./components/navbar";
11 | import Player from "./components/player";
12 | import Instructions from "./components/instructions/index";
13 | import Command from "./components/command";
14 |
15 | import youtube, { baseTerms } from "./services/youtube";
16 |
17 | require("dotenv").config();
18 |
19 | // document.body.style.backgroundColor = "#282c34";
20 |
21 | class App extends Component {
22 | state = {
23 | inputTerm: "",
24 | commands: [],
25 | videos: [],
26 | selectedVideo: null,
27 | instruction: "",
28 | vdoEvent: null,
29 | vdoPauseTime: 0,
30 | theme: "dark",
31 | };
32 |
33 | handleVideoEvent = (c) => {
34 | this.setState({
35 | vdoEvent: c,
36 | });
37 | };
38 |
39 | handleInputChange = (event) => {
40 | this.setState({
41 | inputTerm: event.target.value,
42 | });
43 | };
44 |
45 | handleCommandSubmit = async (event) => {
46 | event.preventDefault();
47 |
48 | // For displaying user inputs
49 | let commands = [...this.state.commands];
50 | let selectedVideo = { ...this.state.selectedVideo };
51 | let videos = [...this.state.videos];
52 | const vdoEvent = this.state.vdoEvent;
53 | let instruction = "";
54 | let searchTerm = "";
55 |
56 | const inputTerm = this.state.inputTerm.toLowerCase();
57 |
58 | if (!isNaN(inputTerm) && !_.isEmpty(videos)) {
59 | if (!_.isEmpty(selectedVideo)) {
60 | this.stopVideo();
61 | }
62 | let index = parseInt(inputTerm);
63 | if (index > videos.length) index = videos.length;
64 | selectedVideo = { ...videos[index - 1] };
65 | videos = []; // For not showing Search list in the menu anymore
66 | if (!_.isEmpty(vdoEvent)) {
67 | vdoEvent.target.loadVideoById(selectedVideo.id.videoId);
68 | }
69 | commands.push(`Playing ${selectedVideo.snippet.title}`);
70 | instruction = "";
71 | searchTerm = "";
72 | } else {
73 | instruction = inputTerm.match(/!\w+/g);
74 | if (instruction)
75 | instruction = instruction.length > 0 ? instruction[0] : "";
76 |
77 | searchTerm = instruction
78 | ? inputTerm.toLowerCase().split(instruction)[1].trim()
79 | : "";
80 | }
81 |
82 | if (instruction === "!volume") {
83 | this.setVolume(parseInt(searchTerm));
84 | commands.push(`Volume set to: ${searchTerm}%`);
85 | } else if (instruction === "!play" || instruction === "!search") {
86 | if (searchTerm !== "") videos = await this.searchVideo(searchTerm);
87 | }
88 |
89 | switch (instruction) {
90 | case "!play":
91 | if (searchTerm === "") {
92 | const videoResumed = this.resumeVideo();
93 | const msg = videoResumed
94 | ? `Resuming ${selectedVideo.snippet.title}`
95 | : "No music on the list.";
96 | commands.push(msg);
97 | } else {
98 | if (!_.isEmpty(selectedVideo)) {
99 | this.stopVideo();
100 | }
101 | selectedVideo = { ...videos[0] };
102 | videos = []; // For not showing Search list in the menu
103 | if (!_.isEmpty(vdoEvent)) {
104 | vdoEvent.target.loadVideoById(selectedVideo.id.videoId);
105 | }
106 | commands.push(`Playing ${selectedVideo.snippet.title}`);
107 | }
108 | break;
109 | case "!pause":
110 | this.pauseVideo();
111 | break;
112 | case "!resume":
113 | const videoResumed = this.resumeVideo();
114 | const resumeMsg = videoResumed
115 | ? `Resuming ${selectedVideo.snippet.title}`
116 | : "No music on the list.";
117 | commands.push(resumeMsg);
118 | break;
119 | case "!stop":
120 | const stoppedVideo = this.stopVideo();
121 | if (stoppedVideo) {
122 | commands.push(`Stopped playing ${selectedVideo.snippet.title}`);
123 | selectedVideo = null;
124 | } else {
125 | commands.push("No music to stop.");
126 | }
127 | break;
128 | default:
129 | break;
130 | }
131 |
132 | commands = commands.reverse();
133 | // localStorage.setItem("commands", commands);
134 |
135 | this.setState({
136 | inputTerm: "",
137 | commands,
138 | videos,
139 | selectedVideo,
140 | });
141 | };
142 |
143 | searchVideo = async (searchTerm) => {
144 | console.log("Process key: ", process.env);
145 | const response = await youtube.get("/search", {
146 | params: {
147 | ...baseTerms,
148 | q: searchTerm,
149 | },
150 | });
151 | return response.data.items;
152 | };
153 |
154 | setVolume = (volume) => {
155 | const { vdoEvent } = this.state;
156 | if (!_.isEmpty(vdoEvent)) {
157 | vdoEvent.target.setVolume(volume);
158 | this.setState({
159 | vdoPauseTime: vdoEvent.target.getDuration(),
160 | });
161 | }
162 | };
163 |
164 | pauseVideo = () => {
165 | const { vdoEvent } = this.state;
166 | if (!_.isEmpty(vdoEvent)) {
167 | vdoEvent.target.pauseVideo();
168 | this.setState({
169 | vdoPauseTime: vdoEvent.target.getDuration(),
170 | });
171 | }
172 | };
173 |
174 | resumeVideo = () => {
175 | const { vdoEvent } = this.state;
176 | if (!_.isEmpty(vdoEvent)) {
177 | vdoEvent.target.playVideo();
178 | return 1;
179 | }
180 | return 0;
181 | };
182 |
183 | stopVideo = () => {
184 | const { vdoEvent } = this.state;
185 | if (!_.isEmpty(vdoEvent)) {
186 | vdoEvent.target.stopVideo();
187 | // this.setState({
188 | // vdoEvent: null,
189 | // });
190 | return 1;
191 | }
192 | return 0;
193 | };
194 |
195 | // The function that toggles between themes
196 | toggleTheme = () => {
197 | // if the theme is not light, then set it to dark
198 | if (this.state.theme === "light") {
199 | this.setState({ theme: "dark" });
200 | // otherwise, it should be light
201 | } else {
202 | this.setState({ theme: "light" });
203 | }
204 | };
205 |
206 | render() {
207 | const { inputTerm, videos, selectedVideo, commands, theme } = this.state;
208 |
209 | return (
210 |
211 | <>
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
this.handleVideoEvent(c)}
223 | />
224 |
225 |
226 |
233 |
234 |
235 |
236 | >
237 |
238 | );
239 | }
240 | }
241 |
242 | export default App;
243 |
--------------------------------------------------------------------------------