├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerrun.aws.json ├── LICENSE ├── README.md ├── __tests__ ├── client │ ├── App.test.jsx │ ├── components │ │ └── LoginPage.test.jsx │ └── index.test.jsx └── server │ ├── controllers │ └── apiController.test.js │ ├── routes │ └── auth.test.js │ └── server.test.js ├── client ├── App │ ├── PrivateRoute.jsx │ ├── index.css │ └── index.jsx ├── assets │ └── graffiti.jpg ├── components │ ├── Chat │ │ ├── index.css │ │ └── index.jsx │ ├── CreateNewRoomModal │ │ ├── index.css │ │ └── index.jsx │ ├── DashboardPage │ │ ├── index.css │ │ └── index.jsx │ ├── HostDisableRoomButton │ │ ├── index.css │ │ └── index.jsx │ ├── LoginPage │ │ ├── index.css │ │ └── index.jsx │ ├── PlaybackControls │ │ ├── index.css │ │ └── index.jsx │ ├── RoomOption │ │ ├── index.css │ │ └── index.jsx │ ├── RoomPage │ │ ├── index.jsx │ │ └── index.scss │ ├── SongOption │ │ ├── index.css │ │ └── index.jsx │ └── SongSearch │ │ ├── index.css │ │ └── index.jsx ├── index.html ├── index.js ├── store │ ├── action_types │ │ ├── player.js │ │ └── songQueue.js │ ├── index.js │ └── reducers │ │ ├── index.js │ │ ├── player.js │ │ └── songQueue.js └── stylesheets │ ├── constants.scss │ └── index.css ├── package.json ├── scripts └── main.sql ├── server ├── config │ └── passport-setup.js ├── controllers │ ├── apiController.js │ ├── authController.js │ ├── chatController.js │ ├── roomController.js │ ├── songController.js │ └── userController.js ├── models │ └── roomModels.js ├── routes │ ├── api.js │ └── auth.js └── server.js └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "jest": true 6 | }, 7 | "extends": ["plugin:react/recommended", "airbnb", "prettier"], 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "ecmaVersion": 11, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["react", "prettier"], 17 | "rules": { 18 | "react/jsx-props-no-spreading": "off", 19 | "prettier/prettier": "error", 20 | "no-unused-vars": ["warn", { "args": "none" }], 21 | "prefer-destructuring": "off", 22 | "radix": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | build 4 | .env 5 | pandaudio.zip -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": false, 4 | "endOfLine": "auto", 5 | "arrowParens": "avoid", 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pandaudio 2 | 3 | We want to make contributing to this project as easy and transparent as possible. 4 | 5 | ## Pull Requests 6 | 7 | We actively welcome your pull requests. 8 | 9 | 1. For the repo and create your branch from `master`. 10 | 2. If you've added code that should be tested, add tests. 11 | 3. Ensure the test suite passes. 12 | 4. Make sure your code lints and is formatted with `prettier`. 13 | 14 | ## Issues 15 | 16 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 17 | 18 | ## License 19 | 20 | By contributing to Pandaudio, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | CMD npm start 14 | 15 | EXPOSE 3000 -------------------------------------------------------------------------------- /Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": "pandaudio/panda:1", 5 | "Update": "true" 6 | }, 7 | "Ports": [ 8 | { 9 | "ContainerPort": "3000" 10 | } 11 | ], 12 | "Volumes": [] 13 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pandaudio 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pandaudio 2 | 3 | Pandaudio is an Open Source platform where users can join chat rooms and listen to the host's favorite music. 4 | 5 | ## Installation 6 | 7 | The Pandaudio platform lives on https://www.pandaudio.com. 8 | 9 | ## Contributing 10 | 11 | Development of Pandaudio happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Pandaudio. 12 | 13 | - [Contributing Guide](./CONTRIBUTING.md) 14 | 15 | ### License 16 | 17 | Pandaudio is [MIT licensed](./LICENSE) 18 | -------------------------------------------------------------------------------- /__tests__/client/App.test.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/__tests__/client/App.test.jsx -------------------------------------------------------------------------------- /__tests__/client/components/LoginPage.test.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/__tests__/client/components/LoginPage.test.jsx -------------------------------------------------------------------------------- /__tests__/client/index.test.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/__tests__/client/index.test.jsx -------------------------------------------------------------------------------- /__tests__/server/controllers/apiController.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/__tests__/server/controllers/apiController.test.js -------------------------------------------------------------------------------- /__tests__/server/routes/auth.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/__tests__/server/routes/auth.test.js -------------------------------------------------------------------------------- /__tests__/server/server.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/__tests__/server/server.test.js -------------------------------------------------------------------------------- /client/App/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const PrivateRoute = ({ component: Component, ...rest }) => { 6 | return ( 7 | { 10 | // Need to implement auth file 11 | if (auth.isAuthenticated()) { 12 | return ; 13 | } 14 | return ( 15 | 23 | ); 24 | }} 25 | /> 26 | ); 27 | }; 28 | 29 | PrivateRoute.propTypes = { 30 | component: PropTypes.element.isRequired, 31 | location: PropTypes.shape({ 32 | pathname: PropTypes.string.isRequired, 33 | }).isRequired, 34 | }; 35 | 36 | export default PrivateRoute; 37 | -------------------------------------------------------------------------------- /client/App/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/client/App/index.css -------------------------------------------------------------------------------- /client/App/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import LoginPage from '../components/LoginPage'; 4 | import RoomPage from '../components/RoomPage'; 5 | import DashboardPage from '../components/DashboardPage'; 6 | // import Chat from '../components/Chat'; 7 | // import { PrivateRoute } from './PrivateRoute'; 8 | import Cookies from 'js-cookie'; 9 | 10 | import { PLAYER_STATE_UPDATE, PLAYER_READY_UPDATE } from '../store/action_types/player'; 11 | import { useDispatch } from 'react-redux'; 12 | import { SONG_QUEUE_UPDATE } from '../store/action_types/songQueue'; 13 | 14 | const App = () => { 15 | // NOTE: This is for storing the player in the Store in case window strategy doesn't work 16 | const dispatch = useDispatch(); 17 | const accessToken = Cookies.get('accessToken'); 18 | const [deviceId, setDeviceId] = useState(""); 19 | 20 | let playerCheckInterval = null; 21 | 22 | useEffect(() => { 23 | playerCheckInterval = setInterval(() => { 24 | checkForPlayer(); 25 | }, 1000); 26 | }, []); 27 | 28 | 29 | // checks for if Spotify is accessible then creates a new player 30 | const checkForPlayer = () => { 31 | if (window.Spotify !== null) { 32 | clearInterval(playerCheckInterval); 33 | 34 | // create the spotify player 35 | const newPlayer = new window.Spotify.Player({ 36 | name: 'Music Zoom Player', 37 | getOAuthToken: cb => { 38 | cb(accessToken); 39 | }, 40 | volume: 0.1 41 | }); 42 | // create event handlers 43 | newPlayer.addListener('ready', data => { 44 | let { device_id } = data; 45 | console.log("Let the music play on!"); 46 | setDeviceId(device_id); 47 | }); 48 | 49 | // Playback status updates 50 | newPlayer.addListener('player_state_changed', state => { 51 | // store in player store 52 | dispatch({ type: PLAYER_STATE_UPDATE, payload: state }) 53 | }); 54 | 55 | // intialize the player connection immediatley after intializing 56 | newPlayer.connect(); 57 | 58 | // NOTE: This is for storing the player in the Store in case window strategy doesn't work 59 | // dispatch({ type: PLAYER_INITIALIZE, payload: newPlayer }); 60 | 61 | // flag in player store that it's ready 62 | dispatch({ type: PLAYER_READY_UPDATE, payload: true }) 63 | 64 | // store player reference in the window 65 | window.globalSpotifyPlayer = newPlayer; 66 | } 67 | }; 68 | 69 | return ( 70 |
71 | 72 | 73 | 74 | 75 | '404 NOT FOUND'} /> 76 | 77 |
78 | ); 79 | }; 80 | 81 | export default App; 82 | -------------------------------------------------------------------------------- /client/assets/graffiti.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/client/assets/graffiti.jpg -------------------------------------------------------------------------------- /client/components/Chat/index.css: -------------------------------------------------------------------------------- 1 | .chatBox { 2 | width: auto; 3 | } 4 | .chatFeed { 5 | height: 500px; 6 | background-color: transparent; 7 | overflow-y: scroll; 8 | display: flex; 9 | flex-direction: column-reverse; 10 | color: white; 11 | } 12 | .chat-img { 13 | height: 30px !important; 14 | width: 30px !important; 15 | border-radius: 50%; 16 | } 17 | -------------------------------------------------------------------------------- /client/components/Chat/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Cookies from 'js-cookie'; 3 | import Axios from 'axios'; 4 | import './index.css'; 5 | 6 | const URL = process.env.NODE_ENV === 'production' ? '/' : 'http://localhost:3000'; 7 | const socket = io.connect(URL); 8 | const moment = require('moment'); 9 | 10 | const Chat = ({ roomId }) => { 11 | const uuid = Cookies.get('uuid'); 12 | 13 | const [comments, addComment] = useState([]); 14 | const feed = []; 15 | 16 | useEffect(() => { 17 | // Fetch chat log from db on mount, not complete 18 | Axios.get(`/api/v1/rooms/${roomId}/chat`) 19 | .then(response => { 20 | console.log('this is the chat response: ', response.data); 21 | for (let i = 0; i < response.data.length; i++) { 22 | feed.push( 23 |
24 |

25 | 26 | {response.data[i].username}: 27 | {response.data[i].content} 28 |

29 |
30 | ); 31 | } 32 | addComment(feed); 33 | }) 34 | .catch(error => { 35 | console.log(error); 36 | }); 37 | 38 | // Enter key in the input box will send messages 39 | document.getElementById('chatText').addEventListener('keyup', event => { 40 | event.preventDefault(); 41 | if (event.keyCode === 13) { 42 | document.getElementById('send').click(); 43 | } 44 | }); 45 | 46 | // Join socket when component mounts 47 | socket.emit('join_room', `chat${roomId}`); 48 | }, []); 49 | 50 | // Click sends message to the server socket for processing 51 | const handleClick = () => { 52 | const currentMessage = document.getElementById('chatText').value; 53 | document.getElementById('chatText').value = ''; 54 | 55 | socket.emit('chat', { 56 | room: `chat${roomId}`, 57 | message: currentMessage, 58 | uuid, 59 | }); 60 | }; 61 | 62 | // Listen to incoming chats, update state with new comments 63 | socket.on('chat', data => { 64 | // console.log('Incoming message: ', data); 65 | // console.log('this is what comments looks like: ', comments); 66 | addComment( 67 | [ 68 |
69 |

70 | 71 | {data.username}: 72 | {data.message} 73 | {new Date().toLocaleTimeString()} 74 |

75 |
, 76 | ].concat(comments) 77 | ); 78 | }); 79 | 80 | return ( 81 |
82 |
{comments}
83 | 84 | 87 |
88 | ); 89 | }; 90 | 91 | export default Chat; 92 | -------------------------------------------------------------------------------- /client/components/CreateNewRoomModal/index.css: -------------------------------------------------------------------------------- 1 | .modal-main { 2 | position:fixed; 3 | background: white; 4 | width: 80%; 5 | height: auto; 6 | top:50%; 7 | left:50%; 8 | transform: translate(-50%,-50%); 9 | } -------------------------------------------------------------------------------- /client/components/CreateNewRoomModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Cookies from 'js-cookie'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | const CreateNewRoomModal = () => { 6 | const [roomName, setRoomName] = useState(''); 7 | const history = useHistory(); 8 | 9 | const handleChange = e => { 10 | setRoomName(e.target.value); 11 | }; 12 | 13 | const handleClick = () => { 14 | const userId = Cookies.get('uuid'); 15 | const data = { userId, roomName }; 16 | fetch('/api/v1/rooms', { 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify(data), 22 | }) 23 | .then(response => response.json()) 24 | .then(response => { 25 | console.log('response is', response); 26 | history.push({ 27 | pathname: '/room', 28 | state: { isHost: true, roomInfo: response }, 29 | }); 30 | }) 31 | .catch(error => { 32 | console.error('Error:', error); 33 | }); 34 | }; 35 | 36 | return ( 37 |
38 |
39 |

Create New Room

40 | {/* Have input field, have a button that will grab that input field value */} 41 | 42 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default CreateNewRoomModal; 51 | -------------------------------------------------------------------------------- /client/components/DashboardPage/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Ubuntu Light', 'Century Gothic', sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | h1 { 7 | font-family: Arial, Helvetica, sans-serif; 8 | font-size: 40px !important; 9 | color: #ffffff; 10 | /* left: 5vh !important; */ 11 | width: 10vh; 12 | } 13 | .lobby { 14 | color: white !important; 15 | } 16 | .dashboard-page { 17 | background: #1f1f1f; 18 | display: inline-block; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | text-align: center; 23 | min-height: 100vh; 24 | width: 100%; 25 | margin: 0 auto; 26 | } 27 | button { 28 | background-color: #393232; 29 | display: inline-block; 30 | border-radius: 6px; 31 | color: rgb(63, 121, 150); 32 | text-align: center; 33 | font-size: 20px; 34 | padding: 3px; 35 | width: 70px; 36 | cursor: pointer; 37 | margin: 5px; 38 | transition: 200ms; 39 | } 40 | button:hover { 41 | background-color: #524f4f; 42 | } 43 | 44 | .dashboard-container { 45 | display: flex; 46 | flex-wrap: wrap; 47 | justify-content: space-evenly; 48 | justify-content: center; 49 | margin: 5vh 5vh; 50 | } 51 | 52 | .page-header { 53 | margin-top: 30px; 54 | margin-left: 5vh; 55 | margin-bottom: 30px; 56 | color: #323131; 57 | } 58 | 59 | .create-room { 60 | margin-top: 30px; 61 | margin-left: 40px; 62 | margin-bottom: 30px; 63 | } 64 | 65 | .dashboard-box { 66 | display: 1; 67 | background-color: #787373; 68 | /* background-image: url('https://images.unsplash.com/photo-1574494461754-de04dc403eec?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80'); 69 | background-repeat: no-repeat; 70 | background-size: cover; */ 71 | /* background: linear-gradient(0deg, rgba(8, 34, 34, 1) 0%, rgba(58, 110, 136, 1) 100%); */ 72 | border: 1px solid; 73 | border-radius: 5px; 74 | padding: 0px 5px; 75 | padding-top: 120px; 76 | /* height: 20vh; */ 77 | width: 20vh; 78 | height: 15vh; 79 | margin: 20px 20px !important; 80 | float: left; 81 | font-size: 1.5em; 82 | /* box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.2); */ 83 | } 84 | -------------------------------------------------------------------------------- /client/components/DashboardPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import { Modal } from '@material-ui/core'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import CreateNewRoomModal from '../CreateNewRoomModal'; 6 | import RoomOption from '../RoomOption'; 7 | import './index.css'; 8 | /** 9 | * render component: CreateRoom Modal 10 | * render: button create room name 11 | */ 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | modal: { 15 | display: 'flex', 16 | alignItems: 'center', 17 | justifyContent: 'center', 18 | }, 19 | paper: { 20 | backgroundColor: theme.palette.background.paper, 21 | border: '2px solid #000', 22 | boxShadow: theme.shadows[5], 23 | padding: theme.spacing(2, 4, 3), 24 | }, 25 | })); 26 | 27 | const DashboardPage = () => { 28 | // state hook for a new room name 29 | // const [showCreateNewRoomModal, setShowCreateNewRoomModal] = useState(false); 30 | 31 | const classes = useStyles(); 32 | const [allRooms, setAllRooms] = useState([]); 33 | const [open, setOpen] = useState(false); 34 | 35 | const toggleOpen = e => { 36 | e.preventDefault(); 37 | setOpen(!open); 38 | }; 39 | 40 | useEffect(() => { 41 | axios 42 | .get('/api/v1/rooms') 43 | .then(function (response) { 44 | const rooms = []; 45 | for (let i = 0; i < response.data.length; i += 1) { 46 | console.log('The Room Data', response.data[i]); 47 | if (response.data[i].active) { 48 | rooms.push( 49 |
50 | 51 |
52 | ); 53 | } 54 | } 55 | setAllRooms(rooms); 56 | }) 57 | .catch(function (error) { 58 | console.log(error); 59 | }); 60 | }, []); 61 | 62 | return ( 63 |
64 |
65 |

Lobby

66 |
67 |
68 | 71 |
72 | 73 |
74 | 75 |
76 |
77 |
{allRooms}
78 |
79 | ); 80 | }; 81 | 82 | export default DashboardPage; 83 | -------------------------------------------------------------------------------- /client/components/HostDisableRoomButton/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/client/components/HostDisableRoomButton/index.css -------------------------------------------------------------------------------- /client/components/HostDisableRoomButton/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import Cookies from 'js-cookie'; 4 | 5 | const HostDisableRoomButton = props => { 6 | const history = useHistory(); 7 | 8 | function handleClick(e) { 9 | e.preventDefault(); 10 | // Disable Room 11 | // Go Back to DashBoard 12 | const host = Cookies.get('uuid'); 13 | const roomId = props.roomId; 14 | const data = { host, roomId }; 15 | fetch('/api/v1/rooms', { 16 | method: 'PUT', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify(data), 21 | }) 22 | .then(response => { 23 | console.log('Room set to inactive'); 24 | history.goBack(); 25 | }) 26 | .catch(error => { 27 | console.error('Error:', error); 28 | }); 29 | } 30 | 31 | return ( 32 | 35 | ); 36 | }; 37 | 38 | export default HostDisableRoomButton; 39 | -------------------------------------------------------------------------------- /client/components/LoginPage/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | .spotifyIMG { 6 | width: 100px !important; 7 | height: auto !important; 8 | /* margin-top: 5vh; */ 9 | transition: 200ms; 10 | } 11 | .spotifyIMG:hover { 12 | background-color: black; 13 | } 14 | .login { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | text-align: center; 20 | min-height: 100vh; 21 | background-image: url('https://images.unsplash.com/photo-1420161900862-9a86fa1f5c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80'); 22 | background-repeat: no-repeat; 23 | background-size: cover; 24 | color: black; 25 | } 26 | .login-border { 27 | border: solid 2px #2a3439; 28 | padding: 100px 30px; 29 | background-color: #808080; 30 | border-radius: 20px; 31 | width: 20vh; 32 | /* text-align: center; */ 33 | } 34 | -------------------------------------------------------------------------------- /client/components/LoginPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | 4 | const LoginPage = () => ( 5 |
6 |
7 | {/*

Login with Spotify!

*/} 8 | 9 | 10 | not rendering 15 | 16 |
17 |
18 | ); 19 | 20 | export default LoginPage; 21 | -------------------------------------------------------------------------------- /client/components/PlaybackControls/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaudio/Pandaudio/6c1462afbf0e4643c3f3d88afcfafc9995646085/client/components/PlaybackControls/index.css -------------------------------------------------------------------------------- /client/components/PlaybackControls/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const PlaybackControls = props => { 4 | const [songIsPaused, setPause] = useState(true); 5 | 6 | function handleClick(e) { 7 | const { playSong, pauseSong } = props; 8 | 9 | e.preventDefault(); 10 | setPause(!songIsPaused); 11 | // Functionality to pause song 12 | 13 | if (songIsPaused) { 14 | playSong(); 15 | } else { 16 | pauseSong(); 17 | } 18 | } 19 | 20 | return ( 21 | 24 | ); 25 | }; 26 | 27 | export default PlaybackControls; 28 | -------------------------------------------------------------------------------- /client/components/RoomOption/index.css: -------------------------------------------------------------------------------- 1 | /* .roomOption { 2 | background-color: grey; 3 | } 4 | s { 5 | background-color: grey; 6 | } */ 7 | .roomName { 8 | color: white; 9 | } 10 | -------------------------------------------------------------------------------- /client/components/RoomOption/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import Cookies from 'js-cookie'; 4 | import './index.css'; 5 | 6 | const RoomOption = props => { 7 | const history = useHistory(); 8 | const { room } = props; 9 | const uuid = Cookies.get('uuid'); 10 | 11 | function handleClick(e) { 12 | const { 13 | room: { host }, 14 | } = props; 15 | 16 | e.preventDefault(); 17 | history.push({ 18 | pathname: '/room', 19 | state: { isHost: host === uuid ? true : false, roomInfo: room }, 20 | }); 21 | } 22 | 23 | return ( 24 |
25 |

{room.room_name}

26 | 29 |
30 | ); 31 | }; 32 | 33 | export default RoomOption; 34 | -------------------------------------------------------------------------------- /client/components/RoomPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal } from '@material-ui/core'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import { useSelector, useStore, useDispatch } from 'react-redux'; 5 | import moment from 'moment'; 6 | import PlaybackControls from '../PlaybackControls'; 7 | import SongSearch from '../SongSearch'; 8 | import HostDisableRoomButton from '../HostDisableRoomButton'; 9 | import Chat from '../Chat'; 10 | import { SONG_QUEUE_UPDATE } from '../../store/action_types/songQueue'; 11 | import './index.scss'; 12 | 13 | const URL = process.env.NODE_ENV === 'production' ? '/' : 'http://localhost:3000'; 14 | const socket = io.connect(URL); 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | modal: { 18 | display: 'flex', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }, 22 | paper: { 23 | backgroundColor: '#151515', 24 | border: '2px solid #000', 25 | boxShadow: theme.shadows[5], 26 | // padding: theme.spacing(2, 4, 3), 27 | }, 28 | })); 29 | 30 | const RoomPage = props => { 31 | const { 32 | location: { 33 | state: { isHost, roomInfo }, 34 | }, 35 | } = props; 36 | 37 | const classes = useStyles(); 38 | const [open, setOpen] = useState(false); 39 | const [songQueueReady, setSongQueueReady] = useState(false); 40 | const [initialPlay, setInitialPlay] = useState(false); 41 | const dispatch = useDispatch(); 42 | const store = useStore(); 43 | const playerState = useSelector(state => state.player); 44 | const songQueueState = useSelector(state => state.songQueue); 45 | // hard coded pokemon song 46 | 47 | useEffect(() => { 48 | // setup fetch data method when component loads intially 49 | setup(); 50 | 51 | // join song room when page is loaded is loaded 52 | socket.emit('join_room', `song${roomInfo.id}`); 53 | 54 | // onload of room post message to websocket asking for the current song URI and time 55 | if (!isHost) { 56 | socket.emit('requestPlayInfo', { 57 | room: `song${roomInfo.id}`, 58 | targetGuest: socket.id, 59 | }); 60 | } 61 | 62 | if (isHost) { 63 | socket.on('requestPlayInfo', data => { 64 | // emit a special play message back to ONLY that guest requester 65 | console.log('host receive requests for play Info', data); 66 | getPlayerInfoAndEmit(window.globalSpotifyPlayer, data); 67 | }); 68 | } 69 | 70 | // add listeners of websocket to play a song at a certain time 71 | socket.on('play', data => { 72 | console.log('Incoming play message: ', data); 73 | 74 | // only play song if the targetGuest is my own socket.id or if its falsy (broadcast to everyone to play) 75 | if (data.targetGuest === socket.id || !data.targetGuest) { 76 | playSong(window.globalSpotifyPlayer, data.spotify_uris, data.start_time); 77 | } 78 | }); 79 | 80 | // add listener of websocket to pause a song 81 | socket.on('pause', data => { 82 | console.log('Incoming message: ', data); 83 | pauseSong(window.globalSpotifyPlayer); 84 | }); 85 | }, []); 86 | 87 | const setup = () => { 88 | // async call to get all songs and dispatch to songQueue store 89 | 90 | fetch(`/api/v1/rooms/${roomInfo.id}/songs`, { 91 | method: 'GET', 92 | headers: { 93 | 'Content-Type': 'appplication/json', 94 | }, 95 | }) 96 | .then(response => response.json()) 97 | .then(data => { 98 | console.log('grabbed all the songs from db', data); 99 | dispatch({ type: SONG_QUEUE_UPDATE, payload: data }); 100 | setSongQueueReady(true); 101 | }); 102 | }; 103 | 104 | const getPlayerInfoAndEmit = (player, requestData) => { 105 | const { 106 | _options: { getOAuthToken }, 107 | } = player; 108 | 109 | getOAuthToken(access_token => { 110 | fetch(`https://api.spotify.com/v1/me/player`, { 111 | method: 'GET', 112 | headers: { 113 | 'Content-Type': 'application/json', 114 | Authorization: `Bearer ${access_token}`, 115 | }, 116 | }) 117 | .then(response => response.json()) 118 | .then(playerInfo => { 119 | const { 120 | item: { uri }, 121 | progress_ms, 122 | } = playerInfo; 123 | 124 | const trackWindow = store.getState().player.data.track_window; 125 | const currentTrack = trackWindow.current_track; 126 | const nextTracks = trackWindow.next_tracks; 127 | const tracks = [currentTrack, ...nextTracks]; 128 | 129 | socket.emit('play', { 130 | room: `song${roomInfo.id}`, 131 | spotify_uris: tracks.map(track => track.uri), 132 | start_time: progress_ms, 133 | targetGuest: requestData.targetGuest, 134 | }); 135 | }); 136 | }); 137 | }; 138 | 139 | // helper to play a song 140 | const playSong = (player, spotify_uris, start_time) => { 141 | // function to play song from spotify API 142 | const play = ({ 143 | spotify_uris, 144 | playerInstance: { 145 | _options: { getOAuthToken, id }, 146 | }, 147 | start_time = 0, 148 | }) => { 149 | getOAuthToken(access_token => { 150 | console.log('we are in the get oauth', access_token, id); 151 | console.log('this is the spotify uri', spotify_uris); 152 | fetch(`https://api.spotify.com/v1/me/player/play?device_id=${id}`, { 153 | method: 'PUT', 154 | body: JSON.stringify({ uris: spotify_uris, position_ms: start_time }), 155 | headers: { 156 | 'Content-Type': 'application/json', 157 | Authorization: `Bearer ${access_token}`, 158 | }, 159 | }); 160 | }); 161 | }; 162 | 163 | console.log('before we call play', player); 164 | 165 | play({ 166 | playerInstance: player, 167 | spotify_uris, 168 | start_time, 169 | }); 170 | }; 171 | 172 | const pauseSong = player => { 173 | player.pause().then(() => { 174 | console.log('Paused!'); 175 | }); 176 | }; 177 | 178 | const handlePlay = e => { 179 | let uris; 180 | 181 | if (!initialPlay) { 182 | uris = songQueueState.data.map(song => song.uri); 183 | setInitialPlay(true); 184 | } else { 185 | const trackWindow = store.getState().player.data.track_window; 186 | const currentTrack = trackWindow.current_track; 187 | const nextTracks = trackWindow.next_tracks; 188 | const tracks = [currentTrack, ...nextTracks]; 189 | 190 | uris = tracks.map(track => track.uri); 191 | } 192 | 193 | socket.emit('play', { 194 | room: `song${roomInfo.id}`, 195 | spotify_uris: uris, 196 | start_time: playerState.data.position || 0, 197 | }); 198 | }; 199 | 200 | const handlePause = e => { 201 | socket.emit('pause', { 202 | room: `song${roomInfo.id}`, 203 | }); 204 | }; 205 | 206 | const toggleOpen = e => { 207 | e.preventDefault(); 208 | setOpen(!open); 209 | }; 210 | 211 | const { location } = props; 212 | const { track_window } = playerState.data; 213 | 214 | let songName, artists, albumName, albumArt, isPaused; 215 | 216 | if (track_window) { 217 | songName = track_window.current_track.name; 218 | artists = track_window.current_track.artists; 219 | albumName = track_window.current_track.album.name; 220 | albumArt = track_window.current_track.album.images[0].url; 221 | isPaused = playerState.data.paused; 222 | } 223 | 224 | return ( 225 |
226 |
227 | {location.state.isHost ? ( 228 |
229 | 232 | 233 | 234 |
235 | 236 |
237 |
238 |
239 | ) : null} 240 |
241 |

{roomInfo.room_name}

242 |

Back to Lobby

243 |
244 |
245 |
246 | {albumArt && } 247 |
248 |
249 |
250 |

{!track_window ? 'Waiting for tunes' : isPaused ? 'Paused' : 'Now Playing'}

251 |
252 |
253 |

{songName || 'Song Name'}

254 |

{albumName || 'Album Name'}

255 |

0:00 / 2:30

256 |
257 |
258 | {isHost && playerState.ready && songQueueReady ? ( 259 | { 261 | handlePlay(); 262 | }} 263 | pauseSong={() => { 264 | handlePause(); 265 | }} 266 | /> 267 | ) : null} 268 |
269 |
270 |
271 |
272 |
273 |
274 |

Song Queue

275 |
276 |
277 | 278 |
279 |
280 |
281 | ); 282 | }; 283 | 284 | export default RoomPage; 285 | -------------------------------------------------------------------------------- /client/components/RoomPage/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../stylesheets/constants.scss'; 2 | 3 | .room-page { 4 | background-color: #040404; 5 | height: 100vh; 6 | display: flex; 7 | 8 | .room-content { 9 | flex: 1; 10 | padding: 25px 50px; 11 | position: relative; 12 | 13 | h2 { 14 | font-size: 250%; 15 | margin: 0 0 10px; 16 | font-weight: 600; 17 | } 18 | 19 | p { 20 | color: $yellow; 21 | font-size: 90%; 22 | position: relative; 23 | } 24 | 25 | .room-player { 26 | position: absolute; 27 | top: 50%; 28 | left: 50%; 29 | transform: translate(-45%, -50%); 30 | @include centerWithFlex($align: stretch); 31 | 32 | .player-cover { 33 | width: 250px; 34 | height: 250px; 35 | border-radius: 0; 36 | border: thin solid black; 37 | 38 | img { 39 | height: 100% 40 | } 41 | } 42 | 43 | .player-content { 44 | @include centerWithFlex($direction: column, $justify: space-evenly, $align: flex-start); 45 | padding: 0 30px; 46 | width: 300px; 47 | 48 | .player-playing {} 49 | 50 | .player-details { 51 | height: 70px; 52 | display: flex; 53 | flex-direction: column; 54 | justify-content: space-between; 55 | 56 | h3 { 57 | font-size: 145%; 58 | color: white; 59 | } 60 | } 61 | 62 | .player-btn { 63 | button { 64 | color: white; 65 | border: thin solid white; 66 | border-radius: 0; 67 | background-color: transparent; 68 | transition: 0.1s; 69 | 70 | &:hover { 71 | background-color: lighten(black, 25%); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | .sidebar { 80 | width: 30%; 81 | height: 100%; 82 | } 83 | } -------------------------------------------------------------------------------- /client/components/SongOption/index.css: -------------------------------------------------------------------------------- 1 | .songOption { 2 | border: 1px solid rgba(255, 255, 255, 0.02); 3 | padding: 16px; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | background-color: rgba(255, 255, 255, 0.05); 8 | color: white; 9 | } 10 | 11 | .songOption-add { 12 | color: #fff; 13 | font-size: 12px; 14 | background-color: transparent; 15 | outline: none; 16 | border: none; 17 | font-weight: 600; 18 | padding: 0; 19 | } -------------------------------------------------------------------------------- /client/components/SongOption/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { SONG_QUEUE_ADD } from '../../store/action_types/songQueue'; 5 | import './index.css' 6 | 7 | const SongOption = props => { 8 | const { roomId, track, artist, length, thumbnail, uri } = props; 9 | const dispatch = useDispatch(); 10 | 11 | function handleClick(e) { 12 | e.preventDefault(); 13 | const data = { roomId, track, artist, length, thumbnail, uri }; 14 | // Functionality to add song 15 | fetch(`/api/v1/rooms/${roomId}/songs`, { 16 | method: 'POST', // or 'PUT' 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify(data), 21 | }) 22 | .then(response => response.json()) 23 | .then(data => { 24 | console.log('Song Added to Queue!'); 25 | // dispatch response data to redux 26 | dispatch({type: SONG_QUEUE_ADD, payload: data}) 27 | 28 | //add to player queue 29 | window.globalSpotifyPlayer._options.getOAuthToken(access_token => { 30 | fetch(`https://api.spotify.com/v1/me/player/queue?uri=${data.uri}&device_id=${window.globalSpotifyPlayer._options.id}`, { 31 | method: 'POST', // or 'PUT' 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | Authorization: `Bearer ${access_token}`, 35 | }, 36 | }) 37 | .then(response => response.json()) 38 | .then(data => { 39 | console.log('added to player queue!!!') 40 | }) 41 | }) 42 | 43 | 44 | }) 45 | .catch(error => { 46 | console.error('Error:', error); 47 | }); 48 | } 49 | 50 | return ( 51 |
52 | {`${track} | ${artist} | ${length}`} 53 | 56 |
57 | ); 58 | }; 59 | 60 | SongOption.propTypes = { 61 | roomId: PropTypes.number.isRequired, 62 | track: PropTypes.string.isRequired, 63 | artist: PropTypes.string.isRequired, 64 | length: PropTypes.number.isRequired, 65 | thumbnail: PropTypes.string.isRequired, 66 | uri: PropTypes.string.isRequired, 67 | }; 68 | 69 | export default SongOption; 70 | -------------------------------------------------------------------------------- /client/components/SongSearch/index.css: -------------------------------------------------------------------------------- 1 | .songSearch-container { 2 | position: relative; 3 | display: flex; 4 | } 5 | 6 | .songSearch-container-dummy-X { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | position: absolute; 11 | top: -60px; 12 | width: 40px; 13 | height: 40px; 14 | border: 1px solid white; 15 | font-size: 18px; 16 | font-weight: 800; 17 | } 18 | 19 | .songSearch-results-container { 20 | box-sizing: border-box; 21 | width: 720px; 22 | min-height: 500px; 23 | padding: 24px; 24 | } 25 | 26 | .songSearch-header { 27 | color: white; 28 | margin: 0 0 24px 0; 29 | font-size: 2.0rem; 30 | } 31 | 32 | .songSearch-form { 33 | display: flex; 34 | padding: 16px; 35 | background-color: rgba(0, 0, 0, 0.3); 36 | border-radius: 5px; 37 | } 38 | 39 | .songSearch-bar { 40 | outline: none; 41 | border: none; 42 | flex: 1 0 80%; 43 | background-color: transparent; 44 | font-size: 16px; 45 | } 46 | 47 | .songSearch-queue-container { 48 | padding: 24px; 49 | width: 300px; 50 | min-height: 100%; 51 | max-height: 100%; 52 | overflow: scroll; 53 | background-color: rgba(255, 255, 255, 0.03); 54 | } 55 | 56 | .songSearch-queue-header { 57 | font-size: 1.5rem; 58 | color: white; 59 | } 60 | 61 | .songSearch-queue-linebreak { 62 | width: 100%; 63 | height: 1px; 64 | background-color: rgba(255, 255, 255, 0.1); 65 | margin: 16px 0; 66 | } 67 | 68 | .songSearch-queue-item { 69 | margin: 16px 0; 70 | } 71 | 72 | .songSearch-submit { 73 | background-color: transparent; 74 | outline: none; 75 | border: none; 76 | color: white; 77 | font-weight: 700; 78 | } -------------------------------------------------------------------------------- /client/components/SongSearch/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Cookies from 'js-cookie'; 3 | import SongOption from '../SongOption'; 4 | import './index.css'; 5 | import { useSelector } from 'react-redux'; 6 | 7 | const SongSearch = props => { 8 | const [songName, setSongName] = useState(''); 9 | const [songResults, setSongResults] = useState([]); 10 | const songQueue = useSelector(state => state.songQueue); 11 | 12 | // useEffect to pass songs down to SongOption component 13 | useEffect(() => {}, [songResults]); 14 | 15 | function handleChange(e) { 16 | setSongName(e.target.value); 17 | } 18 | 19 | function handleSubmit(e) { 20 | e.preventDefault(); 21 | // Functionality to search for song 22 | const accessToken = Cookies.get('accessToken'); 23 | const data = { token: accessToken, searchQuery: songName }; 24 | 25 | fetch('/api/v1/spotify/songs', { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify(data), 31 | }) 32 | .then(response => response.json()) 33 | .then(response => { 34 | const songOptions = []; 35 | for (let i = 0; i < response.length; i += 1) { 36 | const track = response[i].name; 37 | const artist = response[i].artists[0].name; 38 | const length = Math.floor(response[i].duration_ms / 1000); 39 | const thumbnail = response[i].album.images[0].url; 40 | const uri = response[i].uri; 41 | songOptions.push( 42 | 51 | ); 52 | } 53 | setSongResults(songOptions); 54 | }) 55 | .catch(error => { 56 | console.error('Error:', error); 57 | }); 58 | } 59 | 60 | return ( 61 |
62 |
X
63 |
64 |

Browse Music

65 |
66 | 74 | 75 |
76 | {songResults} 77 |
78 |
79 |

Queue

80 |

81 | { 82 | songQueue.data.map(song => { 83 | return

{song.track}

84 | }) 85 | } 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default SongSearch; 92 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pandaudio 7 | 8 | 9 | 10 |
11 | 12 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { Provider } from 'react-redux'; 6 | import App from './App'; 7 | import store from './store'; 8 | import './stylesheets/index.css'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('app') 17 | ); 18 | -------------------------------------------------------------------------------- /client/store/action_types/player.js: -------------------------------------------------------------------------------- 1 | export const PLAYER_STATE_UPDATE = "PLAYER_STATE_UPDATE"; 2 | export const PLAYER_READY_UPDATE = "PLAYER_READY_UPDATE"; -------------------------------------------------------------------------------- /client/store/action_types/songQueue.js: -------------------------------------------------------------------------------- 1 | export const SONG_QUEUE_UPDATE = "SONG_QUEUE_UPDATE" 2 | export const SONG_QUEUE_ADD= "SONG_QUEUE_ADD" -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import thunk from 'redux-thunk'; 4 | import rootReducer from './reducers'; 5 | 6 | const initialState = {}; 7 | 8 | const middleware = [thunk]; 9 | 10 | /** 11 | * create store 12 | * @param rootReducer all combined 13 | * @param initialState 14 | * @param middleware to be composed using redux-devtools-extension 15 | */ 16 | const store = createStore( 17 | rootReducer, 18 | initialState, 19 | composeWithDevTools(applyMiddleware(...middleware)) 20 | ); 21 | 22 | export default store; -------------------------------------------------------------------------------- /client/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import playerReducer from './player' 3 | import songQueueReducer from './songQueue' 4 | 5 | export default combineReducers({ 6 | player: playerReducer, 7 | songQueue: songQueueReducer 8 | }); -------------------------------------------------------------------------------- /client/store/reducers/player.js: -------------------------------------------------------------------------------- 1 | import { PLAYER_STATE_UPDATE, PLAYER_READY_UPDATE } from '../action_types/player' 2 | 3 | const DEFAULT_STATE = { 4 | ready: false, 5 | data: {} 6 | } 7 | 8 | export default (state = DEFAULT_STATE, action) => { 9 | switch(action.type) { 10 | case PLAYER_STATE_UPDATE: 11 | return { 12 | ...state, 13 | data: { 14 | ...state.data, 15 | ...action.payload, 16 | }, 17 | } 18 | case PLAYER_READY_UPDATE: 19 | return { 20 | ...state, 21 | ready: action.payload, 22 | } 23 | default: 24 | return state; 25 | } 26 | } -------------------------------------------------------------------------------- /client/store/reducers/songQueue.js: -------------------------------------------------------------------------------- 1 | import { SONG_QUEUE_UPDATE, SONG_QUEUE_ADD } from '../action_types/songQueue' 2 | 3 | const DEFAULT_STATE = { 4 | data: [], 5 | } 6 | 7 | export default (state = DEFAULT_STATE, action) => { 8 | switch(action.type) { 9 | case SONG_QUEUE_UPDATE: 10 | return { 11 | ...state, 12 | data: [ 13 | ...state.data, 14 | ...action.payload, 15 | ], 16 | } 17 | case SONG_QUEUE_ADD: 18 | return { 19 | ...state, 20 | data: [ 21 | ...state.data, 22 | action.payload, 23 | ], 24 | } 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/stylesheets/constants.scss: -------------------------------------------------------------------------------- 1 | $yellow: #FFD601; 2 | 3 | @mixin centerWithFlex($direction: row, $align: center, $justify: center) { 4 | display: flex; 5 | flex-direction: $direction; 6 | align-items: $align; 7 | justify-content: $justify; 8 | } 9 | -------------------------------------------------------------------------------- /client/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-size: 1em; 11 | font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pandaudio", 3 | "version": "1.0.0", 4 | "description": "Spotify zoom rooms.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "NODE_ENV=production node server/server.js", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "concurrently --kill-others \"NODE_ENV=development webpack-dev-server\" \"nodemon server/server.js\"", 11 | "server": "nodemon server/server.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/pandaudio/Pandaudio.git" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/pandaudio/Pandaudio/issues" 21 | }, 22 | "homepage": "https://github.com/pandaudio/Pandaudio#readme", 23 | "dependencies": { 24 | "@material-ui/core": "^4.11.0", 25 | "axios": "^0.19.2", 26 | "cookie-parser": "^1.4.5", 27 | "dotenv": "^8.2.0", 28 | "express": "^4.17.1", 29 | "http": "0.0.1-security", 30 | "js-cookie": "^2.2.1", 31 | "moment": "^2.27.0", 32 | "node": "^14.5.0", 33 | "passport": "^0.4.1", 34 | "passport-spotify": "^1.1.0", 35 | "pg": "^8.3.0", 36 | "prop-types": "^15.7.2", 37 | "react": "^16.13.1", 38 | "react-dom": "^16.13.1", 39 | "react-redux": "^7.2.0", 40 | "react-router": "^5.2.0", 41 | "react-router-dom": "^5.2.0", 42 | "redux": "^4.0.5", 43 | "redux-thunk": "^2.3.0", 44 | "socket.io": "^2.3.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.10.5", 48 | "@babel/plugin-proposal-class-properties": "^7.10.4", 49 | "@babel/preset-env": "^7.10.4", 50 | "@babel/preset-react": "^7.10.4", 51 | "babel-eslint": "^10.1.0", 52 | "babel-loader": "^8.1.0", 53 | "concurrently": "^5.2.0", 54 | "cross-env": "^7.0.2", 55 | "css-loader": "^3.6.0", 56 | "eslint": "^7.5.0", 57 | "eslint-config-airbnb": "^18.2.0", 58 | "eslint-config-prettier": "^6.11.0", 59 | "eslint-plugin-import": "^2.22.0", 60 | "eslint-plugin-jsx-a11y": "^6.3.1", 61 | "eslint-plugin-prettier": "^3.1.4", 62 | "eslint-plugin-react": "^7.20.3", 63 | "eslint-plugin-react-hooks": "^4.0.8", 64 | "file-loader": "^6.0.0", 65 | "image-webpack-loader": "^6.0.0", 66 | "jest": "^26.1.0", 67 | "jest-config": "^26.1.0", 68 | "node-sass": "^4.14.1", 69 | "nodemon": "^2.0.4", 70 | "prettier": "^2.0.5", 71 | "redux-devtools-extension": "^2.13.8", 72 | "sass-loader": "^9.0.2", 73 | "style-loader": "^1.2.1", 74 | "webpack": "^4.43.0", 75 | "webpack-cli": "^3.3.12", 76 | "webpack-dev-server": "^3.11.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/main.sql: -------------------------------------------------------------------------------- 1 | -- Execute the following command to run this script on the psql shell => \i 2 | 3 | CREATE TABLE users ( 4 | id UUID PRIMARY KEY, 5 | spotify_id VARCHAR(32) NOT NULL, 6 | username VARCHAR(32) NOT NULL, 7 | thumbnail VARCHAR(2100) NOT NULL, 8 | created_at TIME DEFAULT CURRENT_TIMESTAMP 9 | ); 10 | 11 | CREATE TABLE rooms ( 12 | id SERIAL PRIMARY KEY, 13 | room_name VARCHAR(24) NOT NULL UNIQUE, 14 | host UUID REFERENCES users(id) NOT NULL, 15 | active BOOLEAN DEFAULT 'true', 16 | created_at TIME DEFAULT CURRENT_TIMESTAMP 17 | ); 18 | 19 | -- Example data 20 | -- INSERT INTO users(id, spotify_id, username, thumbnail) 21 | -- VALUES('d827e187-d1b4-40f5-8d72-fdd03337b912', 'abc123', 'myusername', 'aws.s3.create'); 22 | 23 | -- INSERT INTO rooms(id, roomname, host) 24 | -- VALUES(uuid_generate_v4(), 'myroom', 'd827e187-d1b4-40f5-8d72-fdd03337b912'); 25 | 26 | -- CREATE TABLE IF NOT EXISTS songs${roomId} ( 27 | -- id SERIAL PRIMARY KEY, 28 | -- track VARCHAR(50), 29 | -- artist VARCHAR(50), 30 | -- length INTEGER, 31 | -- thumbnail VARCHAR(100), 32 | -- uri VARCHAR(100) 33 | -- ); -------------------------------------------------------------------------------- /server/config/passport-setup.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | require('dotenv').config(); 3 | 4 | const SpotifyStrategy = require('passport-spotify').Strategy; 5 | 6 | const db = require('../models/roomModels.js'); 7 | 8 | const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } = process.env; 9 | 10 | passport.use( 11 | new SpotifyStrategy( 12 | { 13 | clientID: SPOTIFY_CLIENT_ID, 14 | clientSecret: SPOTIFY_CLIENT_SECRET, 15 | callbackURL: '/auth/spotify/callback', 16 | }, 17 | function (accessToken, refreshToken, expires_in, profile, done) { 18 | const { id, display_name, images } = profile._json; 19 | 20 | const body = { accessToken }; 21 | 22 | const selectQuery = ` 23 | SELECT * FROM users 24 | WHERE spotify_id=$1`; 25 | 26 | const insertQuery = ` 27 | INSERT INTO users (id, spotify_id, username, thumbnail) 28 | VALUES (uuid_generate_v4(), $1, $2, $3) 29 | RETURNING *`; 30 | 31 | db.query(selectQuery, [id]) 32 | .then(data => { 33 | // User exists in database 34 | if (data.rows.length) { 35 | body.userId = data.rows[0].id; 36 | return done(null, body); 37 | } 38 | 39 | // User does not exist, add user to database 40 | db.query(insertQuery, [id, display_name, images[0] ? images[0].url : null]) 41 | .then(user => { 42 | body.userId = user.rows[0].id; 43 | return done(null, body); 44 | }) 45 | .catch(err => console.log('INSERT QUERY', err)); 46 | }) 47 | .catch(err => console.log('SELECT QUERY', err)); 48 | } 49 | ) 50 | ); 51 | 52 | /** Configure Passport authenticated session persistence. 53 | * 54 | * In order to restore authentication state across HTTP requests, Passport needs 55 | * to serialize users into and deserialize users out of the session. We 56 | * supply the user ID when serializing, and query the user record by ID 57 | * from the database when deserializing. 58 | **/ 59 | 60 | passport.serializeUser(function (user, done) { 61 | // console.log('IN SERIALIZE ', user); 62 | done(null, user); 63 | }); 64 | 65 | passport.deserializeUser(function (obj, done) { 66 | const findUserQuery = `SELECT * FROM users WHERE id = $1`; 67 | db.query(findUserQuery, [id]).then(user => { 68 | done(null, user); // done is used to progress to the next middleware 69 | }); 70 | }); 71 | 72 | module.exports = passport.deserializeUser; 73 | -------------------------------------------------------------------------------- /server/controllers/apiController.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /** 4 | * Controller for interactions with the Spotify API 5 | */ 6 | const apiController = {}; 7 | 8 | /** 9 | * Search for a particular song on the spotify API 10 | */ 11 | apiController.search = async (req, res, next) => { 12 | try { 13 | const { token } = req.body; 14 | let { searchQuery } = req.body; 15 | 16 | // Replace all spaces with '%20' 17 | searchQuery = searchQuery.replace(/\s/g, '%20'); 18 | 19 | const result = await axios.get( 20 | `https://api.spotify.com/v1/search?q=${searchQuery}&type=track&market=US&limit=5`, 21 | { 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | Authorization: `Bearer ${token}`, 25 | }, 26 | } 27 | ); 28 | 29 | console.log(result); 30 | 31 | res.locals.searchResult = result.data.tracks.items; 32 | 33 | return next(); 34 | 35 | // Catch errors 36 | } catch ({ message }) { 37 | return next({ 38 | log: 'Error in apiController.search', 39 | message, 40 | }); 41 | } 42 | }; 43 | 44 | module.exports = apiController; 45 | -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const authController = {}; 2 | 3 | authController.getAccessToken = (req, res, next) => { 4 | const { accessToken } = req.cookies; 5 | res.locals.accessToken = accessToken; 6 | next(); 7 | }; 8 | 9 | authController.saveAccessToken = (req, res, next) => { 10 | const { user } = req; 11 | 12 | res.cookie('accessToken', user.accessToken, { maxAge: 36000000 }); 13 | res.cookie('uuid', user.userId, { maxAge: 36000000 }); 14 | next(); 15 | }; 16 | 17 | module.exports = authController; 18 | -------------------------------------------------------------------------------- /server/controllers/chatController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/roomModels'); 2 | 3 | /** 4 | * Controller for interactions with room tables 5 | */ 6 | const chatController = {}; 7 | 8 | let query = ''; 9 | /** 10 | * Create chat table for room if none exists 11 | */ 12 | chatController.createChat = async (req, res, next) => { 13 | try { 14 | const { id } = res.locals.room; 15 | 16 | query = ` 17 | CREATE TABLE IF NOT EXISTS chat${id} ( 18 | id SERIAL PRIMARY KEY, 19 | content VARCHAR(255), 20 | owner VARCHAR(50), 21 | created_at TIMESTAMP default now() 22 | );`; 23 | 24 | await db.query(query); 25 | return next(); 26 | } catch ({ message }) { 27 | return next({ 28 | log: 'Error in chat.createChat', 29 | status: 500, 30 | message, 31 | }); 32 | } 33 | }; 34 | 35 | /** 36 | * Add chat message to chat table based on room id 37 | */ 38 | chatController.addMessage = async (req, res, next) => { 39 | try { 40 | const { roomId } = req.params; 41 | const { content, owner } = req.body; 42 | 43 | query = `INSERT INTO chat${roomId} (content, owner) VALUES ($1, $2) RETURNING *`; 44 | 45 | const result = await db.query(query, [content, owner]); 46 | 47 | res.locals.chatMessage = result.rows[0]; 48 | 49 | return next(); 50 | } catch ({ message }) { 51 | return next({ 52 | log: 'Error in chat.addMessage', 53 | status: 500, 54 | message, 55 | }); 56 | } 57 | }; 58 | 59 | /** 60 | * Get chat history (up to 50 messages) from roomId 61 | */ 62 | chatController.getAll = async (req, res, next) => { 63 | try { 64 | const { roomId } = req.params; 65 | query = `SELECT users.username, users.thumbnail, chat${roomId}.content, chat${roomId}.created_at FROM chat${roomId} INNER JOIN users ON CAST(chat${roomId}.owner as uuid)=users.id ORDER BY created_at DESC LIMIT 20`; 66 | 67 | const result = await db.query(query); 68 | 69 | res.locals.roomChat = result.rows; 70 | return next(); 71 | } catch ({ message }) { 72 | return next({ 73 | log: 'Error in chat.getAll', 74 | status: 500, 75 | message, 76 | }); 77 | } 78 | }; 79 | module.exports = chatController; 80 | -------------------------------------------------------------------------------- /server/controllers/roomController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/roomModels'); 2 | 3 | /** 4 | * Controller for interactions with room tables 5 | */ 6 | const roomController = {}; 7 | 8 | let query = ''; 9 | /** 10 | * Get all rooms (from the rooms table) that are active (True) 11 | */ 12 | roomController.getAllActive = async (req, res, next) => { 13 | try { 14 | query = 'SELECT * FROM rooms WHERE rooms.active = True;'; 15 | const result = await db.query(query); 16 | res.locals.activeRooms = result.rows; 17 | return next(); 18 | } catch ({ message }) { 19 | return next({ 20 | log: 'Error in room.getAllActive', 21 | status: 500, 22 | message, 23 | }); 24 | } 25 | }; 26 | 27 | /** 28 | * Set the room (UUID - in the rooms table) to active (True) 29 | */ 30 | roomController.makeActive = async (req, res, next) => { 31 | try { 32 | const roomId = req.body.id; 33 | query = 'UPDATE rooms SET active = True WHERE uuid = $1'; 34 | const values = [roomId]; 35 | await db.query(query, values); 36 | return next(); 37 | } catch ({ message }) { 38 | return next({ 39 | log: 'Error in room.makeActive', 40 | status: 500, 41 | message, 42 | }); 43 | } 44 | }; 45 | /** 46 | * Set the room (UUID - in the rooms table) to inactive (False) 47 | */ 48 | roomController.makeInactive = async (req, res, next) => { 49 | try { 50 | const { host, roomId } = req.body; 51 | query = 'UPDATE rooms SET active = False WHERE host = $1 AND id = $2'; 52 | const values = [host, roomId]; 53 | await db.query(query, values); 54 | return next(); 55 | } catch ({ message }) { 56 | return next({ 57 | log: 'Error in room.makeActive', 58 | status: 500, 59 | message, 60 | }); 61 | } 62 | }; 63 | 64 | /** 65 | * Create new room entry in room table 66 | */ 67 | roomController.createRoom = async (req, res, next) => { 68 | try { 69 | const { roomName, userId } = req.body; 70 | 71 | query = 'INSERT INTO rooms (room_name, active, host) VALUES ($1, True, $2) RETURNING *'; 72 | const values = [roomName, userId]; 73 | const room = await db.query(query, values); 74 | 75 | res.locals.room = room.rows[0]; 76 | 77 | return next(); 78 | } catch ({ message }) { 79 | return next({ 80 | log: 'Error in room.createRoom', 81 | status: 500, 82 | message, 83 | }); 84 | } 85 | }; 86 | 87 | module.exports = roomController; 88 | -------------------------------------------------------------------------------- /server/controllers/songController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/roomModels'); 2 | 3 | /** 4 | * Controller for the interactions with the room-songs table 5 | * The name of the room songs table will be in the form of a UUID + 'songs' 6 | * where the UUID represents a room 7 | */ 8 | const songController = {}; 9 | 10 | /** 11 | * Create a room-songs table 12 | * @requires roomId Provided in request params 13 | */ 14 | songController.createTable = async (req, res, next) => { 15 | try { 16 | const { id } = res.locals.room; 17 | 18 | const query = ` 19 | CREATE TABLE IF NOT EXISTS songs${id} ( 20 | id SERIAL PRIMARY KEY, 21 | track VARCHAR(50), 22 | artist VARCHAR(50), 23 | length INTEGER, 24 | thumbnail VARCHAR(100), 25 | uri VARCHAR(100) 26 | );`; 27 | 28 | await db.query(query); 29 | 30 | return next(); 31 | 32 | // Catch errors 33 | } catch ({ message }) { 34 | return next({ 35 | log: 'Error in songController.createTable', 36 | message, 37 | }); 38 | } 39 | }; 40 | 41 | songController.getAll = async (req, res, next) => { 42 | 43 | const { roomId } = req.params; 44 | 45 | try { 46 | const query = ` 47 | SELECT * from songs${roomId} 48 | `; 49 | 50 | const results = await db.query(query); 51 | 52 | res.locals.roomSongs = results.rows; 53 | 54 | return next(); 55 | 56 | } catch({ message }) { 57 | return next({ 58 | log: 'Error in songController.getAll', 59 | message, 60 | }) 61 | } 62 | } 63 | 64 | /** 65 | * Insert a new entry for a song added to the room-songs table 66 | * @requires roomId {string} UUID provided in request params 67 | * @requires track {string} The name of the song 68 | * @requires artist {string} The track artists 69 | * @requires length {integer} The length of the song in seconds () 70 | * @requires thumbnail {string} The url of the song cover art 71 | * @requires uri {string} The Spotify uri of the song 72 | */ 73 | songController.addSong = async (req, res, next) => { 74 | try { 75 | const { roomId } = req.params; 76 | const { track, artist, length, thumbnail, uri } = req.body; 77 | 78 | const query = ` 79 | INSERT INTO songs${roomId} (track, artist, length, thumbnail, uri) 80 | VALUES ($1, $2, $3, $4, $5) 81 | RETURNING *`; 82 | 83 | const result = await db.query(query, [track, artist, length, thumbnail, uri]); 84 | 85 | res.locals.addedSong = result.rows[0]; 86 | 87 | return next(); 88 | 89 | // Catch errors 90 | } catch ({ message }) { 91 | return next({ 92 | log: 'Error in songController.addSong', 93 | message, 94 | }); 95 | } 96 | }; 97 | 98 | /** 99 | * Remove a song from the room-songs table 100 | * @requires roomId {string} UUID provided in request params 101 | * @requires songId {integer} The id of the song in the room-songs table 102 | */ 103 | songController.removeSong = async (req, res, next) => { 104 | try { 105 | const { roomId, songId } = req.params; 106 | 107 | const query = ` 108 | DELETE FROM songs${roomId} 109 | WHERE id = $1 110 | RETURNING *`; 111 | 112 | const result = await db.query(query, [parseInt(songId)]); 113 | 114 | res.locals.removedSong = result.rows[0]; 115 | 116 | return next(); 117 | 118 | // Catch errors 119 | } catch ({ message }) { 120 | return next({ 121 | log: 'Error in songController.removeSong', 122 | message, 123 | }); 124 | } 125 | }; 126 | module.exports = songController; 127 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/roomModels'); 2 | 3 | const userController = {}; 4 | 5 | /** 6 | * Retrieves and stores the user username and thumbnail to local variables 7 | * @requires userId The userId from the users table 8 | */ 9 | userController.getUserInfo = async (req, res, next) => { 10 | try { 11 | const { userId } = req.cookies; 12 | 13 | const query = ` 14 | SELECT username, thumbnail FROM users 15 | WHERE id = $1`; 16 | 17 | const result = await db.query(query, [userId]); 18 | 19 | // Save username to local variables 20 | res.locals.username = result.rows[0].username; 21 | res.locals.thumbnail = result.rows[0].thumbnail; 22 | 23 | return next(); 24 | 25 | // Catch errors 26 | } catch ({ message }) { 27 | return next({ 28 | log: 'Error in userController.getUsername', 29 | message, 30 | }); 31 | } 32 | }; 33 | 34 | module.exports = userController; 35 | -------------------------------------------------------------------------------- /server/models/roomModels.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | 4 | const pool = new Pool({ 5 | connectionString: process.env.PG_URI, 6 | }); 7 | 8 | module.exports = { 9 | query: (text, params, callback) => { 10 | console.log('executed query', text); 11 | return pool.query(text, params, callback); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const roomController = require('../controllers/roomController'); 4 | const songController = require('../controllers/songController'); 5 | const apiController = require('../controllers/apiController'); 6 | const chatController = require('../controllers/chatController'); 7 | 8 | // Create new room and associated song/chat tables. 9 | router.post( 10 | '/rooms', 11 | roomController.createRoom, 12 | songController.createTable, 13 | chatController.createChat, 14 | (req, res) => { 15 | res.status(200).json(res.locals.room); 16 | } 17 | ); 18 | 19 | // Get all rooms 20 | router.get('/rooms', roomController.getAllActive, (req, res) => { 21 | res.status(200).json(res.locals.activeRooms); 22 | }); 23 | 24 | // get songs form queue 25 | router.get('/rooms/:roomId/songs', songController.getAll, (req, res) => { 26 | res.status(200).json(res.locals.roomSongs) 27 | }) 28 | // set room to inactive 29 | router.put('/rooms', roomController.makeInactive, (req, res) => { 30 | res.sendStatus(200); 31 | }); 32 | 33 | // post song to queue 34 | router.post('/rooms/:roomId/songs', songController.addSong, (req, res) => { 35 | res.status(200).json(res.locals.addedSong); 36 | }); 37 | 38 | // get endpoint to grab all chat for specific room 39 | router.get('/rooms/:roomId/chat', chatController.getAll, (req, res) => { 40 | res.status(200).json(res.locals.roomChat); 41 | }); 42 | 43 | // post chat message to room for specific room 44 | router.post('/rooms/:roomId/chat', chatController.addMessage, (req, res) => { 45 | res.status(200).json(res.locals.chatMessage); 46 | }); 47 | 48 | // query songs from spotify API 49 | router.post('/spotify/songs', apiController.search, (req, res) => { 50 | res.status(200).json(res.locals.searchResult); 51 | }); 52 | 53 | module.exports = router; 54 | -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const passport = require('passport'); 3 | require('dotenv').config(); 4 | 5 | const authController = require('../controllers/authController.js'); 6 | 7 | router.get( 8 | '/spotify', 9 | passport.authenticate('spotify', { 10 | scope: ['streaming', 'user-read-email', 'user-read-private', 'user-modify-playback-state', 'user-read-playback-state'], 11 | showDialog: true, 12 | }), 13 | function (req, res) { 14 | // The request will be redirected to spotify for authentication, so this 15 | // function will not be called. 16 | } 17 | ); 18 | 19 | router.get('/fail', (req, res) => { 20 | res.status(401).send('FAILURE TO AUTHENTICATE'); 21 | }); 22 | 23 | router.get( 24 | '/spotify/callback', 25 | passport.authenticate('spotify', { 26 | // if failure to authenticate: 27 | // placeholder 28 | failureRedirect: '/fail', 29 | }), 30 | authController.saveAccessToken, 31 | (req, res) => { 32 | // if successful authentication: 33 | 34 | console.log('SUCCESSFUL AUTHENTICATION'); 35 | if (process.env.NODE_ENV === 'production') { 36 | res.redirect(process.env.SPOTIFY_REDIRECT_URL); 37 | } else { 38 | res.redirect('/dashboard'); 39 | } 40 | } 41 | ); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const cookieParser = require('cookie-parser'); 4 | const passport = require('passport'); 5 | require('./config/passport-setup'); 6 | // const songController = require('./controllers/songController'); 7 | // const apiController = require('./controllers/apiController'); 8 | // const userController = require('./controllers/userController'); 9 | 10 | const PORT = process.env.PORT || 3000; 11 | const app = express(); 12 | 13 | const http = require('http').Server(app); 14 | const io = require('socket.io')(http); 15 | 16 | const db = require('./models/roomModels'); 17 | 18 | const authRoute = require('./routes/auth'); 19 | const apiRoute = require('./routes/api'); 20 | 21 | // Initialize passport 22 | app.use(passport.initialize()); 23 | 24 | app.use(express.json()); 25 | app.use(express.urlencoded({ extended: true })); 26 | app.use(cookieParser()); 27 | 28 | // Use routes 29 | app.use('/auth', authRoute); 30 | app.use('/api/v1', apiRoute); 31 | 32 | app.get('/', (req, res) => { 33 | return res.status(200).sendFile(path.resolve(__dirname, '../client/index.html')); 34 | }); 35 | 36 | io.on('connection', socket => { 37 | console.log('User connected!!', socket.id); 38 | socket.on('join_room', room => { 39 | socket.join(room); 40 | console.log('User joined room: ', room); 41 | }); 42 | 43 | socket.on('chat', async data => { 44 | console.log('Getting chat from room', data); 45 | // Query to get user thumbnail and username to send back to the chat room 46 | let query = ` 47 | SELECT username, thumbnail FROM users 48 | WHERE id = '${data.uuid}'`; 49 | const result = await db.query(query); 50 | console.log('This is the user data: ', result.rows); 51 | 52 | // Save data to appropriate chat table 53 | query = `INSERT INTO ${data.room} (content, owner) VALUES ('${data.message}', '${data.uuid}')`; 54 | db.query(query); 55 | 56 | // Emit the appropriate data back to the chat room 57 | io.to(data.room).emit('chat', { 58 | username: result.rows[0].username, 59 | thumbnail: result.rows[0].thumbnail, 60 | message: data.message, 61 | }); 62 | }); 63 | 64 | socket.on('play', async data => { 65 | console.log('Getting play from room', data); 66 | 67 | io.to(data.room).emit('play', data); 68 | }) 69 | 70 | socket.on('pause', async data => { 71 | console.log('Getting pause from room', data); 72 | 73 | io.to(data.room).emit('pause', data); 74 | }) 75 | 76 | socket.on('requestPlayInfo', async data => { 77 | console.log('Getting requestPlayInfo from room', data); 78 | 79 | io.to(data.room).emit('requestPlayInfo', data) 80 | }) 81 | }); 82 | 83 | /** 84 | * *************************************** 85 | * Serve static files in production mode * 86 | * *************************************** 87 | */ 88 | if (process.env.NODE_ENV === 'production') { 89 | app.use('/build', express.static(path.resolve(__dirname, '../build'))); 90 | 91 | // Handle redirects 92 | app.get('*', (req, res) => { 93 | res.sendFile(path.resolve(__dirname, '../client/index.html')); 94 | }); 95 | } 96 | 97 | // Global error handler 98 | app.use((err, req, res, next) => { 99 | const defaultErr = { 100 | log: 'Untracked error caught in global error handler', 101 | status: 500, 102 | message: 'Check logs for more information', 103 | }; 104 | 105 | const returnErr = Object.assign(defaultErr, err); 106 | 107 | // Print error in terminal 108 | console.log(returnErr); 109 | res.status(501).json({ message: 'Internal server error' }); 110 | }); 111 | 112 | http.listen(PORT, () => { 113 | console.log(`Server listening on port: ${PORT}`); 114 | }); 115 | 116 | module.exports = app; 117 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './client/index.js', 5 | output: { 6 | path: path.resolve(__dirname, 'build'), 7 | filename: 'bundle.js', 8 | }, 9 | devtool: 'eval-source-map', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.jsx?$/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | presets: ['@babel/preset-env', '@babel/preset-react'], 18 | plugins: ['@babel/plugin-proposal-class-properties'], 19 | }, 20 | }, 21 | exclude: /node_modules/, 22 | }, 23 | { 24 | test: /\.s?css$/, 25 | use: ['style-loader', 'css-loader', 'sass-loader'], 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.(jpg|jpeg|png|ttf|svg)$/, 30 | use: [ 31 | 'file-loader', 32 | { 33 | loader: 'image-webpack-loader', 34 | options: { 35 | mozjpeg: { 36 | quality: 10, 37 | }, 38 | }, 39 | }, 40 | ], 41 | exclude: /node_modules/, 42 | }, 43 | ], 44 | }, 45 | mode: process.env.NODE_ENV, 46 | devServer: { 47 | hot: true, 48 | publicPath: '/build', 49 | historyApiFallback: true, 50 | contentBase: path.join(__dirname, 'client'), 51 | proxy: { 52 | '/api/*': 'http://localhost:3000', 53 | '/auth': 'http://localhost:3000', 54 | }, 55 | }, 56 | resolve: { 57 | extensions: ['.js', '.jsx'], 58 | }, 59 | }; 60 | --------------------------------------------------------------------------------