├── Music ├── .prettierignore ├── src │ ├── containers │ │ ├── App │ │ │ ├── index.ts │ │ │ ├── App.test.tsx │ │ │ ├── styled.js │ │ │ └── App.tsx │ │ ├── MainContainer │ │ │ ├── index.js │ │ │ ├── styled.js │ │ │ └── MainContainer.js │ │ ├── TrackContainer │ │ │ ├── index.js │ │ │ ├── styled.js │ │ │ └── TrackContainer.js │ │ └── PlayerContainer │ │ │ ├── index.ts │ │ │ ├── note.png │ │ │ ├── styled.js │ │ │ ├── PlayerContainer.test.tsx │ │ │ └── PlayerContainer.tsx │ ├── react-app-env.d.ts │ ├── components │ │ ├── Loading │ │ │ ├── index.js │ │ │ └── Loading.js │ │ ├── NowPlaying │ │ │ ├── index.ts │ │ │ ├── NowPlaying.tsx │ │ │ ├── styled.js │ │ │ └── NowPlaying.test.tsx │ │ ├── CoverArt │ │ │ ├── index.js │ │ │ ├── PlayButton │ │ │ │ ├── index.js │ │ │ │ ├── PlayButton.js │ │ │ │ └── styled.js │ │ │ ├── styled.js │ │ │ └── CoverArt.js │ │ ├── PlaylistView │ │ │ ├── index.js │ │ │ ├── styled.js │ │ │ └── PlaylistView.js │ │ ├── SideNavbar │ │ │ ├── index.js │ │ │ ├── SideNavbar.module.css │ │ │ ├── SideNavbar.js │ │ │ └── styled.js │ │ ├── PlayerControls │ │ │ ├── index.ts │ │ │ ├── styled.js │ │ │ ├── PlayerControls.tsx │ │ │ └── PlayerControls.test.tsx │ │ ├── Search │ │ │ ├── PlayButton │ │ │ │ ├── index.js │ │ │ │ ├── PlayButton.js │ │ │ │ └── styled.js │ │ │ ├── Search.css │ │ │ ├── CoverArt.js │ │ │ ├── styled.js │ │ │ └── Search.js │ │ ├── VolumeControl │ │ │ ├── index.js │ │ │ ├── VolumeControl.js │ │ │ └── styled.js │ │ ├── Recommend │ │ │ ├── PlayButton │ │ │ │ ├── index.js │ │ │ │ ├── PlayButton.js │ │ │ │ └── styled.js │ │ │ ├── CoverArt.js │ │ │ ├── styled.js │ │ │ └── Recommend.js │ │ ├── PlaylistPlayButton │ │ │ ├── index.ts │ │ │ ├── styled.js │ │ │ ├── PlaylistPlayButton.tsx │ │ │ └── PlaylistPlayButton.test.tsx │ │ ├── TrackControlButton │ │ │ ├── index.ts │ │ │ ├── styled.js │ │ │ ├── TrackControlButton.tsx │ │ │ └── TrackControlButton.test.tsx │ │ └── PlaylistSelectorView │ │ │ ├── index.js │ │ │ ├── styled.js │ │ │ └── PlaylistSelectorView.js │ ├── actions │ │ ├── index.js │ │ ├── action-types.js │ │ ├── player-control-actions.js │ │ └── fetch-actions.js │ ├── css-variables │ │ ├── layout.js │ │ └── colors.js │ ├── utils │ │ ├── idFromHref.js │ │ ├── searchPrevTrack.js │ │ └── skipUnavailableTracks.js │ ├── images │ │ ├── Spotify_Icon_RGB_Black.png │ │ └── Spotify_Icon_RGB_White.png │ ├── setupTests.ts │ ├── index.tsx │ ├── store.js │ ├── variables.js │ ├── index.css │ ├── config.js │ └── reducers │ │ └── index.js ├── public │ ├── note.png │ ├── favicon.png │ ├── Spotify_Icon_RGB_Black.png │ ├── Spotify_Icon_RGB_White.png │ └── index.html ├── .prettierrc ├── .gitignore ├── server.js ├── tslint.json ├── tsconfig.json ├── .eslintrc.json ├── .travis.yml └── package.json ├── Authentication ├── client │ ├── src │ │ ├── App.css │ │ ├── img │ │ │ ├── img5.png │ │ │ └── img7.jpg │ │ ├── actions │ │ │ ├── types.js │ │ │ └── authActions.js │ │ ├── reducers │ │ │ ├── index.js │ │ │ ├── errorReducer.js │ │ │ └── authReducer.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── auth │ │ │ │ ├── Auth.module.css │ │ │ │ ├── Login.js │ │ │ │ └── Register.js │ │ │ ├── layout │ │ │ │ └── Navbar.js │ │ │ ├── private-route │ │ │ │ └── PrivateRoute.js │ │ │ └── dashboard │ │ │ │ └── Dashboard.js │ │ ├── utils │ │ │ └── setAuthToken.js │ │ ├── index.css │ │ ├── index.js │ │ ├── store.js │ │ ├── App.js │ │ └── serviceWorker.js │ ├── public │ │ ├── note.png │ │ ├── favicon.ico │ │ ├── background.jpg │ │ ├── manifest.json │ │ └── index.html │ ├── .gitignore │ └── package.json ├── .gitignore ├── models │ └── User.js ├── config │ └── passport.js ├── validation │ ├── login.js │ └── register.js ├── package.json ├── server.js └── routes │ └── api │ └── users.js ├── Screenshots ├── new.png ├── landing.png ├── playing.png ├── search.png ├── signin.png ├── signup.png ├── featured.png └── recommend.png ├── projectPresentation.pptx ├── LICENSE ├── README.md └── .gitignore /Music/.prettierignore: -------------------------------------------------------------------------------- 1 | *.png -------------------------------------------------------------------------------- /Authentication/client/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Authentication/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /Music/src/containers/App/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './App'; 2 | -------------------------------------------------------------------------------- /Music/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /Music/src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Loading'; 2 | -------------------------------------------------------------------------------- /Music/src/components/NowPlaying/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './NowPlaying' -------------------------------------------------------------------------------- /Music/src/components/CoverArt/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './CoverArt'; 2 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistView/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PlaylistView'; 2 | -------------------------------------------------------------------------------- /Music/src/components/SideNavbar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SideNavbar'; 2 | -------------------------------------------------------------------------------- /Music/src/components/CoverArt/PlayButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PlayButton'; 2 | -------------------------------------------------------------------------------- /Music/src/components/PlayerControls/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PlayerControls'; 2 | -------------------------------------------------------------------------------- /Music/src/components/Search/PlayButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PlayButton'; 2 | -------------------------------------------------------------------------------- /Music/src/components/VolumeControl/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './VolumeControl'; 2 | -------------------------------------------------------------------------------- /Music/src/containers/MainContainer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MainContainer'; 2 | -------------------------------------------------------------------------------- /Music/src/containers/TrackContainer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './TrackContainer'; 2 | -------------------------------------------------------------------------------- /Music/src/components/Recommend/PlayButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PlayButton'; 2 | -------------------------------------------------------------------------------- /Music/src/containers/PlayerContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PlayerContainer'; 2 | -------------------------------------------------------------------------------- /Music/public/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/public/note.png -------------------------------------------------------------------------------- /Music/src/components/PlaylistPlayButton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PlaylistPlayButton'; 2 | -------------------------------------------------------------------------------- /Music/src/components/TrackControlButton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TrackControlButton'; 2 | -------------------------------------------------------------------------------- /Screenshots/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/new.png -------------------------------------------------------------------------------- /Music/src/components/PlaylistSelectorView/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PlaylistSelectorView'; 2 | -------------------------------------------------------------------------------- /Screenshots/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/landing.png -------------------------------------------------------------------------------- /Screenshots/playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/playing.png -------------------------------------------------------------------------------- /Screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/search.png -------------------------------------------------------------------------------- /Screenshots/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/signin.png -------------------------------------------------------------------------------- /Screenshots/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/signup.png -------------------------------------------------------------------------------- /Music/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/public/favicon.png -------------------------------------------------------------------------------- /Music/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './fetch-actions'; 2 | export * from './player-control-actions'; 3 | -------------------------------------------------------------------------------- /Screenshots/featured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/featured.png -------------------------------------------------------------------------------- /Screenshots/recommend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Screenshots/recommend.png -------------------------------------------------------------------------------- /projectPresentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/projectPresentation.pptx -------------------------------------------------------------------------------- /Music/src/css-variables/layout.js: -------------------------------------------------------------------------------- 1 | export const sidebarWidth = '220px'; 2 | export const playerHeight = '90px'; 3 | -------------------------------------------------------------------------------- /Music/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /Authentication/client/public/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Authentication/client/public/note.png -------------------------------------------------------------------------------- /Authentication/client/src/img/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Authentication/client/src/img/img5.png -------------------------------------------------------------------------------- /Authentication/client/src/img/img7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Authentication/client/src/img/img7.jpg -------------------------------------------------------------------------------- /Music/src/utils/idFromHref.js: -------------------------------------------------------------------------------- 1 | export default href => { 2 | const args = href.split('/'); 3 | return args[args.length - 1]; 4 | }; 5 | -------------------------------------------------------------------------------- /Authentication/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Authentication/client/public/favicon.ico -------------------------------------------------------------------------------- /Music/public/Spotify_Icon_RGB_Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/public/Spotify_Icon_RGB_Black.png -------------------------------------------------------------------------------- /Music/public/Spotify_Icon_RGB_White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/public/Spotify_Icon_RGB_White.png -------------------------------------------------------------------------------- /Authentication/client/public/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Authentication/client/public/background.jpg -------------------------------------------------------------------------------- /Music/src/images/Spotify_Icon_RGB_Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/src/images/Spotify_Icon_RGB_Black.png -------------------------------------------------------------------------------- /Music/src/images/Spotify_Icon_RGB_White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/src/images/Spotify_Icon_RGB_White.png -------------------------------------------------------------------------------- /Music/src/containers/PlayerContainer/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartika-nair/MusicAppMERN/HEAD/Music/src/containers/PlayerContainer/note.png -------------------------------------------------------------------------------- /Music/src/components/SideNavbar/SideNavbar.module.css: -------------------------------------------------------------------------------- 1 | .bottom { 2 | width: 100%; 3 | bottom: 100px; 4 | position: absolute; 5 | color: rgb(160, 160, 160); 6 | } 7 | -------------------------------------------------------------------------------- /Music/src/components/Loading/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = () =>
Loading.....
; 4 | 5 | export default Loading; 6 | -------------------------------------------------------------------------------- /Authentication/client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const GET_ERRORS = "GET_ERRORS"; 2 | export const USER_LOADING = "USER_LOADING"; 3 | export const SET_CURRENT_USER = "SET_CURRENT_USER"; 4 | -------------------------------------------------------------------------------- /Music/src/components/TrackControlButton/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | font-size: 20px; 5 | padding-right: 16px; 6 | width: 48px; 7 | `; 8 | -------------------------------------------------------------------------------- /Music/src/components/Search/Search.css: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | color: #000; 3 | padding: 6px 48px; 4 | height: 40px; 5 | width: 100%; 6 | border: 0; 7 | border-radius: 500px; 8 | text-overflow: ellipsis; 9 | } 10 | -------------------------------------------------------------------------------- /Music/src/css-variables/colors.js: -------------------------------------------------------------------------------- 1 | export const spotifyGreen = '#1DB954'; 2 | export const spotifyGreenPlaying = '#1ED660'; 3 | export const spotifyBlack = '#191414'; 4 | export const spotifyWhite = '#FFFFFF'; 5 | export const spotifyGray = 'rgb(160, 160, 160)'; 6 | -------------------------------------------------------------------------------- /Authentication/client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import authReducer from "./authReducer"; 3 | import errorReducer from "./errorReducer"; 4 | 5 | export default combineReducers({ 6 | auth: authReducer, 7 | errors: errorReducer 8 | }); 9 | -------------------------------------------------------------------------------- /Music/src/setupTests.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Authentication/client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /Music/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | /playground 20 | -------------------------------------------------------------------------------- /Music/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './containers/App'; 5 | 6 | import './index.css'; 7 | 8 | // Disable right click to match Spotify behaviour 9 | window.oncontextmenu = () => false; 10 | 11 | ReactDOM.render(, document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /Authentication/client/src/reducers/errorReducer.js: -------------------------------------------------------------------------------- 1 | import { GET_ERRORS } from "../actions/types"; 2 | 3 | const initialState = {}; 4 | 5 | export default function(state = initialState, action) { 6 | switch (action.type) { 7 | case GET_ERRORS: 8 | return action.payload; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Music/src/containers/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | xdescribe('App component', () => { 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div'); 9 | ReactDOM.render(, div); 10 | ReactDOM.unmountComponentAtNode(div); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /Music/src/utils/searchPrevTrack.js: -------------------------------------------------------------------------------- 1 | export default (playlist, nr) => { 2 | let trackNumber = nr; 3 | if (trackNumber <= 0) { 4 | return -1; 5 | } 6 | 7 | trackNumber -= 1; 8 | while (!playlist[trackNumber].track.preview_url) { 9 | trackNumber -= 1; 10 | if (trackNumber < 0) { 11 | return -1; 12 | } 13 | } 14 | 15 | return trackNumber; 16 | }; 17 | -------------------------------------------------------------------------------- /Music/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import reducers from './reducers'; 5 | 6 | const store = createStore( 7 | reducers, 8 | compose( 9 | applyMiddleware(thunk), 10 | window.devToolsExtension ? window.devToolsExtension() : noop => noop 11 | ) 12 | ); 13 | 14 | export default store; 15 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistPlayButton/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Button = styled.button` 4 | background-color: #2ebd59; 5 | border: none; 6 | border-radius: 500px; 7 | color: #fff; 8 | cursor: pointer; 9 | font-size: 14px; 10 | height: 43px; 11 | margin-top: 16px; 12 | outline: none; 13 | padding: 13px 44px; 14 | width: 130px; 15 | `; 16 | -------------------------------------------------------------------------------- /Authentication/client/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 | -------------------------------------------------------------------------------- /Authentication/client/src/components/auth/Auth.module.css: -------------------------------------------------------------------------------- 1 | .centered { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | text-align: center; 7 | margin-top: 9%; 8 | } 9 | 10 | .lowered { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | text-align: center; 16 | margin-bottom: 1%; 17 | } 18 | -------------------------------------------------------------------------------- /Music/src/variables.js: -------------------------------------------------------------------------------- 1 | export const gridTemplateColumns = w => { 2 | switch (true) { 3 | case w <= 547: 4 | return 'repeat(2, 1fr)'; 5 | case w >= 548 && w <= 771: 6 | return 'repeat(3, 1fr)'; 7 | case w >= 772 && w <= 979: 8 | return 'repeat(4, 1fr)'; 9 | default: 10 | return 'repeat(6, 1fr)'; 11 | } 12 | }; 13 | 14 | export const rootUrl = 'https://api.spotify.com/v1'; 15 | -------------------------------------------------------------------------------- /Authentication/client/src/utils/setAuthToken.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const setAuthToken = token => { 4 | if (token) { 5 | // Apply authorization token to every request if logged in 6 | axios.defaults.headers.common["Authorization"] = token; 7 | } else { 8 | // Delete auth header 9 | delete axios.defaults.headers.common["Authorization"]; 10 | } 11 | }; 12 | 13 | export default setAuthToken; 14 | -------------------------------------------------------------------------------- /Music/src/containers/PlayerContainer/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.footer` 4 | background-color: rgb(40, 40, 40); 5 | color: white; 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: space-between; 9 | align-items: center; 10 | height: 90px; 11 | padding: 0 16px; 12 | position: fixed; 13 | left: 0; 14 | bottom: 0; 15 | width: 100vw; 16 | `; 17 | -------------------------------------------------------------------------------- /Authentication/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /Music/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const express = require('express'); 4 | const cors = require('cors'); 5 | const app = express(); 6 | app.use(cors()); 7 | app.use(express.static('build')); 8 | 9 | app.get('*', (req, res) => { 10 | res.sendFile(path.join(__dirname, 'build', 'index.html')); 11 | }); 12 | 13 | app.listen(5000, function () { 14 | console.log('Example app listening on port ' + 5000 + '!'); 15 | }); 16 | -------------------------------------------------------------------------------- /Music/src/utils/skipUnavailableTracks.js: -------------------------------------------------------------------------------- 1 | export default (playlist, nr) => { 2 | let trackNumber = nr; 3 | if (!playlist) { 4 | return 0; 5 | } 6 | if (trackNumber >= playlist.length) { 7 | return -1; 8 | } 9 | 10 | while (!playlist[trackNumber].track.preview_url) { 11 | trackNumber += 1; 12 | if (trackNumber >= playlist.length) { 13 | return -1; 14 | } 15 | } 16 | 17 | return trackNumber; 18 | }; 19 | -------------------------------------------------------------------------------- /Authentication/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistPlayButton/PlaylistPlayButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from './styled'; 4 | 5 | export interface IProps { 6 | isPlaying: boolean; 7 | onClick: () => void; 8 | } 9 | 10 | const PlaylistPlayButton: React.SFC = ({ isPlaying, onClick }) => { 11 | return ( 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | export default PlaylistPlayButton; 19 | -------------------------------------------------------------------------------- /Music/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | nav, 5 | p, 6 | a, 7 | ul, 8 | li, 9 | section, 10 | footer, 11 | img, 12 | i, 13 | button { 14 | margin: 0; 15 | padding: 0; 16 | box-sizing: border-box; } 17 | 18 | html, 19 | body { 20 | height: 100vh; } 21 | 22 | body { 23 | cursor: default; 24 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } 25 | 26 | a, 27 | a:visited, 28 | a:hover { 29 | text-decoration: none; } 30 | 31 | .container { 32 | display: flex; 33 | flex-direction: row; } 34 | -------------------------------------------------------------------------------- /Music/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "linterOptions": { 4 | "exclude": ["config/**/*.js", "node_modules/**/*.ts"] 5 | }, 6 | "rules": { 7 | "jsx-boolean-value": false, 8 | "jsx-no-lambda": false, 9 | "member-access": [true, "no-public"], 10 | "ordered-imports": [ 11 | true, 12 | { 13 | "import-sources-order": "any", 14 | "named-imports-order": "case-insensitive" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Authentication/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /Authentication/client/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import rootReducer from "./reducers"; 4 | 5 | const initialState = {}; 6 | 7 | const middleware = [thunk]; 8 | 9 | const store = createStore( 10 | rootReducer, 11 | initialState, 12 | compose( 13 | applyMiddleware(...middleware), 14 | (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && 15 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__()) || 16 | compose 17 | ) 18 | ); 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistSelectorView/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Header = styled.h1` 4 | color: #fff; 5 | font-size: 36px; 6 | font-weight: 600; 7 | letter-spacing: -0.18px; 8 | line-height: 44px; 9 | margin: 0 0 24px; 10 | padding-top: 24px; 11 | text-align: center; 12 | `; 13 | 14 | export const Wrapper = styled.div` 15 | display: grid; 16 | grid-template-columns: ${p => p.template}; 17 | justify-content: center; 18 | margin: auto; 19 | max-width: 1480px; 20 | padding: 0 28px; 21 | `; 22 | -------------------------------------------------------------------------------- /Authentication/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | // Create Schema 5 | const UserSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | }, 18 | date: { 19 | type: Date, 20 | default: Date.now, 21 | }, 22 | username: { 23 | type: Date, 24 | default: Date.now, 25 | }, 26 | }); 27 | 28 | module.exports = User = mongoose.model("users", UserSchema); 29 | -------------------------------------------------------------------------------- /Music/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | "lib": [ 18 | "esnext", 19 | "dom" 20 | ] 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /Music/src/components/VolumeControl/VolumeControl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Wrapper } from './styled'; 4 | 5 | class VolumeControl extends React.Component { 6 | updateVolume = e => { 7 | this.props.handleChange(e.target.value); 8 | }; 9 | 10 | render() { 11 | return ( 12 | 13 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | export default VolumeControl; 27 | -------------------------------------------------------------------------------- /Music/src/components/CoverArt/PlayButton/PlayButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Pause, Play, Wrapper } from './styled'; 5 | 6 | const propTypes = { 7 | dataName: PropTypes.string, 8 | showPlay: PropTypes.bool, 9 | }; 10 | 11 | const defaultProps = { 12 | dataName: 'play', 13 | showPlay: true, 14 | }; 15 | 16 | const PlayButton = ({ dataName, showPlay }) => ( 17 | {showPlay ? : } 18 | ); 19 | 20 | PlayButton.propTypes = propTypes; 21 | PlayButton.defaultProps = defaultProps; 22 | 23 | export default PlayButton; 24 | -------------------------------------------------------------------------------- /Music/src/components/Recommend/PlayButton/PlayButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Pause, Play, Wrapper } from './styled'; 5 | 6 | const propTypes = { 7 | dataName: PropTypes.string, 8 | showPlay: PropTypes.bool, 9 | }; 10 | 11 | const defaultProps = { 12 | dataName: 'play', 13 | showPlay: true, 14 | }; 15 | 16 | const PlayButton = ({ dataName, showPlay }) => ( 17 | {showPlay ? : } 18 | ); 19 | 20 | PlayButton.propTypes = propTypes; 21 | PlayButton.defaultProps = defaultProps; 22 | 23 | export default PlayButton; 24 | -------------------------------------------------------------------------------- /Music/src/components/Search/PlayButton/PlayButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Pause, Play, Wrapper } from './styled'; 5 | 6 | const propTypes = { 7 | dataName: PropTypes.string, 8 | showPlay: PropTypes.bool, 9 | }; 10 | 11 | const defaultProps = { 12 | dataName: 'play', 13 | showPlay: true, 14 | }; 15 | 16 | const PlayButton = ({ dataName, showPlay }) => ( 17 | {showPlay ? : } 18 | ); 19 | 20 | PlayButton.propTypes = propTypes; 21 | PlayButton.defaultProps = defaultProps; 22 | 23 | export default PlayButton; 24 | -------------------------------------------------------------------------------- /Authentication/client/src/components/layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class Navbar extends Component { 5 | render() { 6 | return ( 7 |
8 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default Navbar; 27 | -------------------------------------------------------------------------------- /Music/src/containers/App/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { sidebarWidth, playerHeight } from '../../css-variables/layout'; 4 | 5 | export const Background = styled.div` 6 | background-color: #36096d; 7 | background-image: linear-gradient(315deg, #36096d 0%, #37d5d6 74%); 8 | background-size: cover; 9 | background-repeat: no-repeat; 10 | height: 100vh; 11 | width: 100vw; 12 | position: fixed; 13 | z-index: -1; 14 | `; 15 | 16 | export const Section = styled.div` 17 | margin-left: ${sidebarWidth}; 18 | margin-bottom: ${playerHeight}; 19 | `; 20 | 21 | export const Wrapper = styled.div` 22 | width: 100%; 23 | `; 24 | -------------------------------------------------------------------------------- /Authentication/client/src/reducers/authReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_CURRENT_USER, USER_LOADING } from "../actions/types"; 2 | 3 | const isEmpty = require("is-empty"); 4 | 5 | const initialState = { 6 | isAuthenticated: false, 7 | user: {}, 8 | loading: false 9 | }; 10 | 11 | export default function(state = initialState, action) { 12 | switch (action.type) { 13 | case SET_CURRENT_USER: 14 | return { 15 | ...state, 16 | isAuthenticated: !isEmpty(action.payload), 17 | user: action.payload 18 | }; 19 | case USER_LOADING: 20 | return { 21 | ...state, 22 | loading: true 23 | }; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Authentication/client/src/components/private-route/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | 6 | const PrivateRoute = ({ component: Component, auth, ...rest }) => ( 7 | 10 | auth.isAuthenticated === true ? ( 11 | 12 | ) : ( 13 | 14 | ) 15 | } 16 | /> 17 | ); 18 | 19 | PrivateRoute.propTypes = { 20 | auth: PropTypes.object.isRequired 21 | }; 22 | 23 | const mapStateToProps = state => ({ 24 | auth: state.auth 25 | }); 26 | 27 | export default connect(mapStateToProps)(PrivateRoute); 28 | -------------------------------------------------------------------------------- /Music/src/components/NowPlaying/NowPlaying.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Artist, 5 | EllipsisText, 6 | Image, 7 | ImageContainer, 8 | InfoBox, 9 | Title, 10 | Wrapper, 11 | } from './styled'; 12 | 13 | export interface IProps { 14 | artist: string; 15 | src: string; 16 | title: string; 17 | } 18 | 19 | const NowPlaying: React.SFC = ({ artist, title, src }) => ( 20 | 21 | 22 | {`${artist} 23 | 24 | 25 | 26 | <EllipsisText>{title}</EllipsisText> 27 | 28 | {artist} 29 | 30 | 31 | ); 32 | 33 | export default NowPlaying; 34 | -------------------------------------------------------------------------------- /Authentication/config/passport.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require("passport-jwt").Strategy; 2 | const ExtractJwt = require("passport-jwt").ExtractJwt; 3 | const mongoose = require("mongoose"); 4 | const User = mongoose.model("users"); 5 | const keys = require("../config/keys"); 6 | 7 | const opts = {}; 8 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); 9 | opts.secretOrKey = keys.secretOrKey; 10 | 11 | module.exports = passport => { 12 | passport.use( 13 | new JwtStrategy(opts, (jwt_payload, done) => { 14 | User.findById(jwt_payload.id) 15 | .then(user => { 16 | if (user) { 17 | return done(null, user); 18 | } 19 | return done(null, false); 20 | }) 21 | .catch(err => console.log(err)); 22 | }) 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /Music/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier", 5 | "prettier/react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "jest": true 10 | }, 11 | "parser": "babel-eslint", 12 | "plugins": [ 13 | "babel", 14 | "prettier" 15 | ], 16 | "rules": { 17 | "arrow-parens": "off", 18 | "babel/semi": 1, 19 | "comma-dangle": "off", 20 | "function-paren-newline": "off", 21 | "no-confusing-arrow": "warn", 22 | "object-curly-newline": "off", 23 | "react/forbid-prop-types": "off", 24 | "react/jsx-filename-extension": "off", 25 | "react/no-danger": "off", 26 | "react/prefer-stateless-function": "off", 27 | "prettier/prettier": [ 28 | "error", 29 | { 30 | "trailingComma": "es5", 31 | "singleQuote": true 32 | } 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /Authentication/validation/login.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const isEmpty = require("is-empty"); 3 | 4 | module.exports = function validateLoginInput(data) { 5 | let errors = {}; 6 | 7 | // Convert empty fields to an empty string so we can use validator functions 8 | data.email = !isEmpty(data.email) ? data.email : ""; 9 | data.password = !isEmpty(data.password) ? data.password : ""; 10 | 11 | // Email checks 12 | if (Validator.isEmpty(data.email)) { 13 | errors.email = "Email field is required"; 14 | } else if (!Validator.isEmail(data.email)) { 15 | errors.email = "Email is invalid"; 16 | } 17 | // Password checks 18 | if (Validator.isEmpty(data.password)) { 19 | errors.password = "Password field is required"; 20 | } 21 | 22 | return { 23 | errors, 24 | isValid: isEmpty(errors) 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /Authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authentication", 3 | "version": "1.0.0", 4 | "description": "authentication", 5 | "main": "server.js", 6 | "scripts": { 7 | "client-install": "npm install --prefix client", 8 | "start": "node server.js", 9 | "server": "nodemon server.js", 10 | "client": "npm start --prefix client", 11 | "dev": "concurrently \"npm run server\" \"npm run client\"" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "body-parser": "^1.18.3", 18 | "concurrently": "^4.0.1", 19 | "express": "^4.16.4", 20 | "is-empty": "^1.2.0", 21 | "jsonwebtoken": "^8.3.0", 22 | "mongoose": "^5.3.11", 23 | "nodemon": "^2.0.6", 24 | "passport": "^0.4.0", 25 | "passport-jwt": "^4.0.0", 26 | "validator": "^10.9.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Music/src/components/VolumeControl/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | width: 180px; 5 | 6 | & input[type='range'] { 7 | -webkit-appearance: none; 8 | } 9 | 10 | & input[type='range']::-webkit-slider-runnable-track { 11 | background: #a0a0a0; 12 | border: none; 13 | border-radius: 3px; 14 | cursor: pointer; 15 | height: 4px; 16 | } 17 | 18 | & input[type='range']::-webkit-slider-thumb { 19 | -webkit-appearance: none; 20 | background: #fff; 21 | border: none; 22 | border-radius: 50%; 23 | height: 12px; 24 | margin-top: -4px; 25 | outline: none; 26 | width: 12px; 27 | } 28 | 29 | input[type='range']:focus { 30 | outline: none; 31 | } 32 | 33 | input[type='range']:focus::-webkit-slider-runnable-track { 34 | background: #a0a0a0; 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /Music/src/config.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import store from './store'; 3 | 4 | export default { 5 | albums: { 6 | initPlay: href => store.dispatch(actions.startAlbum({ href })), 7 | onMount: () => store.dispatch(actions.fetchNewReleases()), 8 | sectionMessage: 'New Albums and Singles', 9 | selection: 'newReleases', 10 | }, 11 | category: { 12 | initPlay: href => store.dispatch(actions.startPlaylist({ href })), 13 | onMount: id => store.dispatch(actions.fetchCategoryPlaylist(id)), 14 | onUnmount: () => store.dispatch(actions.clearCategoryPlaylist()), 15 | sectionMessage: 'Popular Playlists', 16 | selection: 'categoryPlaylist', 17 | }, 18 | featured: { 19 | initPlay: href => store.dispatch(actions.startPlaylist({ href })), 20 | onMount: () => store.dispatch(actions.fetchFeatured()), 21 | selection: 'featured', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /Music/src/containers/TrackContainer/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { spotifyGray, spotifyGreenPlaying } from '../../css-variables/colors'; 4 | 5 | export const Artist = styled.div` 6 | color: ${spotifyGray}; 7 | font-weight: 200; 8 | `; 9 | 10 | export const Description = styled.div` 11 | flex: 1; 12 | `; 13 | 14 | export const TrackName = styled.div` 15 | font-size: 16px; 16 | font-weight: 200; 17 | `; 18 | 19 | export const Wrapper = styled.div` 20 | align-items: center; 21 | color: ${p => 22 | p.active 23 | ? spotifyGreenPlaying 24 | : p.hasPreview ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 255, 255, 0.3)'}; 25 | display: flex; 26 | flex-direction: row; 27 | font-size: 14px; 28 | font-weight: 200; 29 | height: 70px; 30 | max-width: 1480px; 31 | padding: 0 28px; 32 | transition: all 0.2s; 33 | 34 | &:hover { 35 | background-color: rgba(0, 0, 0, 0.2); 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /Authentication/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.1", 7 | "@material-ui/icons": "^4.9.1", 8 | "axios": "^0.18.0", 9 | "classnames": "^2.2.6", 10 | "jwt-decode": "^2.2.0", 11 | "react": "^16.6.3", 12 | "react-dom": "^16.6.3", 13 | "react-redux": "^5.1.1", 14 | "react-router-dom": "^4.3.1", 15 | "react-scripts": "2.1.1", 16 | "redux": "^4.0.1", 17 | "redux-thunk": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "proxy": "http://localhost:5000", 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /Authentication/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ΜΟΥΣΙΚΗ 13 | 14 | 15 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Music/src/components/NowPlaying/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Artist = styled.div` 4 | color: #a0a0a0; 5 | font-size: 11px; 6 | `; 7 | 8 | export const Image = styled.img` 9 | width: 100%; 10 | `; 11 | 12 | export const ImageContainer = styled.div` 13 | margin-right: 13px; 14 | width: 64px; 15 | `; 16 | 17 | export const InfoBox = styled.div` 18 | align-items: flex-start; 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | line-height: 20px; 23 | width: calc(100% - 76px); 24 | `; 25 | 26 | export const Title = styled.div` 27 | display: flex; 28 | font-size: 14px; 29 | max-width: 100%; 30 | `; 31 | 32 | export const EllipsisText = styled.div` 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | `; 37 | 38 | export const Wrapper = styled.div` 39 | display: flex; 40 | flex-direction: row; 41 | height: 64px; 42 | justify-content: flex-start; 43 | max-width: 450px; 44 | width: 30%; 45 | `; 46 | -------------------------------------------------------------------------------- /Music/src/components/SideNavbar/SideNavbar.js: -------------------------------------------------------------------------------- 1 | //import classes from '*.module.css'; 2 | import React, { Component } from 'react'; 3 | 4 | import { Group, GroupHeader, Navbar, NavItem } from './styled'; 5 | import classes from './SideNavbar.module.css'; 6 | 7 | class SideNavbar extends Component { 8 | render() { 9 | return ( 10 | 11 | 12 |

18 | ΜΟΥΣΙΚΗ 19 |

20 |
21 | 22 | Home 23 | 24 | 29 | 30 |

Back

31 |
32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default SideNavbar; 39 | -------------------------------------------------------------------------------- /Authentication/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const mongoose = require("mongoose"); 3 | const bodyParser = require("body-parser"); 4 | const passport = require("passport"); 5 | 6 | const users = require("./routes/api/users"); 7 | 8 | const app = express(); 9 | 10 | // Bodyparser middleware 11 | app.use( 12 | bodyParser.urlencoded({ 13 | extended: false, 14 | }), 15 | ); 16 | app.use(bodyParser.json()); 17 | 18 | // DB Config 19 | const db = require("./config/keys").mongoURI; 20 | 21 | // Connect to MongoDB 22 | mongoose 23 | .connect(db, { useNewUrlParser: true, useUnifiedTopology: true }) 24 | .then(() => console.log("MongoDB successfully connected")) 25 | .catch((err) => console.log(err)); 26 | 27 | // Passport middleware 28 | app.use(passport.initialize()); 29 | 30 | // Passport config 31 | require("./config/passport")(passport); 32 | 33 | // Routes 34 | app.use("/api/users", users); 35 | 36 | const port = process.env.PORT || 5000; 37 | 38 | app.listen(port, () => console.log(`Server up and running on port ${port} !`)); 39 | -------------------------------------------------------------------------------- /Music/src/actions/action-types.js: -------------------------------------------------------------------------------- 1 | export const ALBUM_UPDATE = 'ALBUM_UPDATE'; 2 | export const CATEGORY_PLAYLIST_CLEAR = 'CATEGORY_PLAYLIST_CLEAR'; 3 | export const CATEGORY_PLAYLIST_SET = 'CATEGORY_PLAYLIST_SET'; 4 | export const CLEAR_PLAYLIST_VIEW = 'CLEAR_PLAYLIST_VIEW'; 5 | export const COPY_FROM_VIEW_AND_PLAY = 'COPY_FROM_VIEW_AND_PLAY'; 6 | export const COPY_TO_VIEW = 'COPY_TO_VIEW'; 7 | export const FEATURED_SET = 'FEATURED_SET'; 8 | export const GENRES_SET = 'GENRES_SET'; 9 | export const NEW_RELEASES_SET = 'NEW_RELEASES_SET'; 10 | export const PLAY_NEXT_TRACK = 'PLAY_NEXT_TRACK'; 11 | export const PLAY_TRACK = 'PLAY_TRACK'; 12 | export const PLAYLIST_SET = 'PLAYLIST_SET'; 13 | export const RESET_NO_PREVIEW = 'RESET_NO_PREVIEW'; 14 | export const SET_PAUSE = 'SET_PAUSE'; 15 | export const SET_PLAYLIST_VIEW = 'SET_PLAYLIST_VIEW'; 16 | export const STOP_PLAY = 'STOP_PLAY'; 17 | export const STOP_TRACK = 'STOP_TRACK'; 18 | export const TOKEN_SET = 'TOKEN_SET'; 19 | export const TRACK_TIME_UPDATE = 'TRACK_TIME_UPDATE'; 20 | export const UNPAUSE = 'UNPAUSE'; 21 | -------------------------------------------------------------------------------- /Music/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '14' 4 | notifications: 5 | email: false 6 | before_install: 7 | - npm install codecov -g 8 | after_success: 9 | - codecov 10 | before_deploy: npm run build 11 | deploy: 12 | provider: s3 13 | bucket: sc.jenovs.com 14 | skip_cleanup: true 15 | access_key_id: AKIAJCC45WY2ETHOOVVA 16 | secret_access_key: 17 | secure: QNKWl1NlBIiKpR1ZEvFs2RiNPOCZhimqqDt4HpAcZV9ZDQmCWerylyYzRmsCHqqg1u6t4vnr23TbFf7RSbDrCypCBZ2pT0Dt3wyyzhvraabkojfi6lBeQu9nF6l5rKfnBWgqUPwW8q6thl0XHtGvh9/4nOXw+CYUHgRvWe26yeFTfli6zg7djMoPxjoLY5YOuk5M5ME+lLaM0GBfOu4MhUP0oMzHhT+/2pDjQk5xDOctttpVpu6+82xsUmFBaiBsn8SHPrYppYC/hjOR36G5mCcqtk5cLYpoWmGJC0V1uq9gbgsnbML8G5svCsqSkVfwDeNNyzmgM/CA+XOwDyQi6L9y34/TYKZNkTeVpbSmBF27RAV6V/jeuK3S7WONvybCDEQXWF4bYHsaUXmaH79Ttrzxp2Qnf36bB0LlXx/92shodh9OGKO8kPqCrT0RXT5MdmZnKs0S3j23mMq3Ow+70jLlZEtC6Xq6DvJGwGsnOkx/nWHzA/tF0oS6RcUMN/7xPi2XBMLsDP6PO+eQMZycvujMPqJolDShuEqLUx3b+VZDncSzAEG/ywIeiSnoyH3mAk04gIEUtxnRax0vtRH/6IMbu3bjCQPP1g8DJT2dIyuewHMCpv50wJANYq/k8rVyeHBlQ72A1zWmhzHRZtLBye0C2E0fFUMWVo3nSYWApEg= 18 | local_dir: build 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kartika Nair 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 | -------------------------------------------------------------------------------- /Music/src/components/Search/PlayButton/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { spotifyGreen } from '../../../css-variables/colors'; 4 | 5 | const ActionButton = styled.div` 6 | pointer-events: none; 7 | position: absolute; 8 | top: calc(50% - 16px); 9 | `; 10 | 11 | export const Pause = styled(ActionButton)` 12 | border-left: 5px solid #fff; 13 | border-right: 5px solid #fff; 14 | height: 32px; 15 | left: calc(50% - 9px); 16 | width: 18px; 17 | `; 18 | 19 | export const Play = styled(ActionButton)` 20 | border: 15px solid transparent; 21 | border-left-color: #fff; 22 | height: 0; 23 | left: calc(50%); 24 | transform: scaleX(1.5); 25 | width: 0; 26 | `; 27 | 28 | export const Wrapper = styled.div` 29 | background-color: ${spotifyGreen}; 30 | border-radius: 50%; 31 | bottom: 16px; 32 | cursor: default; 33 | filter: brightness(1); 34 | height: 40px; 35 | position: absolute; 36 | right: 20px; 37 | transition: transform 0.1s ease; 38 | width: 40px; 39 | 40 | &:hover { 41 | border-width: 2px; 42 | transform: scale(1.1); 43 | } 44 | 45 | &:active { 46 | border-width: 1px; 47 | transform: scale(1); 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /Music/src/components/CoverArt/PlayButton/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { spotifyGreen } from '../../../css-variables/colors'; 4 | 5 | const ActionButton = styled.div` 6 | pointer-events: none; 7 | position: absolute; 8 | top: calc(50% - 16px); 9 | `; 10 | 11 | export const Pause = styled(ActionButton)` 12 | border-left: 5px solid #fff; 13 | border-right: 5px solid #fff; 14 | height: 32px; 15 | left: calc(50% - 9px); 16 | width: 18px; 17 | `; 18 | 19 | export const Play = styled(ActionButton)` 20 | border: 15px solid transparent; 21 | border-left-color: #fff; 22 | height: 0; 23 | left: calc(50%); 24 | transform: scaleX(1.5); 25 | width: 0; 26 | `; 27 | 28 | export const Wrapper = styled.div` 29 | background-color: ${spotifyGreen}; 30 | border-radius: 50%; 31 | bottom: 16px; 32 | cursor: default; 33 | filter: brightness(1); 34 | height: 40px; 35 | position: absolute; 36 | right: 20px; 37 | transition: transform 0.1s ease; 38 | width: 40px; 39 | 40 | &:hover { 41 | border-width: 2px; 42 | transform: scale(1.1); 43 | } 44 | 45 | &:active { 46 | border-width: 1px; 47 | transform: scale(1); 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /Music/src/components/NowPlaying/NowPlaying.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { cleanup, getByAltText, render } from '@testing-library/react'; 4 | 5 | import NowPlaying from './NowPlaying'; 6 | import { IProps } from './NowPlaying'; 7 | 8 | const props: IProps = { 9 | artist: 'Foo', 10 | src: 'some_url', 11 | title: 'Bar', 12 | }; 13 | 14 | afterAll(cleanup); 15 | 16 | describe('NowPlaying component', () => { 17 | it('should render', () => { 18 | const div = document.createElement('div'); 19 | ReactDOM.render(, div); 20 | ReactDOM.unmountComponentAtNode(div); 21 | }); 22 | 23 | it('should display image, artist name and song title', () => { 24 | const { container, getByText } = render(); 25 | 26 | // artist is displayed 27 | expect(getByText('Foo')).toBeVisible(); 28 | // title is displayed 29 | expect(getByText('Bar')).toBeVisible(); 30 | // image is displayed 31 | const imageEl = getByAltText(container, 'Foo - Bar') as HTMLImageElement; 32 | expect(imageEl).toBeVisible(); 33 | expect(imageEl.src.includes('some_url')).toBeTruthy(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /Music/src/components/Recommend/PlayButton/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { spotifyGreen } from '../../../css-variables/colors'; 4 | 5 | const ActionButton = styled.div` 6 | pointer-events: none; 7 | position: absolute; 8 | top: calc(50% - 16px); 9 | `; 10 | 11 | export const Pause = styled(ActionButton)` 12 | border-left: 5px solid #fff; 13 | border-right: 5px solid #fff; 14 | height: 32px; 15 | left: calc(50% - 9px); 16 | width: 18px; 17 | `; 18 | 19 | export const Play = styled(ActionButton)` 20 | border: 15px solid transparent; 21 | border-left-color: #fff; 22 | height: 0; 23 | left: calc(50%); 24 | transform: scaleX(1.5); 25 | width: 0; 26 | `; 27 | 28 | export const Wrapper = styled.div` 29 | background-color: ${spotifyGreen}; 30 | border-radius: 50%; 31 | bottom: 16px; 32 | cursor: default; 33 | filter: brightness(1); 34 | height: 40px; 35 | position: absolute; 36 | right: 20px; 37 | transition: transform 0.1s ease; 38 | width: 40px; 39 | 40 | &:hover { 41 | border-width: 2px; 42 | transform: scale(1.1); 43 | } 44 | 45 | &:active { 46 | border-width: 1px; 47 | transform: scale(1); 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /Music/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | ΜΟΥΣΙΚΗ 17 | 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Music/src/containers/PlayerContainer/PlayerContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { 5 | IDispatchProps, 6 | IPlaylist, 7 | IStateProps, 8 | PlayerContainer, 9 | } from './PlayerContainer'; 10 | 11 | describe('PlayerContainer component', () => { 12 | it('renders without crashing', () => { 13 | const playlist: IPlaylist[] = [ 14 | { 15 | track: { 16 | album: { images: [{ url: './foo' }] }, 17 | artists: [{ name: 'Foo' }], 18 | name: 'Bar', 19 | preview_url: null, 20 | }, 21 | }, 22 | ]; 23 | 24 | const stateProps: IStateProps = { 25 | isPaused: true, 26 | isPlaying: false, 27 | playlist, 28 | songInd: 0, 29 | }; 30 | 31 | const dispatchProps: IDispatchProps = { 32 | pause: () => null, 33 | playNextTrack: (pl: IPlaylist[], s: number) => null, 34 | playPrevTrack: (pl: IPlaylist[], s: number) => null, 35 | stop: () => null, 36 | unpause: () => null, 37 | }; 38 | 39 | const div = document.createElement('div'); 40 | ReactDOM.render( 41 | , 42 | div 43 | ); 44 | ReactDOM.unmountComponentAtNode(div); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /Music/src/components/CoverArt/styled.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | export const Card = styled.div` 4 | cursor: pointer; 5 | margin-bottom: 35px; 6 | position: relative; 7 | `; 8 | 9 | export const Clipart = styled.div` 10 | backface-visibility: hidden; 11 | background-image: url('${props => props.icon}'); 12 | background-size: cover; 13 | filter: brightness(1); 14 | height: 0; 15 | padding-bottom: 100%; 16 | transition: all 1s ease; 17 | transition: filter 0.3s cubic-bezier(0.3, 0, 0, 1); 18 | width: 100%; 19 | 20 | ${props => 21 | props.hover && 22 | css` 23 | filter: brightness(0.3); 24 | `}; 25 | `; 26 | 27 | export const ClipartWrapper = styled.div` 28 | box-shadow: ${p => (p.shrink ? '0 0 0' : '0 0 10px rgba(0, 0, 0, 0.3)')}; 29 | transform: scale(${p => (p.shrink ? 0.95 : 1)}); 30 | transition: transform 0.1s ease; 31 | `; 32 | 33 | export const Title = styled.p` 34 | color: #fff; 35 | display: block; 36 | font-size: ${p => (p.bigTitle ? '26px' : '14px')}; 37 | font-weight: ${p => (p.bigTitle ? 600 : 400)}; 38 | height: ${p => (p.bigTitle ? '36px' : '20px')}; 39 | margin: 12px 0 4px; 40 | text-align: center; 41 | `; 42 | 43 | export const Wrapper = styled.div` 44 | padding: 0 8px; 45 | max-width: 320px; 46 | width: 100%; 47 | `; 48 | -------------------------------------------------------------------------------- /Music/src/components/PlayerControls/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | flex: 1; 5 | height: 56px; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | export const Controls = styled.div` 13 | align-items: center; 14 | display: flex; 15 | flex-direction: row; 16 | height: 32px; 17 | justify-content: space-between; 18 | align-items: center; 19 | width: 224px; 20 | 21 | & > button { 22 | color: #a0a0a0; 23 | outline: none; 24 | } 25 | `; 26 | 27 | export const PlayButton = styled.button` 28 | background-color: transparent; 29 | border: none; 30 | // color: #fff; 31 | cursor: pointer; 32 | font-size: 2.1rem; 33 | height: 32px; 34 | margin-top: -0.5rem; 35 | padding: 0; 36 | width: 32px; 37 | transition: all 0.1s; 38 | 39 | &:hover { 40 | color: #fff; 41 | transform: scale(1.2); 42 | } 43 | `; 44 | 45 | export const SkipButton = styled.button` 46 | background-color: transparent; 47 | border: none; 48 | color: #fff; 49 | cursor: pointer; 50 | font-size: 1.2rem; 51 | height: 32px; 52 | padding: 0; 53 | width: 32px; 54 | 55 | &:hover { 56 | color: #fff; 57 | } 58 | 59 | &:disabled:hover { 60 | color: #a0a0a0; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistPlayButton/PlaylistPlayButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { cleanup, fireEvent, render } from '@testing-library/react'; 4 | 5 | import PlaylistPlayButton from './PlaylistPlayButton'; 6 | import { IProps } from './PlaylistPlayButton'; 7 | 8 | const mockOnClick = jest.fn(); 9 | 10 | const props: IProps = { 11 | isPlaying: false, 12 | onClick: mockOnClick, 13 | }; 14 | 15 | afterEach(() => { 16 | cleanup(); 17 | mockOnClick.mockReset(); 18 | }); 19 | 20 | describe('PlaylistPlayButton component', () => { 21 | it('should render', () => { 22 | const div = document.createElement('div'); 23 | ReactDOM.render(, div); 24 | ReactDOM.unmountComponentAtNode(div); 25 | }); 26 | 27 | it('should display a button with a text PLAY', () => { 28 | const { getByText } = render(); 29 | 30 | const btn = getByText('PLAY'); 31 | expect(btn).toBeVisible(); 32 | fireEvent.click(btn); 33 | expect(mockOnClick).toBeCalled(); 34 | }); 35 | 36 | it('should display a button with a text PAUSE', () => { 37 | const { getByText } = render( 38 | 39 | ); 40 | 41 | const btn = getByText('PAUSE'); 42 | expect(btn).toBeVisible(); 43 | fireEvent.click(btn); 44 | expect(mockOnClick).toBeCalled(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /Music/src/components/SideNavbar/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { sidebarWidth } from '../../css-variables/layout'; 5 | import { spotifyGray, spotifyGreen } from '../../css-variables/colors'; 6 | 7 | const activeClassName = 'selected'; 8 | 9 | export const Group = styled.div` 10 | border: none; 11 | border-top: 1px solid rgba(255, 255, 255, 0.25); 12 | font-size: 16px; 13 | font-weight: 600; 14 | letter-spacing: 0.24px; 15 | line-height: 26px; 16 | padding: 13px 0 8px 0; 17 | `; 18 | 19 | export const GroupHeader = styled.div` 20 | border: none; 21 | padding-bottom: 10px; 22 | `; 23 | 24 | export const Navbar = styled.nav` 25 | background-color: rgba(0, 0, 0, 0.5); 26 | color: ${spotifyGray}; 27 | font-size: 16px; 28 | font-weight: bold; 29 | height: 100vh; 30 | padding: 24px 24px 95px 24px; 31 | position: fixed; 32 | width: 100%; 33 | max-width: ${sidebarWidth}; 34 | min-width: ${sidebarWidth}; 35 | 36 | & img { 37 | width: 32px; 38 | } 39 | 40 | & hr { 41 | color: ${spotifyGray}; 42 | } 43 | `; 44 | 45 | export const NavItem = styled(NavLink).attrs({ 46 | activeClassName, 47 | })` 48 | align-items: center; 49 | color: ${spotifyGray}; 50 | display: flex; 51 | justify-content: space-between; 52 | 53 | &.${activeClassName} { 54 | color: ${spotifyGreen}; 55 | } 56 | 57 | & .fa-search { 58 | font-size: 1.25rem; 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /Music/src/actions/player-control-actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | import searchPrevTrack from '../utils/searchPrevTrack'; 4 | import skipUnavailableTracks from '../utils/skipUnavailableTracks'; 5 | 6 | export const stopPlay = () => ({ 7 | type: types.STOP_PLAY, 8 | }); 9 | 10 | export const playNextTrack = (playlist, songInd) => dispatch => { 11 | if (songInd === -1) { 12 | return; 13 | } 14 | 15 | const nextSongInd = skipUnavailableTracks(playlist, songInd + 1); 16 | 17 | if (nextSongInd === -1) { 18 | return dispatch(stopPlay()); 19 | } 20 | 21 | dispatch({ type: types.STOP_TRACK }); 22 | 23 | setTimeout(() => { 24 | dispatch({ 25 | activeTrackId: skipUnavailableTracks(playlist, songInd + 1), 26 | type: types.PLAY_NEXT_TRACK, 27 | }); 28 | }, 0); 29 | }; 30 | 31 | export const playPrevTrack = (playlist, songInd) => dispatch => { 32 | if (songInd === -1) { 33 | return; 34 | } 35 | const prevSongInd = searchPrevTrack(playlist, songInd); 36 | if (prevSongInd === -1) { 37 | return { type: 'NOOP' }; 38 | } 39 | dispatch({ type: types.STOP_TRACK }); 40 | setTimeout(() => { 41 | dispatch({ 42 | activeTrackId: prevSongInd, 43 | type: types.PLAY_NEXT_TRACK, 44 | }); 45 | }, 0); 46 | }; 47 | 48 | export const setPause = () => { 49 | return { 50 | type: types.SET_PAUSE, 51 | }; 52 | }; 53 | 54 | export const unpause = () => ({ 55 | type: types.UNPAUSE, 56 | }); 57 | -------------------------------------------------------------------------------- /Authentication/validation/register.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const isEmpty = require("is-empty"); 3 | 4 | module.exports = function validateRegisterInput(data) { 5 | let errors = {}; 6 | 7 | // Convert empty fields to an empty string so we can use validator functions 8 | data.name = !isEmpty(data.name) ? data.name : ""; 9 | data.email = !isEmpty(data.email) ? data.email : ""; 10 | data.password = !isEmpty(data.password) ? data.password : ""; 11 | data.password2 = !isEmpty(data.password2) ? data.password2 : ""; 12 | 13 | // Name checks 14 | if (Validator.isEmpty(data.name)) { 15 | errors.name = "Name field is required"; 16 | } 17 | 18 | // Email checks 19 | if (Validator.isEmpty(data.email)) { 20 | errors.email = "Email field is required"; 21 | } else if (!Validator.isEmail(data.email)) { 22 | errors.email = "Email is invalid"; 23 | } 24 | 25 | // Password checks 26 | if (Validator.isEmpty(data.password)) { 27 | errors.password = "Password field is required"; 28 | } 29 | 30 | if (Validator.isEmpty(data.password2)) { 31 | errors.password2 = "Confirm password field is required"; 32 | } 33 | 34 | if (!Validator.isLength(data.password, { min: 6, max: 30 })) { 35 | errors.password = "Password must be at least 6 characters"; 36 | } 37 | 38 | if (!Validator.equals(data.password, data.password2)) { 39 | errors.password2 = "Passwords must match"; 40 | } 41 | 42 | return { 43 | errors, 44 | isValid: isEmpty(errors) 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /Music/src/components/TrackControlButton/TrackControlButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faPause, faPlay, faVolumeUp } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | import { Wrapper } from './styled'; 6 | 7 | const playIcon = ; 8 | const pauseIcon = ; 9 | const speakerIcon = ; 10 | 11 | export interface IProps { 12 | handlePause: () => void; 13 | handlePlay: () => void; 14 | hasPreview: boolean; 15 | isActive: boolean; 16 | isHovered: boolean; 17 | isPlaying: boolean; 18 | nr: number; 19 | unpause: () => void; 20 | } 21 | 22 | const TrackControlButton: React.SFC = ({ 23 | handlePause, 24 | handlePlay, 25 | hasPreview, 26 | isActive, 27 | isHovered, 28 | isPlaying, 29 | nr, 30 | unpause, 31 | }) => { 32 | let cursorStyle = 'default'; 33 | const showIcon = hasPreview && (isHovered || isPlaying); 34 | let icon = null; 35 | 36 | if (hasPreview) { 37 | cursorStyle = 'pointer'; 38 | if (isHovered && isPlaying) { 39 | icon = pauseIcon; 40 | } else if (isHovered && !isPlaying) { 41 | icon = playIcon; 42 | } else if (isPlaying) { 43 | icon = speakerIcon; 44 | } 45 | } 46 | 47 | return ( 48 | 52 | {showIcon ? icon : ++nr + '.'} 53 | 54 | ); 55 | }; 56 | 57 | export default TrackControlButton; 58 | -------------------------------------------------------------------------------- /Music/src/containers/MainContainer/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { spotifyGray, spotifyGreen } from '../../css-variables/colors'; 5 | 6 | const activeClassName = 'selected'; 7 | 8 | export const Container = styled.div` 9 | font-size: 12.8px; 10 | font-weight: bold; 11 | letter-spacing: 1px; 12 | margin: auto; 13 | padding: 0 28px; 14 | width: 100%; 15 | `; 16 | 17 | export const ListWrapper = styled.ul` 18 | font-weight: bold; 19 | list-style: none; 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: center; 23 | width: 100%; 24 | align-items: center; 25 | 26 | & li { 27 | margin: 10px; 28 | margin-top: 0; 29 | padding: 10px; 30 | padding-top: 0; 31 | } 32 | `; 33 | 34 | export const Navbar = styled.nav` 35 | align-items: center; 36 | display: flex; 37 | flex-direction: column; 38 | height: 86px; 39 | justify-content: center; 40 | min-width: 515px; 41 | padding: 20px 0; 42 | `; 43 | 44 | export const NavItem = styled(NavLink).attrs({ 45 | activeClassName, 46 | })` 47 | color: ${spotifyGray}; 48 | margin: 10px; 49 | padding: 10px; 50 | padding-bottom: 0; 51 | transition-duration: 0.2s; 52 | transition-property: color; 53 | 54 | &.${activeClassName} { 55 | color: white; 56 | position: relative; 57 | 58 | &::after { 59 | content: ''; 60 | position: absolute; 61 | left: 35%; 62 | bottom: -5px; 63 | height: 1px; 64 | width: 20px; 65 | padding-bottom: 5px; 66 | border-bottom: 2px solid ${spotifyGreen}; 67 | } 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /Authentication/client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import jwt_decode from "jwt-decode"; 4 | import setAuthToken from "./utils/setAuthToken"; 5 | 6 | import { setCurrentUser, logoutUser } from "./actions/authActions"; 7 | import { Provider } from "react-redux"; 8 | import store from "./store"; 9 | 10 | import Navbar from "./components/layout/Navbar"; 11 | import Register from "./components/auth/Register"; 12 | import Login from "./components/auth/Login"; 13 | import PrivateRoute from "./components/private-route/PrivateRoute"; 14 | import Dashboard from "./components/dashboard/Dashboard"; 15 | 16 | import "./App.css"; 17 | 18 | if (localStorage.jwtToken) { 19 | const token = localStorage.jwtToken; 20 | setAuthToken(token); 21 | const decoded = jwt_decode(token); 22 | store.dispatch(setCurrentUser(decoded)); 23 | const currentTime = Date.now() / 1000; 24 | if (decoded.exp < currentTime) { 25 | store.dispatch(logoutUser()); 26 | window.location.href = "./login"; 27 | } 28 | } 29 | class App extends Component { 30 | render() { 31 | return ( 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 |
47 |
48 |
49 | ); 50 | } 51 | } 52 | export default App; 53 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistView/styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const DescriptionWrapper = styled.div` 4 | align-items: center; 5 | justify-content: flex-start; 6 | display: flex; 7 | flex-direction: column; 8 | flex: 1; 9 | padding: 0 8px; 10 | position: relative; 11 | text-align: center; 12 | top: 40px; 13 | 14 | @media (max-width: 1199px) { 15 | top: 0; 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: flex-start; 19 | align-items: stretch; 20 | max-width: 100%; 21 | width: 100%; 22 | max-height: 255px; 23 | } 24 | 25 | @media (min-width: 1200px) and (min-width: 1499px) { 26 | } 27 | `; 28 | 29 | export const InfoBox = styled.div` 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: space-around; 33 | position: relative; 34 | `; 35 | 36 | export const Text = styled.div` 37 | color: rgba(255, 255, 255, 0.6); 38 | font-size: 14px; 39 | font-weight: 200; 40 | line-height: 28px; 41 | margin: 12px; 42 | pointer-events: none; 43 | 44 | & > a { 45 | color: currentColor; 46 | } 47 | `; 48 | 49 | export const TracksWrapper = styled.div` 50 | flex: 2; 51 | 52 | @media (max-width: 1199px) { 53 | } 54 | 55 | @media (min-width: 1200px) and (max-width: 1499px) { 56 | flex: 3; 57 | } 58 | `; 59 | 60 | export const Wrapper = styled.div` 61 | display: flex; 62 | flex-direction: row; 63 | color: white; 64 | margin: auto; 65 | max-width: 1480px; 66 | padding: 35px 28px 0; 67 | 68 | @media (min-width: 1200px) and (min-width: 1499px) { 69 | } 70 | 71 | @media (max-width: 1199px) { 72 | display: flex; 73 | flex-direction: column; 74 | min-height: 100vh; 75 | min-width: 515px; 76 | width: 100%; 77 | } 78 | `; 79 | -------------------------------------------------------------------------------- /Music/src/components/TrackControlButton/TrackControlButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fireEvent, render, screen } from '@testing-library/react'; 3 | 4 | import TrackControlButton, { IProps } from './TrackControlButton'; 5 | 6 | const mockHandlePause = jest.fn(); 7 | const mockHandlePlay = jest.fn(); 8 | const mockUnpause = jest.fn(); 9 | 10 | const props: IProps = { 11 | handlePause: mockHandlePause, 12 | handlePlay: mockHandlePlay, 13 | hasPreview: true, 14 | isActive: false, 15 | isHovered: false, 16 | isPlaying: false, 17 | nr: 0, 18 | unpause: mockUnpause, 19 | }; 20 | 21 | afterEach(() => { 22 | mockHandlePause.mockReset(); 23 | mockHandlePlay.mockReset(); 24 | mockUnpause.mockReset(); 25 | }); 26 | 27 | describe('PlaylistPlayButton component', () => { 28 | it('should display an ordinal number', () => { 29 | render(); 30 | 31 | expect(screen.getByText('1.')).toBeVisible(); 32 | }); 33 | 34 | // Following tests are not quite good because they test implementation (depends on the class name) 35 | it('should display play icon', () => { 36 | render(); 37 | 38 | expect(screen.getByRole('img', { hidden: true })).toHaveClass( 39 | 'fa-volume-up' 40 | ); 41 | }); 42 | 43 | it('should display pause icon and pause on click', () => { 44 | render( 45 | 51 | ); 52 | 53 | const btn = screen.getByRole('img', { hidden: true }); 54 | 55 | expect(btn).toHaveClass('fa-pause'); 56 | 57 | fireEvent.click(btn); 58 | 59 | expect(mockHandlePause).toBeCalled(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /Music/src/containers/MainContainer/MainContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | import config from '../../config'; 5 | 6 | import PlaylistSelectorView from '../../components/PlaylistSelectorView'; 7 | import Search from '../../components/Search/Search'; 8 | import Recommend from '../../components/Recommend/Recommend'; 9 | import { Container, ListWrapper, Navbar, NavItem } from './styled'; 10 | 11 | class MainContainer extends Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | FEATURED 18 | NEW RELEASES 19 | SEARCH 20 | RECOMMENDATIONS 21 | {/* DISCOVER */} 22 | 23 | 24 | ( 27 | 32 | )} 33 | /> 34 | ( 37 | 42 | )} 43 | /> 44 | } 47 | /> 48 | } 51 | /> 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default MainContainer; 58 | -------------------------------------------------------------------------------- /Authentication/client/src/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { logoutUser } from "../../actions/authActions"; 5 | import { Button } from "@material-ui/core"; 6 | import Background from '../../img/img5.png'; 7 | 8 | class Dashboard extends Component { 9 | onLogoutClick = (e) => { 10 | e.preventDefault(); 11 | this.props.logoutUser(); 12 | }; 13 | 14 | render() { 15 | const { user } = this.props.auth; 16 | return ( 17 |
25 |
29 |
30 |
31 |

32 | Hey there, {user.name.split(" ")[0]} 33 |

34 | You have logged in to{" "} 35 | 41 | ΜΟΥΣΙΚΗ 42 | 43 |

44 |

45 | 46 | 47 | 48 |
49 | 50 |
51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | Dashboard.propTypes = { 59 | logoutUser: PropTypes.func.isRequired, 60 | auth: PropTypes.object.isRequired, 61 | }; 62 | 63 | const mapStateToProps = (state) => ({ 64 | auth: state.auth, 65 | }); 66 | 67 | export default connect(mapStateToProps, { logoutUser })(Dashboard); 68 | -------------------------------------------------------------------------------- /Music/src/components/PlayerControls/PlayerControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { 4 | faPauseCircle, 5 | faPlayCircle, 6 | } from '@fortawesome/free-regular-svg-icons'; 7 | import { 8 | faStepBackward, 9 | faStepForward, 10 | } from '@fortawesome/free-solid-svg-icons'; 11 | 12 | import { Container, Controls, PlayButton, SkipButton } from './styled'; 13 | 14 | const playButton = ; 15 | const pauseButton = ; 16 | const prevButton = ; 17 | const nextButton = ; 18 | 19 | export interface IProps { 20 | handleNext: (() => void); 21 | handlePause: (() => void); 22 | handlePlay: (() => void); 23 | handlePrev: (() => void); 24 | hasPrevTrack: boolean; 25 | hasNextTrack: boolean; 26 | isPlaying: boolean; 27 | } 28 | 29 | const PlayerControls: React.SFC = ({ 30 | handleNext, 31 | handlePause, 32 | handlePlay, 33 | handlePrev, 34 | hasPrevTrack, 35 | hasNextTrack, 36 | isPlaying, 37 | }) => ( 38 | 39 | 40 | 45 | {prevButton} 46 | 47 | {!isPlaying ? ( 48 | 49 | {playButton} 50 | 51 | ) : ( 52 | 53 | {pauseButton} 54 | 55 | )} 56 | 61 | {nextButton} 62 | 63 | 64 | 65 | ); 66 | 67 | export default PlayerControls; 68 | -------------------------------------------------------------------------------- /Authentication/client/src/actions/authActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import setAuthToken from "../utils/setAuthToken"; 3 | import jwt_decode from "jwt-decode"; 4 | 5 | import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from "./types"; 6 | 7 | // Register User 8 | export const registerUser = (userData, history) => dispatch => { 9 | axios 10 | .post("/api/users/register", userData) 11 | .then(res => history.push("/login")) 12 | .catch(err => 13 | dispatch({ 14 | type: GET_ERRORS, 15 | payload: err.response.data 16 | }) 17 | ); 18 | }; 19 | 20 | // Login - get user token 21 | export const loginUser = userData => dispatch => { 22 | axios 23 | .post("/api/users/login", userData) 24 | .then(res => { 25 | // Save to localStorage 26 | 27 | // Set token to localStorage 28 | const { token } = res.data; 29 | localStorage.setItem("jwtToken", token); 30 | // Set token to Auth header 31 | setAuthToken(token); 32 | // Decode token to get user data 33 | const decoded = jwt_decode(token); 34 | // Set current user 35 | dispatch(setCurrentUser(decoded)); 36 | }) 37 | .catch(err => 38 | dispatch({ 39 | type: GET_ERRORS, 40 | payload: err.response.data 41 | }) 42 | ); 43 | }; 44 | 45 | // Set logged in user 46 | export const setCurrentUser = decoded => { 47 | return { 48 | type: SET_CURRENT_USER, 49 | payload: decoded 50 | }; 51 | }; 52 | 53 | // User loading 54 | export const setUserLoading = () => { 55 | return { 56 | type: USER_LOADING 57 | }; 58 | }; 59 | 60 | // Log user out 61 | export const logoutUser = () => dispatch => { 62 | // Remove token from local storage 63 | localStorage.removeItem("jwtToken"); 64 | // Remove auth header for future requests 65 | setAuthToken(false); 66 | // Set current user to empty object {} which will set isAuthenticated to false 67 | dispatch(setCurrentUser({})); 68 | }; 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music Recommender System 2 | 3 | A basic MERN stack web application that recommends albums to the user based on the genre(s) they choose. This project was done for the subject "Web Technologies" (UE19CS204). 4 | 5 | ## Folders 6 | - Authentication: includes sign in, sign up, and the landing page 7 | - Music: includes featured playlists, new releases, search, and recommendations 8 | 9 | ## Screenshots 10 | - Sign Up: 11 |

12 | 13 |

14 | 15 | - Sign in: 16 |

17 | 18 |

19 | 20 | - Landing Page (Dashboard): 21 |

22 | 23 |

24 | 25 | - Featured Playlists: 26 |

27 | 28 |

29 | 30 | - New Releases: 31 |

32 | 33 |

34 | 35 | - Search: 36 |

37 | 38 |

39 | 40 | - Recommendations: 41 |

42 | 43 |

44 | 45 | - Playing Music: 46 |

47 | 48 |

49 | 50 | ## Installation 51 | To clone the repository, open terminal and type: 52 | ```bash 53 | git clone https://github.com/kartika-nair/MusicRecommender.git 54 | ``` 55 | 56 | ## Execution of Code 57 | Open 2 terminals. 58 | 59 | In terminal 1: 60 | ```bash 61 | cd Authentication 62 | npm i 63 | cd client 64 | npm i 65 | cd .. 66 | npm run dev 67 | ``` 68 | 69 | In terminal 2: 70 | ```bash 71 | cd Music 72 | npm i 73 | npm start 74 | ``` 75 | 76 | ## Team Members 77 | - [Karthik Sairam](https://github.com/karthik-sairam) 78 | - [Kartika Nair](https://github.com/kartika-nair) 79 | - [Maitreyi P](https://github.com/Maitreyi-P) 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /Music/src/containers/TrackContainer/TrackContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import TrackControlButton from '../../components/TrackControlButton'; 4 | 5 | import { Artist, Description, TrackName, Wrapper } from './styled'; 6 | 7 | const formatDuration = ms => { 8 | let sec = Math.floor(ms / 1000); 9 | const min = Math.floor(sec / 60); 10 | sec = `${sec % 60}`; 11 | sec = sec.length < 2 ? 0 + sec : sec; 12 | return `${min}:${sec}`; 13 | }; 14 | 15 | class TrackContainer extends Component { 16 | state = { 17 | showPlayButton: false, 18 | }; 19 | 20 | handleMouseEnter = () => { 21 | this.setState(() => ({ 22 | cursor: this.props.track.preview_url ? 'pointer' : 'default', 23 | showPlayButton: this.props.track.preview_url, 24 | })); 25 | }; 26 | 27 | handleMouseLeave = () => { 28 | this.setState(() => ({ 29 | cursor: 'default', 30 | showPlayButton: false, 31 | })); 32 | }; 33 | 34 | render() { 35 | const { 36 | artists, 37 | isActiveTrack, 38 | track, 39 | nr, 40 | playTrack, 41 | pauseTrack, 42 | isPlaying, 43 | } = this.props; 44 | const { showPlayButton } = this.state; 45 | 46 | return ( 47 | 54 | track.preview_url && playTrack()} 60 | handlePause={() => pauseTrack()} 61 | unpause={this.props.unpause} 62 | isActive={isActiveTrack} 63 | /> 64 | 65 | {track.name} 66 | 67 | {artists && artists.map(artist => artist.name).join(', ')} 68 | {track.album && track.album.name && ` • ${track.album.name}`} 69 | 70 | 71 |
{formatDuration(track.duration_ms)}
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default TrackContainer; 78 | -------------------------------------------------------------------------------- /Music/src/components/Recommend/CoverArt.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PlayButton from './PlayButton'; 4 | import { Card, Clipart, ClipartWrapper, Title, Wrapper } from './styled'; 5 | 6 | class CoverArt extends React.Component { 7 | state = { 8 | hover: this.props.showPlayBtn, 9 | showPlay: true, 10 | showPlayBtn: this.props.showPlayBtn, 11 | shrink: false, 12 | }; 13 | 14 | UNSAFE_componentWillReceiveProps(nextProps) { 15 | if (this.state.showPlayBtn !== nextProps.showPlayBtn) { 16 | this.setState(s => ({ 17 | hover: nextProps.showPlayBtn, 18 | showPlayBtn: nextProps.showPlayBtn, 19 | })); 20 | } 21 | } 22 | 23 | handleMouseOver = () => { 24 | this.setState(() => ({ hover: true })); 25 | }; 26 | 27 | handleMouseLeave = () => { 28 | this.setState(s => ({ hover: false || s.showPlayBtn })); 29 | }; 30 | 31 | handleMouseDown = () => { 32 | this.setState(() => ({ shrink: true })); 33 | }; 34 | 35 | handleMouseUp = () => { 36 | this.setState(() => ({ shrink: false })); 37 | }; 38 | 39 | render() { 40 | const { showPlayBtn, shrink, hover } = this.state; 41 | const link = `http://localhost:4000/albums/${this.props.temp.id}`; 42 | return ( 43 | 44 | 45 | 49 | 54 | 55 | {this.props.playBtn && 56 | (hover || showPlayBtn) && ( 57 | 58 | )} 59 | 60 |
63 | {this.props.temp.name} 64 |
65 |
66 | Artist: {this.props.temp.artist_name} 67 | Tracks: {this.props.temp.total_tracks} 68 |
69 |
70 |
71 | ); 72 | } 73 | } 74 | 75 | export default CoverArt; 76 | -------------------------------------------------------------------------------- /Music/src/components/Search/CoverArt.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PlayButton from './PlayButton'; 4 | import { Card, Clipart, ClipartWrapper, Title, Wrapper } from './styled'; 5 | 6 | class CoverArt extends React.Component { 7 | state = { 8 | hover: this.props.showPlayBtn, 9 | showPlay: true, 10 | showPlayBtn: this.props.showPlayBtn, 11 | shrink: false, 12 | }; 13 | 14 | UNSAFE_componentWillReceiveProps(nextProps) { 15 | if (this.state.showPlayBtn !== nextProps.showPlayBtn) { 16 | this.setState(s => ({ 17 | hover: nextProps.showPlayBtn, 18 | showPlayBtn: nextProps.showPlayBtn, 19 | })); 20 | } 21 | } 22 | 23 | handleMouseOver = () => { 24 | this.setState(() => ({ hover: true })); 25 | }; 26 | 27 | handleMouseLeave = () => { 28 | this.setState(s => ({ hover: false || s.showPlayBtn })); 29 | }; 30 | 31 | handleMouseDown = () => { 32 | this.setState(() => ({ shrink: true })); 33 | }; 34 | 35 | handleMouseUp = () => { 36 | this.setState(() => ({ shrink: false })); 37 | }; 38 | 39 | render() { 40 | const { showPlayBtn, shrink, hover } = this.state; 41 | const link = `http://localhost:4000/albums/${this.props.temp.id}`; 42 | return ( 43 | 44 | 45 | 49 | 54 | 55 | {this.props.playBtn && 56 | (hover || showPlayBtn) && ( 57 | 58 | )} 59 | 60 |
63 | {this.props.temp.name} 64 |
65 |
66 | Artist: {this.props.temp.artist_name} 67 | Tracks: {this.props.temp.total_tracks} 68 |
69 |
70 |
71 | ); 72 | } 73 | } 74 | 75 | export default CoverArt; 76 | -------------------------------------------------------------------------------- /Music/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/", 6 | "dependencies": { 7 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 8 | "@fortawesome/free-regular-svg-icons": "^5.14.0", 9 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 10 | "@fortawesome/react-fontawesome": "^0.1.11", 11 | "@material-ui/core": "^4.11.2", 12 | "axios": "^0.21.0", 13 | "bootstrap": "^4.5.3", 14 | "classnames": "^2.2.5", 15 | "cors": "^2.8.5", 16 | "materialize-css": "^1.0.0-rc.2", 17 | "nanoid": "github:ai/nanoid", 18 | "prop-types": "^15.7.2", 19 | "react": "^16.13.1", 20 | "react-bootstrap": "^1.4.0", 21 | "react-dom": "^16.13.1", 22 | "react-redux": "^7.1.0", 23 | "react-router-dom": "^5.0.1", 24 | "react-uid": "^2.3.1", 25 | "redux": "^4.0.0", 26 | "redux-thunk": "^2.3.0", 27 | "styled-components": "^4.3.1", 28 | "typescript": "^3.9.7" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.11.4", 32 | "@storybook/addon-actions": "^6.0.16", 33 | "@storybook/addon-essentials": "^6.0.16", 34 | "@storybook/addon-links": "^6.0.16", 35 | "@storybook/node-logger": "^6.0.16", 36 | "@storybook/preset-create-react-app": "^3.1.4", 37 | "@storybook/react": "^6.0.16", 38 | "@testing-library/jest-dom": "^5.11.4", 39 | "@testing-library/react": "^10.4.9", 40 | "@types/jest": "24.0.15", 41 | "@types/node": "^14.6.0", 42 | "@types/react": "^16.9.46", 43 | "@types/react-dom": "^16.9.8", 44 | "@types/react-redux": "^7.1.9", 45 | "@types/react-router-dom": "^4.3.5", 46 | "babel-loader": "^8.1.0", 47 | "cross-env": "^7.0.3", 48 | "eslint-config-airbnb": "^16.1.0", 49 | "eslint-plugin-babel": "^4.1.2", 50 | "eslint-plugin-jsx-a11y": "^5.1.1", 51 | "react-is": "^16.13.1", 52 | "react-scripts": "3.4.3", 53 | "tslint": "^5.18.0", 54 | "tslint-config-prettier": "^1.18.0", 55 | "tslint-react": "^4.0.0" 56 | }, 57 | "scripts": { 58 | "start": "cross-env PORT=4000 react-scripts start", 59 | "build": "react-scripts build", 60 | "test": "react-scripts test --coverage", 61 | "test:watch": "react-scripts test --watch", 62 | "eject": "react-scripts eject", 63 | "lint": "prettier --write \"./src/**/*\"", 64 | "storybook": "start-storybook -p 6006 -s public", 65 | "build-storybook": "build-storybook -s public" 66 | }, 67 | "proxy": "https://elegant-croissant.glitch.me/", 68 | "browserslist": [ 69 | ">0.2%", 70 | "not dead", 71 | "not ie <= 11", 72 | "not op_mini all" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /Music/src/components/CoverArt/CoverArt.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import PlayButton from './PlayButton'; 5 | import { Card, Clipart, ClipartWrapper, Title, Wrapper } from './styled'; 6 | 7 | const propTypes = { 8 | bigTitle: PropTypes.bool, 9 | handleClick: PropTypes.func.isRequired, 10 | icon: PropTypes.string.isRequired, 11 | id: PropTypes.string.isRequired, 12 | name: PropTypes.string.isRequired, 13 | playBtn: PropTypes.bool, 14 | }; 15 | 16 | const defaultProps = { 17 | bigTitle: false, 18 | playBtn: true, 19 | }; 20 | 21 | class CoverArt extends React.Component { 22 | state = { 23 | hover: this.props.showPlayBtn, 24 | showPlay: true, 25 | showPlayBtn: this.props.showPlayBtn, 26 | shrink: false, 27 | }; 28 | 29 | UNSAFE_componentWillReceiveProps(nextProps) { 30 | if (this.state.showPlayBtn !== nextProps.showPlayBtn) { 31 | this.setState(s => ({ 32 | hover: nextProps.showPlayBtn, 33 | showPlayBtn: nextProps.showPlayBtn, 34 | })); 35 | } 36 | } 37 | 38 | handleMouseOver = () => { 39 | this.setState(() => ({ hover: true })); 40 | }; 41 | 42 | handleMouseLeave = () => { 43 | this.setState(s => ({ hover: false || s.showPlayBtn })); 44 | }; 45 | 46 | handleMouseDown = () => { 47 | this.setState(() => ({ shrink: true })); 48 | }; 49 | 50 | handleMouseUp = () => { 51 | this.setState(() => ({ shrink: false })); 52 | }; 53 | 54 | handleClick = e => { 55 | const { href, handleClick } = this.props; 56 | const dataName = e.target.dataset.name; 57 | let playClicked = false; 58 | if (dataName === 'play') { 59 | playClicked = true; 60 | } 61 | handleClick(href, playClicked); 62 | }; 63 | 64 | render() { 65 | const { showPlayBtn, shrink, hover } = this.state; 66 | const { bigTitle, icon, name, playBtn } = this.props; 67 | 68 | return ( 69 | 70 | 75 | 80 | 81 | {playBtn && 82 | (hover || showPlayBtn) && ( 83 | 84 | )} 85 | 86 | {name} 87 | 88 | 89 | ); 90 | } 91 | } 92 | 93 | CoverArt.propTypes = propTypes; 94 | CoverArt.defaultProps = defaultProps; 95 | 96 | export default CoverArt; 97 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistSelectorView/PlaylistSelectorView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import * as actions from '../../actions'; 5 | 6 | import CoverArt from '../CoverArt'; 7 | import Loading from '../Loading'; 8 | import { Header, Wrapper } from './styled'; 9 | 10 | import { gridTemplateColumns, rootUrl } from '../../variables'; 11 | 12 | class PlaylistSelectorView extends Component { 13 | componentDidMount() { 14 | const id = this.props.match.params.id; 15 | if (!this.props.selection) { 16 | return this.props.config.onMount(id); 17 | } 18 | } 19 | 20 | componentWillUnmount() { 21 | const { onUnmount } = this.props.config; 22 | if (onUnmount) { 23 | onUnmount(); 24 | } 25 | } 26 | 27 | handleClick = (href, playClicked) => { 28 | const { 29 | activePlaylistHref, 30 | history, 31 | isPaused, 32 | isPlaying, 33 | setPause, 34 | unpause, 35 | } = this.props; 36 | 37 | if (playClicked) { 38 | if (isPlaying && href === activePlaylistHref) { 39 | return isPaused ? unpause() : setPause(); 40 | } else { 41 | return this.props.config.initPlay(href); 42 | } 43 | } 44 | 45 | if (history) { 46 | return history.push(`/${href.replace(rootUrl + '/', '')}`); 47 | } 48 | }; 49 | 50 | render() { 51 | const { 52 | selection, 53 | activePlaylistHref, 54 | isPaused, 55 | isPlaying, 56 | message, 57 | windowWidth, 58 | } = this.props; 59 | 60 | if (!selection) { 61 | return ; 62 | } 63 | 64 | return ( 65 | 66 |
{message}
67 | 68 | {selection.map(item => { 69 | if (!item.images.length) { 70 | return null; 71 | } 72 | return ( 73 | 84 | ); 85 | })} 86 | 87 |
88 | ); 89 | } 90 | } 91 | 92 | const mapStateToProps = (state, props) => ({ 93 | activePlaylistHref: state.playlist.href, 94 | isPaused: state.isPaused, 95 | isPlaying: state.isPlaying, 96 | message: state.sectionMessage || props.config.sectionMessage, 97 | selection: state[props.config.selection], 98 | }); 99 | 100 | const mapDispatchToProps = dispatch => ({ 101 | setPause: () => { 102 | dispatch(actions.setPause()); 103 | }, 104 | unpause: () => { 105 | dispatch(actions.unpause()); 106 | }, 107 | }); 108 | 109 | export default connect( 110 | mapStateToProps, 111 | mapDispatchToProps 112 | )(PlaylistSelectorView); 113 | -------------------------------------------------------------------------------- /Authentication/routes/api/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const bcrypt = require("bcryptjs"); 4 | const jwt = require("jsonwebtoken"); 5 | const keys = require("../../config/keys"); 6 | const passport = require("passport"); 7 | 8 | // Load input validation 9 | const validateRegisterInput = require("../../validation/register"); 10 | const validateLoginInput = require("../../validation/login"); 11 | 12 | // Load User model 13 | const User = require("../../models/User"); 14 | 15 | // @route POST api/users/register 16 | // @desc Register user 17 | // @access Public 18 | router.post("/register", (req, res) => { 19 | // Form validation 20 | 21 | const { errors, isValid } = validateRegisterInput(req.body); 22 | 23 | // Check validation 24 | if (!isValid) { 25 | return res.status(400).json(errors); 26 | } 27 | 28 | User.findOne({ email: req.body.email }).then(user => { 29 | if (user) { 30 | return res.status(400).json({ email: "Email already exists" }); 31 | } else { 32 | const newUser = new User({ 33 | name: req.body.name, 34 | email: req.body.email, 35 | password: req.body.password 36 | }); 37 | 38 | // Hash password before saving in database 39 | bcrypt.genSalt(10, (err, salt) => { 40 | bcrypt.hash(newUser.password, salt, (err, hash) => { 41 | if (err) throw err; 42 | newUser.password = hash; 43 | newUser 44 | .save() 45 | .then(user => res.json(user)) 46 | .catch(err => console.log(err)); 47 | }); 48 | }); 49 | } 50 | }); 51 | }); 52 | 53 | // @route POST api/users/login 54 | // @desc Login user and return JWT token 55 | // @access Public 56 | router.post("/login", (req, res) => { 57 | // Form validation 58 | 59 | const { errors, isValid } = validateLoginInput(req.body); 60 | 61 | // Check validation 62 | if (!isValid) { 63 | return res.status(400).json(errors); 64 | } 65 | 66 | const email = req.body.email; 67 | const password = req.body.password; 68 | 69 | // Find user by email 70 | User.findOne({ email }).then(user => { 71 | // Check if user exists 72 | if (!user) { 73 | return res.status(404).json({ emailnotfound: "Email not found" }); 74 | } 75 | 76 | // Check password 77 | bcrypt.compare(password, user.password).then(isMatch => { 78 | if (isMatch) { 79 | // User matched 80 | // Create JWT Payload 81 | const payload = { 82 | id: user.id, 83 | name: user.name 84 | }; 85 | 86 | // Sign token 87 | jwt.sign( 88 | payload, 89 | keys.secretOrKey, 90 | { 91 | expiresIn: 31556926 // 1 year in seconds 92 | }, 93 | (err, token) => { 94 | res.json({ 95 | success: true, 96 | token: "Bearer " + token 97 | }); 98 | } 99 | ); 100 | } else { 101 | return res 102 | .status(400) 103 | .json({ passwordincorrect: "Password incorrect" }); 104 | } 105 | }); 106 | }); 107 | }); 108 | 109 | module.exports = router; 110 | -------------------------------------------------------------------------------- /Music/src/components/Recommend/styled.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { spotifyGray, spotifyGreen } from '../../css-variables/colors'; 5 | 6 | const activeClassName = 'selected'; 7 | 8 | export const Card = styled.div` 9 | cursor: pointer; 10 | margin-bottom: 35px; 11 | position: relative; 12 | `; 13 | 14 | export const Clipart = styled.div` 15 | backface-visibility: hidden; 16 | background-image: url('${props => props.icon}'); 17 | background-size: cover; 18 | filter: brightness(1); 19 | height: 0; 20 | padding-bottom: 100%; 21 | transition: all 1s ease; 22 | transition: filter 0.3s cubic-bezier(0.3, 0, 0, 1); 23 | width: 100%; 24 | 25 | ${props => 26 | props.hover && 27 | css` 28 | filter: brightness(0.3); 29 | `}; 30 | `; 31 | 32 | export const ClipartWrapper = styled.div` 33 | box-shadow: ${p => (p.shrink ? '0 0 0' : '0 0 10px rgba(0, 0, 0, 0.3)')}; 34 | transform: scale(${p => (p.shrink ? 0.95 : 1)}); 35 | transition: transform 0.1s ease; 36 | `; 37 | 38 | export const Title = styled.p` 39 | color: #fff; 40 | display: block; 41 | font-size: ${p => (p.bigTitle ? '26px' : '18px')}; 42 | font-weight: ${p => (p.bigTitle ? 600 : 400)}; 43 | height: ${p => (p.bigTitle ? '30px' : '10px')}; 44 | margin: 12px 0 4px; 45 | text-align: center; 46 | `; 47 | 48 | export const Wrapper = styled.div` 49 | padding: 0 8px; 50 | max-width: 320px; 51 | width: 100%; 52 | `; 53 | 54 | 55 | export const DescriptionWrapper = styled.div` 56 | align-items: center; 57 | justify-content: flex-start; 58 | display: flex; 59 | flex-direction: column; 60 | flex: 1; 61 | padding: 0 8px; 62 | position: relative; 63 | text-align: center; 64 | top: 40px; 65 | 66 | @media (max-width: 1199px) { 67 | top: 0; 68 | display: flex; 69 | flex-direction: row; 70 | justify-content: flex-start; 71 | align-items: stretch; 72 | max-width: 100%; 73 | width: 100%; 74 | max-height: 255px; 75 | } 76 | 77 | @media (min-width: 1200px) and (min-width: 1499px) { 78 | } 79 | `; 80 | 81 | export const ListWrapper = styled.ul` 82 | font-weight: bold; 83 | list-style: none; 84 | display: flex; 85 | flex-direction: row; 86 | justify-content: center; 87 | width: 100%; 88 | align-items: center; 89 | 90 | & li { 91 | margin: 10px; 92 | margin-top: 0; 93 | padding: 10px; 94 | padding-top: 0; 95 | } 96 | `; 97 | 98 | export const Navbar = styled.nav` 99 | align-items: center; 100 | display: flex; 101 | flex-direction: column; 102 | height: 86px; 103 | justify-content: center; 104 | min-width: 515px; 105 | padding: 20px 0; 106 | `; 107 | 108 | export const NavItem = styled(NavLink).attrs({ 109 | activeClassName, 110 | })` 111 | color: ${spotifyGray}; 112 | margin: 10px; 113 | padding: 10px; 114 | padding-bottom: 0; 115 | transition-duration: 0.2s; 116 | transition-property: color; 117 | 118 | &.${activeClassName} { 119 | color: white; 120 | position: relative; 121 | 122 | &::after { 123 | content: ''; 124 | position: absolute; 125 | left: 35%; 126 | bottom: -5px; 127 | height: 1px; 128 | width: 20px; 129 | padding-bottom: 5px; 130 | border-bottom: 2px solid ${spotifyGreen}; 131 | } 132 | } 133 | `; 134 | 135 | export const Container = styled.div` 136 | font-size: 12.8px; 137 | font-weight: bold; 138 | letter-spacing: 1px; 139 | margin: auto; 140 | padding: 0 28px; 141 | width: 100%; 142 | `; -------------------------------------------------------------------------------- /Music/src/components/Search/styled.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { spotifyGray, spotifyGreen } from '../../css-variables/colors'; 5 | 6 | const activeClassName = 'selected'; 7 | 8 | export const Card = styled.div` 9 | cursor: pointer; 10 | margin-bottom: 35px; 11 | position: relative; 12 | `; 13 | 14 | export const Clipart = styled.div` 15 | backface-visibility: hidden; 16 | background-image: url('${props => props.icon}'); 17 | background-size: cover; 18 | filter: brightness(1); 19 | height: 0; 20 | padding-bottom: 100%; 21 | transition: all 1s ease; 22 | transition: filter 0.3s cubic-bezier(0.3, 0, 0, 1); 23 | width: 100%; 24 | 25 | ${props => 26 | props.hover && 27 | css` 28 | filter: brightness(0.3); 29 | `}; 30 | `; 31 | 32 | export const ClipartWrapper = styled.div` 33 | box-shadow: ${p => (p.shrink ? '0 0 0' : '0 0 10px rgba(0, 0, 0, 0.3)')}; 34 | transform: scale(${p => (p.shrink ? 0.95 : 1)}); 35 | transition: transform 0.1s ease; 36 | `; 37 | 38 | export const Title = styled.p` 39 | color: #fff; 40 | display: block; 41 | font-size: ${p => (p.bigTitle ? '26px' : '18px')}; 42 | font-weight: ${p => (p.bigTitle ? 600 : 400)}; 43 | height: ${p => (p.bigTitle ? '30px' : '10px')}; 44 | margin: 12px 0 4px; 45 | text-align: center; 46 | `; 47 | 48 | export const Wrapper = styled.div` 49 | padding: 0 8px; 50 | max-width: 320px; 51 | width: 100%; 52 | `; 53 | 54 | 55 | export const DescriptionWrapper = styled.div` 56 | align-items: center; 57 | justify-content: flex-start; 58 | display: flex; 59 | flex-direction: column; 60 | flex: 1; 61 | padding: 0 8px; 62 | position: relative; 63 | text-align: center; 64 | top: 40px; 65 | 66 | @media (max-width: 1199px) { 67 | top: 0; 68 | display: flex; 69 | flex-direction: row; 70 | justify-content: flex-start; 71 | align-items: stretch; 72 | max-width: 100%; 73 | width: 100%; 74 | max-height: 255px; 75 | } 76 | 77 | @media (min-width: 1200px) and (min-width: 1499px) { 78 | } 79 | `; 80 | 81 | export const ListWrapper = styled.ul` 82 | font-weight: bold; 83 | list-style: none; 84 | display: flex; 85 | flex-direction: row; 86 | justify-content: center; 87 | width: 100%; 88 | align-items: center; 89 | 90 | & li { 91 | margin: 10px; 92 | margin-top: 0; 93 | padding: 10px; 94 | padding-top: 0; 95 | } 96 | `; 97 | 98 | export const Navbar = styled.nav` 99 | align-items: center; 100 | display: flex; 101 | flex-direction: column; 102 | height: 86px; 103 | justify-content: center; 104 | min-width: 515px; 105 | padding: 20px 0; 106 | `; 107 | 108 | export const NavItem = styled(NavLink).attrs({ 109 | activeClassName, 110 | })` 111 | color: ${spotifyGray}; 112 | margin: 10px; 113 | padding: 10px; 114 | padding-bottom: 0; 115 | transition-duration: 0.2s; 116 | transition-property: color; 117 | 118 | &.${activeClassName} { 119 | color: white; 120 | position: relative; 121 | 122 | &::after { 123 | content: ''; 124 | position: absolute; 125 | left: 35%; 126 | bottom: -5px; 127 | height: 1px; 128 | width: 20px; 129 | padding-bottom: 5px; 130 | border-bottom: 2px solid ${spotifyGreen}; 131 | } 132 | } 133 | `; 134 | 135 | export const Container = styled.div` 136 | font-size: 12.8px; 137 | font-weight: bold; 138 | letter-spacing: 1px; 139 | margin: auto; 140 | padding: 0 28px; 141 | width: 100%; 142 | `; -------------------------------------------------------------------------------- /Music/src/containers/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { 4 | BrowserRouter as Router, 5 | Redirect, 6 | Route, 7 | Switch, 8 | } from 'react-router-dom'; 9 | 10 | import Loading from '../../components/Loading'; 11 | import PlaylistSelectorView from '../../components/PlaylistSelectorView'; 12 | import PlaylistView from '../../components/PlaylistView'; 13 | import SideNavbar from '../../components/SideNavbar'; 14 | import Search from '../../components/Search/Search'; 15 | import Recommend from '../../components/Recommend/Recommend'; 16 | import MainContainer from '../MainContainer'; 17 | import PlayerContainer from '../PlayerContainer'; 18 | 19 | import * as actions from '../../actions'; 20 | import store from '../../store'; 21 | 22 | import config from '../../config'; 23 | 24 | import { Background, Section, Wrapper } from './styled'; 25 | 26 | class App extends Component { 27 | state = { 28 | tokenLoaded: false, 29 | windowWidth: window.innerWidth - 220, 30 | }; 31 | 32 | // Update the token once an hour 33 | tokenInterval = setInterval(() => { 34 | store.dispatch(actions.fetchToken()); 35 | }, 3500 * 1000); 36 | 37 | componentDidMount() { 38 | window.addEventListener('resize', this.handleResize); 39 | store.dispatch(actions.fetchToken()).then(() => { 40 | this.setState(() => ({ tokenLoaded: true })); 41 | }); 42 | } 43 | 44 | componentWillUnmount() { 45 | window.removeEventListener('resize', this.handleResize); 46 | clearInterval(this.tokenInterval); 47 | } 48 | 49 | handleResize = (e: any) => { 50 | const windowWidth = e.target.innerWidth - 220; // magic number 220 is sidebar width 51 | this.setState(() => ({ windowWidth })); 52 | }; 53 | 54 | render() { 55 | if (!this.state.tokenLoaded) { 56 | return ; 57 | } 58 | 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 | 68 | 69 | 73 | 77 | 78 | ( 81 | 85 | )} 86 | /> 87 | ( 90 | 94 | )} 95 | /> 96 | ( 99 | 104 | )} 105 | /> 106 | } 109 | /> 110 | } 113 | />; 114 | 115 |
116 | 117 |
118 |
119 |
120 | ); 121 | } 122 | } 123 | 124 | export default App; 125 | -------------------------------------------------------------------------------- /Music/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | 3 | const initState = { 4 | activeTrackId: -1, 5 | categoryPlaylist: null, 6 | featured: null, 7 | genres: [], 8 | isPaused: false, 9 | isPlaying: false, 10 | newReleases: null, 11 | playlist: {}, 12 | playlistView: {}, 13 | sectionMessage: null, 14 | token: null, 15 | tracklist: null, 16 | tracklistView: null, 17 | }; 18 | 19 | const playReducer = (state = initState, action) => { 20 | switch (action.type) { 21 | case types.PLAY_NEXT_TRACK: 22 | return { 23 | ...state, 24 | activeTrackId: action.activeTrackId, 25 | isPaused: false, 26 | isPlaying: true, 27 | }; 28 | 29 | case types.STOP_TRACK: { 30 | return { 31 | ...state, 32 | isPaused: false, 33 | isPlaying: false, 34 | }; 35 | } 36 | 37 | case types.TOKEN_SET: 38 | return { 39 | ...state, 40 | token: action.token, 41 | }; 42 | 43 | case types.FEATURED_SET: 44 | return { 45 | ...state, 46 | featured: action.featured, 47 | sectionMessage: action.sectionMessage, 48 | tracklist: action.tracklist, 49 | }; 50 | 51 | case types.GENRES_SET: 52 | return { 53 | ...state, 54 | genres: action.genres, 55 | }; 56 | 57 | case types.NEW_RELEASES_SET: 58 | return { 59 | ...state, 60 | newReleases: action.albums, 61 | sectionMessage: null, 62 | }; 63 | 64 | case types.CATEGORY_PLAYLIST_SET: 65 | return { 66 | ...state, 67 | categoryPlaylist: action.categoryPlaylist, 68 | }; 69 | 70 | case types.CATEGORY_PLAYLIST_CLEAR: 71 | return { 72 | ...state, 73 | categoryPlaylist: null, 74 | }; 75 | 76 | case types.ALBUM_UPDATE: 77 | return { 78 | ...state, 79 | activeTrackId: action.activeTrackId, 80 | isPaused: false, 81 | isPlaying: true, 82 | playlist: { 83 | ...state.playlist, 84 | ...action.playlist, 85 | }, 86 | tracklist: action.tracks, 87 | }; 88 | 89 | case types.PLAYLIST_SET: 90 | return { 91 | ...state, 92 | activeTrackId: action.trackId, 93 | isPaused: false, 94 | isPlaying: true, 95 | playlist: { 96 | ...state.playlist, 97 | ...action.playlist, 98 | }, 99 | tracklist: [...action.tracks], 100 | }; 101 | 102 | case types.PLAY_TRACK: 103 | return { 104 | ...state, 105 | activeTrackId: action.trackId, 106 | isPaused: false, 107 | isPlaying: true, 108 | }; 109 | 110 | case types.SET_PAUSE: 111 | return { 112 | ...state, 113 | isPaused: true, 114 | isPlaying: true, 115 | }; 116 | 117 | case types.COPY_TO_VIEW: 118 | return { 119 | ...state, 120 | playlistView: { ...state.playlist }, 121 | tracklistView: [...state.tracklist], 122 | }; 123 | 124 | case types.COPY_FROM_VIEW_AND_PLAY: 125 | return { 126 | ...state, 127 | activeTrackId: action.trackId, 128 | isPaused: false, 129 | isPlaying: true, 130 | playlist: { ...state.playlistView }, 131 | tracklist: [...state.tracklistView], 132 | }; 133 | 134 | case types.CLEAR_PLAYLIST_VIEW: 135 | return { 136 | ...state, 137 | playlistView: {}, 138 | tracklistView: null, 139 | }; 140 | 141 | case types.UNPAUSE: 142 | return { 143 | ...state, 144 | isPaused: false, 145 | isPlaying: true, 146 | }; 147 | 148 | case types.SET_PLAYLIST_VIEW: 149 | return { 150 | ...state, 151 | playlistView: { ...action.playlist }, 152 | tracklistView: [...action.tracks], 153 | }; 154 | 155 | case types.STOP_PLAY: 156 | return { 157 | ...state, 158 | activeTrackId: -1, 159 | isPaused: false, 160 | isPlaying: false, 161 | playlist: {}, 162 | tracklist: null, 163 | }; 164 | 165 | default: 166 | return state; 167 | } 168 | }; 169 | 170 | export default playReducer; 171 | -------------------------------------------------------------------------------- /Authentication/client/src/components/auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { connect } from "react-redux"; 5 | import { loginUser } from "../../actions/authActions"; 6 | import classnames from "classnames"; 7 | import { Button } from "@material-ui/core"; 8 | import classes from "./Auth.module.css"; 9 | import Background from '../../img/img7.jpg'; 10 | 11 | class Login extends Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | email: "", 16 | password: "", 17 | errors: {}, 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | // If logged in and user navigates to Login page, should redirect them to dashboard 23 | if (this.props.auth.isAuthenticated) { 24 | this.props.history.push("/dashboard"); 25 | } 26 | } 27 | 28 | UNSAFE_componentWillReceiveProps(nextProps) { 29 | if (nextProps.auth.isAuthenticated) { 30 | this.props.history.push("/dashboard"); 31 | } 32 | 33 | if (nextProps.errors) { 34 | this.setState({ 35 | errors: nextProps.errors, 36 | }); 37 | } 38 | } 39 | 40 | onChange = (e) => { 41 | this.setState({ [e.target.id]: e.target.value }); 42 | }; 43 | 44 | onSubmit = (e) => { 45 | e.preventDefault(); 46 | 47 | const userData = { 48 | email: this.state.email, 49 | password: this.state.password, 50 | }; 51 | 52 | this.props.loginUser(userData); 53 | }; 54 | 55 | render() { 56 | const { errors } = this.state; 57 | 58 | return ( 59 |
66 |
67 |
68 |
69 |
73 |

74 | SIGN IN 75 |

76 |
77 |
82 |
83 | 95 | 96 | 97 | {errors.email} 98 | {errors.emailnotfound} 99 | 100 |
101 |
102 | 114 | 115 | 116 | {errors.password} 117 | {errors.passwordincorrect} 118 | 119 |
120 |
124 | 125 |
126 |
127 |
131 |

132 | No account?{" "} 133 | Sign Up! 134 |

135 |
136 |
137 |
138 |
139 |
140 | ); 141 | } 142 | } 143 | 144 | Login.propTypes = { 145 | loginUser: PropTypes.func.isRequired, 146 | auth: PropTypes.object.isRequired, 147 | errors: PropTypes.object.isRequired, 148 | }; 149 | 150 | const mapStateToProps = (state) => ({ 151 | auth: state.auth, 152 | errors: state.errors, 153 | }); 154 | 155 | export default connect(mapStateToProps, { loginUser })(Login); 156 | -------------------------------------------------------------------------------- /Music/src/components/PlaylistView/PlaylistView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import * as actions from '../../actions'; 5 | 6 | import CoverArt from '../CoverArt'; 7 | import PlaylistPlayButton from '../PlaylistPlayButton'; 8 | import TrackContainer from '../../containers/TrackContainer'; 9 | import Loading from '../Loading'; 10 | import { 11 | DescriptionWrapper, 12 | InfoBox, 13 | Text, 14 | TracksWrapper, 15 | Wrapper, 16 | } from './styled'; 17 | 18 | import { rootUrl } from '../../variables'; 19 | 20 | class PlaylistView extends Component { 21 | componentDidMount() { 22 | this.props.fetchPlaylistView(this.props.currentPlaylistHref); 23 | } 24 | 25 | componentWillUnmount() { 26 | this.props.clearPlaylistView(); 27 | } 28 | 29 | handleButton = () => { 30 | const { 31 | isPaused, 32 | isPlaying, 33 | isActivePlaylist, 34 | setPause, 35 | unpause, 36 | startPlay, 37 | } = this.props; 38 | if (isActivePlaylist && isPlaying && !isPaused) { 39 | return setPause(); 40 | } 41 | if (isActivePlaylist && isPlaying && isPaused) { 42 | return unpause(); 43 | } 44 | if (!isActivePlaylist) { 45 | return startPlay(); 46 | } 47 | }; 48 | 49 | render() { 50 | const { 51 | activePlaylistHref, 52 | activeTrackId, 53 | currentPlaylistHref, 54 | isPaused, 55 | isPlaying, 56 | playlist, 57 | setPause, 58 | startPlay, 59 | tracklist, 60 | isActivePlaylist, 61 | } = this.props; 62 | 63 | if (!tracklist) { 64 | return ; 65 | } 66 | 67 | return ( 68 | 69 | 70 | 83 | 84 | 85 | 86 | {tracklist.length} song{tracklist.length > 1 ? 's' : ''} 87 | 88 | 92 | 93 | 94 | 95 | {tracklist.map(({ track }, i) => { 96 | return ( 97 | startPlay(+i)} 106 | pauseTrack={() => setPause()} 107 | unpause={this.props.unpause} 108 | activeTrackId={isActivePlaylist ? activeTrackId : null} 109 | /> 110 | ); 111 | })} 112 | 113 | 114 | ); 115 | } 116 | } 117 | 118 | const mapStateToProps = (state, props) => ({ 119 | activePlaylistHref: state.playlist.href, 120 | activeTrackId: state.activeTrackId, 121 | currentPlaylistHref: rootUrl + props.history.location.pathname, 122 | isActivePlaylist: 123 | state.playlist.href === rootUrl + props.history.location.pathname, 124 | isPaused: state.isPaused, 125 | isPlaying: state.isPlaying, 126 | playlist: state.playlistView, 127 | tracklist: state.tracklistView, 128 | }); 129 | 130 | const mapDispatchToProps = (dispatch, getState) => ({ 131 | clearPlaylistView: () => { 132 | dispatch(actions.clearPlaylistView()); 133 | }, 134 | fetchPlaylistView: href => { 135 | dispatch(actions.fetchPlaylistView(href)); 136 | }, 137 | setPause: () => { 138 | dispatch(actions.setPause()); 139 | }, 140 | startPlay: track => { 141 | dispatch(actions.startPlayFromTracklist(track)); 142 | }, 143 | unpause: () => { 144 | dispatch(actions.unpause()); 145 | }, 146 | }); 147 | 148 | export default connect( 149 | mapStateToProps, 150 | mapDispatchToProps 151 | )(PlaylistView); 152 | -------------------------------------------------------------------------------- /Music/src/components/PlayerControls/PlayerControls.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { cleanup, fireEvent, render } from '@testing-library/react'; 4 | 5 | import PlayerControls from './PlayerControls'; 6 | import { IProps } from './PlayerControls'; 7 | 8 | const mockNext = jest.fn(); 9 | const mockPause = jest.fn(); 10 | const mockPlay = jest.fn(); 11 | const mockPrev = jest.fn(); 12 | 13 | const props: IProps = { 14 | handleNext: mockNext, 15 | handlePause: mockPause, 16 | handlePlay: mockPlay, 17 | handlePrev: mockPrev, 18 | hasNextTrack: false, 19 | hasPrevTrack: false, 20 | isPlaying: false, 21 | }; 22 | 23 | afterEach(() => { 24 | cleanup(); 25 | mockNext.mockReset(); 26 | mockPause.mockReset(); 27 | mockPlay.mockReset(); 28 | mockPrev.mockReset(); 29 | }); 30 | 31 | describe('NowPlaying component', () => { 32 | it('should render', () => { 33 | const div = document.createElement('div'); 34 | ReactDOM.render(, div); 35 | ReactDOM.unmountComponentAtNode(div); 36 | }); 37 | 38 | it('should display play button and two disabled skip buttons', () => { 39 | const { getByTestId, queryByTestId } = render( 40 | 41 | ); 42 | 43 | // show play button 44 | expect(getByTestId('play-btn')).toBeVisible(); 45 | expect(getByTestId('play-btn')).not.toBeDisabled(); 46 | // doesn't show pause button 47 | expect(queryByTestId('pause-btn')).toBeNull(); 48 | // show disabled prev button 49 | expect(getByTestId('prev-btn')).toBeVisible(); 50 | expect(getByTestId('prev-btn')).toBeDisabled(); 51 | // show disabled next button 52 | expect(getByTestId('next-btn')).toBeVisible(); 53 | expect(getByTestId('next-btn')).toBeDisabled(); 54 | 55 | // user can click play button 56 | fireEvent.click(getByTestId('play-btn')); 57 | expect(mockPlay).toBeCalled(); 58 | 59 | // user can't click next button 60 | fireEvent.click(getByTestId('next-btn')); 61 | expect(mockNext).not.toBeCalled(); 62 | 63 | // user can't click prev button 64 | fireEvent.click(getByTestId('prev-btn')); 65 | expect(mockPrev).not.toBeCalled(); 66 | }); 67 | 68 | it('should display pause button and two disabled skip buttons', () => { 69 | const { getByTestId, queryByTestId } = render( 70 | 71 | ); 72 | 73 | // doesn't show play button 74 | expect(queryByTestId('play-btn')).toBeNull(); 75 | // show pause button 76 | expect(getByTestId('pause-btn')).toBeVisible(); 77 | expect(getByTestId('pause-btn')).not.toBeDisabled(); 78 | // show disabled prev button 79 | expect(getByTestId('prev-btn')).toBeVisible(); 80 | expect(getByTestId('prev-btn')).toBeDisabled(); 81 | // show disabled next button 82 | expect(getByTestId('next-btn')).toBeVisible(); 83 | expect(getByTestId('next-btn')).toBeDisabled(); 84 | 85 | // user can click pause button 86 | fireEvent.click(getByTestId('pause-btn')); 87 | expect(mockPause).toBeCalled(); 88 | 89 | // user can't click next button 90 | fireEvent.click(getByTestId('next-btn')); 91 | expect(mockNext).not.toBeCalled(); 92 | 93 | // user can't click prev button 94 | fireEvent.click(getByTestId('prev-btn')); 95 | expect(mockPrev).not.toBeCalled(); 96 | }); 97 | 98 | it('should display pause button and two skip buttons', () => { 99 | const { getByTestId, queryByTestId } = render( 100 | 106 | ); 107 | 108 | // doesn't show play button 109 | expect(queryByTestId('play-btn')).toBeNull(); 110 | // show pause button 111 | expect(getByTestId('pause-btn')).toBeVisible(); 112 | expect(getByTestId('pause-btn')).not.toBeDisabled(); 113 | // show prev button 114 | expect(getByTestId('prev-btn')).toBeVisible(); 115 | expect(getByTestId('prev-btn')).not.toBeDisabled(); 116 | // show next button 117 | expect(getByTestId('next-btn')).toBeVisible(); 118 | expect(getByTestId('next-btn')).not.toBeDisabled(); 119 | 120 | // user can click pause button 121 | fireEvent.click(getByTestId('pause-btn')); 122 | expect(mockPause).toBeCalled(); 123 | 124 | // user can click next button 125 | fireEvent.click(getByTestId('next-btn')); 126 | expect(mockNext).toBeCalled(); 127 | 128 | // user can click prev button 129 | fireEvent.click(getByTestId('prev-btn')); 130 | expect(mockPrev).toBeCalled(); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /Authentication/client/src/components/auth/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { withRouter } from "react-router-dom"; 4 | import PropTypes from "prop-types"; 5 | import { connect } from "react-redux"; 6 | import { registerUser } from "../../actions/authActions"; 7 | import classnames from "classnames"; 8 | import { Button } from "@material-ui/core"; 9 | import classes from "./Auth.module.css"; 10 | import Background from '../../img/img7.jpg'; 11 | 12 | class Register extends Component { 13 | constructor() { 14 | super(); 15 | this.state = { 16 | name: "", 17 | email: "", 18 | password: "", 19 | password2: "", 20 | errors: {}, 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | if (this.props.auth.isAuthenticated) { 26 | this.props.history.push("/dashboard"); 27 | } 28 | } 29 | 30 | UNSAFE_componentWillReceiveProps(nextProps) { 31 | if (nextProps.errors) { 32 | this.setState({ 33 | errors: nextProps.errors, 34 | }); 35 | } 36 | } 37 | 38 | onChange = (e) => { 39 | this.setState({ [e.target.id]: e.target.value }); 40 | }; 41 | 42 | onSubmit = (e) => { 43 | e.preventDefault(); 44 | 45 | const newUser = { 46 | name: this.state.name, 47 | email: this.state.email, 48 | password: this.state.password, 49 | password2: this.state.password2, 50 | }; 51 | 52 | this.props.registerUser(newUser, this.props.history); 53 | }; 54 | 55 | render() { 56 | const { errors } = this.state; 57 | 58 | return ( 59 |
66 |
67 |
68 |
69 |
73 |

74 | SIGN UP 75 |

76 |
77 |
82 |
83 | 93 | 94 | {errors.name} 95 |
96 |
97 | 107 | 108 | {errors.email} 109 |
110 |
111 | 121 | 122 | 123 | {errors.password} 124 | 125 |
126 |
127 | 137 | 140 | 141 | {errors.password2} 142 | 143 |
144 |
148 | 149 |
150 |
151 |
155 |

156 | Already have an account?{" "} 157 | Sign In! 158 |

159 |
160 |
161 |
162 |
163 |
164 | ); 165 | } 166 | } 167 | 168 | Register.propTypes = { 169 | registerUser: PropTypes.func.isRequired, 170 | auth: PropTypes.object.isRequired, 171 | errors: PropTypes.object.isRequired, 172 | }; 173 | 174 | const mapStateToProps = (state) => ({ 175 | auth: state.auth, 176 | errors: state.errors, 177 | }); 178 | 179 | export default connect(mapStateToProps, { registerUser })(withRouter(Register)); 180 | -------------------------------------------------------------------------------- /Authentication/client/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 http://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.1/8 is 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 http://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 http://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 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Music/src/components/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | //import {uid} from 'react-uid'; 3 | import { 4 | DescriptionWrapper, 5 | Wrapper, 6 | ListWrapper, 7 | NavItem, 8 | Navbar, 9 | Container 10 | } from './styled'; 11 | import axios from 'axios'; 12 | import CoverArt from './CoverArt.js'; 13 | import './Search.css'; 14 | import keys from '../../keys.js'; 15 | 16 | export default class Search extends React.Component { 17 | 18 | state = { 19 | clienID: keys.clientID, 20 | clientSecret: keys.clientSecret, 21 | access_token: "", 22 | searchField: "", 23 | albums: [] 24 | } 25 | 26 | componentDidMount() { 27 | this.getToken(); 28 | } 29 | 30 | getToken = async () => { 31 | const result = await fetch('https://accounts.spotify.com/api/token', 32 | { 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/x-www-form-urlencoded', 36 | 'Authorization' : `Basic ` + btoa(this.state.clienID + ':' + this.state.clientSecret) 37 | 38 | }, 39 | body: 'grant_type=client_credentials' 40 | }); 41 | const data = await result.json(); 42 | this.state.access_token = data.access_token; 43 | this.state.data = data; 44 | } 45 | 46 | 47 | 48 | onChangeHandler = (e) => { 49 | const newSearchField = e.target.value.split(' ').join('%20OR%20'); 50 | this.setState({ 51 | searchField: newSearchField 52 | }) 53 | } 54 | 55 | searchHandler = () => { 56 | axios.get(`https://api.spotify.com/v1/search?q=${this.state.searchField}&type=album`, 57 | { 58 | headers: { 59 | Authorization: `Bearer ${this.state.access_token}` 60 | }}) 61 | .then(data => { 62 | var responseData = data.data.albums.items; 63 | var newAlbum = []; 64 | responseData.forEach(element => { 65 | let temp = {}; 66 | temp["id"] = element["id"]; 67 | temp["total_tracks"] = element["total_tracks"]; 68 | temp["name"] = element["name"]; 69 | temp["image"] = element["images"][1]["url"]; 70 | temp["artist_name"] = element["artists"][0]["name"]; 71 | newAlbum.push(temp); 72 | }); 73 | this.setState({ 74 | albums: newAlbum 75 | }) 76 | }) 77 | .catch(err => { 78 | console.log(err); 79 | }) 80 | 81 | } 82 | onKeyUpHandler() { 83 | if (this.state.searchField.length === 0) 84 | return; 85 | this.searchHandler(); 86 | } 87 | 88 | gridTemplateColumns = w => { 89 | switch (true) { 90 | case w <= 547: 91 | return 'repeat(2, minmax(16px, 218px))'; 92 | case w >= 548 && w <= 771: 93 | return 'repeat(3, minmax(146px, 230px))'; 94 | case w >= 772 && w <= 979: 95 | return 'repeat(4, minmax(166px, 217px))'; 96 | default: 97 | return 'repeat(6, minmax(145px, 230px))'; 98 | } 99 | } 100 | 101 | render() { 102 | return ( 103 | 104 | 105 | 106 | 107 | FEATURED 108 | NEW RELEASES 109 | SEARCH 110 | RECOMMENDATIONS 111 | 112 | {/* DISCOVER */} 113 | 114 | 115 | 116 |
117 |
118 | input && input.focus()} 120 | placeholder = "Search for an album" 121 | className = "search-bar" 122 | type="text" 123 | onKeyUp = {() => this.onKeyUpHandler()} 124 | onChange = {(e) => this.onChangeHandler(e)}/> 125 |
126 |
127 |
128 | 129 | { 130 | this.state.albums.length ? 131 | 132 | 133 | { 134 | this.state.albums.map((album, index) => { 135 | return 144 | }) 145 | } 146 | 147 | : null 148 | } 149 | 150 |
151 | 152 |
153 | ); 154 | } 155 | } -------------------------------------------------------------------------------- /Music/src/components/Recommend/Recommend.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | //import {uid} from 'react-uid'; 3 | import { 4 | DescriptionWrapper, 5 | Wrapper, 6 | ListWrapper, 7 | NavItem, 8 | Navbar, 9 | Container 10 | } from './styled'; 11 | import axios from 'axios'; 12 | import CoverArt from './CoverArt.js'; 13 | import keys from '../../keys.js'; 14 | 15 | 16 | export default class Recommend extends React.Component { 17 | 18 | state = { 19 | clientID: keys.clientID, 20 | clientSecret: keys.clientSecret, 21 | access_token: "", 22 | genres: [], 23 | data: {}, 24 | genresInput: "", 25 | seed_artists: "4NHQUGzhtTLFvgF5SZesLK", 26 | seed_tracks: "0c6xIDDpzE81m2q797ordA", 27 | tracks: [], 28 | albums: [] 29 | } 30 | 31 | componentDidMount() { 32 | this.getToken(); 33 | } 34 | 35 | getToken = async () => { 36 | const result = await fetch('https://accounts.spotify.com/api/token', 37 | { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/x-www-form-urlencoded', 41 | 'Authorization' : `Basic ` + btoa(this.state.clientID + ':' + this.state.clientSecret) 42 | 43 | }, 44 | body: 'grant_type=client_credentials' 45 | }); 46 | const data = await result.json(); 47 | this.state.access_token = data.access_token; 48 | this.state.data = data; 49 | } 50 | 51 | getGenres = async () => { 52 | axios.get(`https://api.spotify.com/v1/recommendations/available-genre-seeds`, 53 | { 54 | headers: { 55 | Authorization: `Bearer ${this.state.access_token}` 56 | }}) 57 | .then ((data) => { 58 | this.setState({ 59 | genres: data.data["genres"] 60 | }) 61 | console.log(this.state.genres); 62 | }) 63 | } 64 | 65 | onChangeHandler = (e) => { 66 | const inputGenres = e.target.value.split(' ').join('%20').split(',').join('%2C'); 67 | this.setState({ 68 | genresInput: inputGenres 69 | }) 70 | } 71 | 72 | getRecommendationsHandler = async () => { 73 | axios.get(`https://api.spotify.com/v1/recommendations?seed_artists=4NHQUGzhtTLFvgF5SZesLK&seed_genres=${this.state.genresInput}&seed_tracks=0c6xIDDpzE81m2q797ordA&min_energy=0.4&min_popularity=50`, 74 | { 75 | headers: { 76 | 'Accept': 'application/json', 77 | 'Content-Type': 'application/json', 78 | 'Authorization': `Bearer ${this.state.access_token}`, 79 | } 80 | }) 81 | .then(data => { 82 | this.setState({ 83 | tracks: data.data.tracks 84 | }) 85 | console.log(this.state.tracks) 86 | var newAlbum = []; 87 | this.state.tracks.forEach(track => { 88 | var temp = {}; 89 | temp["id"] = track["album"]["id"]; 90 | temp["total_tracks"] = track["album"]["total_tracks"]; 91 | temp["name"] = track["album"]["name"]; 92 | temp["image"] = track["album"]["images"][1]["url"]; 93 | temp["artist_name"] = track["artists"][0]["name"]; 94 | newAlbum.push(temp); 95 | }) 96 | this.setState({ 97 | albums: newAlbum 98 | }) 99 | }) 100 | .catch(err => { 101 | console.log(err); 102 | }) 103 | } 104 | 105 | keyPressHandler = (e) => { 106 | if (e.key !== 'Enter') { 107 | return; 108 | } 109 | this.getRecommendationsHandler(); 110 | } 111 | 112 | gridTemplateColumns = w => { 113 | switch (true) { 114 | case w <= 547: 115 | return 'repeat(2, minmax(16px, 218px))'; 116 | case w >= 548 && w <= 771: 117 | return 'repeat(3, minmax(146px, 230px))'; 118 | case w >= 772 && w <= 979: 119 | return 'repeat(4, minmax(166px, 217px))'; 120 | default: 121 | return 'repeat(6, minmax(145px, 230px))'; 122 | } 123 | } 124 | 125 | render() { 126 | return ( 127 | 128 | 129 | 130 | 131 | FEATURED 132 | NEW RELEASES 133 | SEARCH 134 | RECOMMENDATIONS 135 | 136 | 137 | 138 |
139 |
140 | input && input.focus()} 142 | placeholder = "Pop, Country, Metal, etc." 143 | className = "search-bar" 144 | type="text" 145 | onChange = {(e) => this.onChangeHandler(e)} 146 | onKeyPress = {(e) => this.keyPressHandler(e)} 147 | /> 148 |
149 |
150 |
151 | 152 | { 153 | this.state.albums.length ? 154 | 155 | { 156 | this.state.albums.map((album, index) => { 157 | return 166 | }) 167 | } 168 | 169 | : null 170 | } 171 | 172 |
173 |
174 | ) 175 | } 176 | } -------------------------------------------------------------------------------- /Music/src/actions/fetch-actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/action-types'; 2 | import skipUnavailableTracks from '../utils/skipUnavailableTracks'; 3 | 4 | const setToken = token => ({ 5 | token, 6 | type: types.TOKEN_SET, 7 | }); 8 | 9 | const setGenres = genres => ({ 10 | genres, 11 | type: types.GENRES_SET, 12 | }); 13 | 14 | const setCategoryPlaylist = categoryPlaylist => ({ 15 | categoryPlaylist, 16 | type: types.CATEGORY_PLAYLIST_SET, 17 | }); 18 | 19 | const fetchWithToken = (url, token) => { 20 | return fetch(url, { 21 | headers: new Headers({ 22 | Authorization: 'Bearer ' + token.access_token, 23 | }), 24 | }).then(res => res.json()); 25 | }; 26 | 27 | export const clearCategoryPlaylist = () => ({ 28 | type: types.CATEGORY_PLAYLIST_CLEAR, 29 | }); 30 | 31 | export const fetchToken = () => dispatch => { 32 | return ( 33 | fetch('https://ndj7ih3fo8.execute-api.eu-central-1.amazonaws.com/LATEST/') 34 | .then(res => res.json()) 35 | .then(token => dispatch(setToken(token))) 36 | // tslint:disable-next-line 37 | .catch(err => console.log('Error fetching Token', err)) 38 | ); // TODO add error handling 39 | }; 40 | 41 | export const fetchFeatured = () => (dispatch, getState) => { 42 | const { token } = getState(); 43 | const url = 'https://api.spotify.com/v1/browse/featured-playlists'; 44 | 45 | return ( 46 | fetchWithToken(url, token) 47 | .then(data => { 48 | if (data.error) { 49 | throw data.error.message; 50 | } 51 | dispatch({ 52 | featured: data.playlists.items, 53 | sectionMessage: data.message, 54 | type: types.FEATURED_SET, 55 | }); 56 | }) 57 | // tslint:disable-next-line 58 | .catch(err => console.log('Error fetching Featured', err)) 59 | ); // TODO add error handling 60 | }; 61 | 62 | export const fetchGenres = () => (dispatch, getState) => { 63 | const { token } = getState(); 64 | const url = 'https://api.spotify.com/v1/browse/categories?limit=50'; 65 | 66 | fetchWithToken(url, token) 67 | .then(data => { 68 | if (data.error) { 69 | throw data.error.message; 70 | } 71 | dispatch(setGenres(data.categories.items)); 72 | }) 73 | // tslint:disable-next-line 74 | .catch(err => console.error(err)); 75 | }; 76 | 77 | export const fetchCategoryPlaylist = categoryId => (dispatch, getState) => { 78 | const { token } = getState(); 79 | const url = `https://api.spotify.com/v1/browse/categories/${categoryId}/playlists?limit=50`; 80 | 81 | fetchWithToken(url, token) 82 | .then(data => { 83 | if (data.error) { 84 | throw data.error.message; 85 | } 86 | dispatch(setCategoryPlaylist(data.playlists.items)); 87 | }) 88 | // tslint:disable-next-line 89 | .catch(err => console.error(err)); 90 | }; 91 | 92 | const setNewReleases = albums => ({ 93 | albums, 94 | type: types.NEW_RELEASES_SET, 95 | }); 96 | 97 | export const fetchNewReleases = () => (dispatch, getState) => { 98 | const { token } = getState(); 99 | const url = 'https://api.spotify.com/v1/browse/new-releases?limit=50'; 100 | 101 | fetchWithToken(url, token).then(data => 102 | dispatch(setNewReleases(data.albums.items)) 103 | ); 104 | }; 105 | 106 | const normalizeTracks = (trackArray, images) => { 107 | return trackArray.map((track, i) => { 108 | return { track: { ...track, album: { images: [...images] } } }; 109 | }); 110 | }; 111 | 112 | export const fetchPlaylistView = href => (dispatch, getState) => { 113 | const { token, playlist } = getState(); 114 | 115 | if (href === playlist.href) { 116 | dispatch({ 117 | type: types.COPY_TO_VIEW, 118 | }); 119 | } else { 120 | fetchWithToken(href, token).then(data => { 121 | const tracks = 122 | data.type === 'album' 123 | ? normalizeTracks(data.tracks.items, data.images) 124 | : data.tracks.items; 125 | dispatch({ 126 | playlist: { 127 | description: data.description, 128 | href, 129 | imageUrl: data.images[0].url, 130 | name: data.name, 131 | owner: data.owner && data.owner.display_name, 132 | type: data.type, 133 | }, 134 | tracks, 135 | type: types.SET_PLAYLIST_VIEW, 136 | }); 137 | }); 138 | } 139 | }; 140 | 141 | // Fetches playlist if not in memory and starts it from first available track 142 | export const startPlaylist = ({ href }) => async (dispatch, getState) => { 143 | let { playlist, tracklist: tracks, token } = getState(); 144 | dispatch({ type: types.STOP_PLAY }); 145 | 146 | if (href !== playlist.href) { 147 | const data = await fetchWithToken(href, token); 148 | if (data.error) { 149 | throw data.error.message; 150 | } 151 | playlist = { 152 | description: data.description, 153 | href, 154 | imageUrl: data.images[0].url, 155 | name: data.name, 156 | owner: data.owner.display_name, 157 | type: data.type, 158 | }; 159 | tracks = data.tracks.items; 160 | } 161 | 162 | const trackId = skipUnavailableTracks(tracks, 0); 163 | dispatch({ 164 | playlist, 165 | trackId, 166 | tracks, 167 | type: types.PLAYLIST_SET, 168 | }); 169 | }; 170 | 171 | // Fetches album if not in memory and starts it from first available track 172 | export const startAlbum = ({ href }) => async (dispatch, getState) => { 173 | let { playlist, token, tracklist } = getState(); 174 | let tracks = tracklist; 175 | 176 | if (href !== playlist.href) { 177 | try { 178 | const data = await fetchWithToken(href, token); 179 | playlist = { 180 | artists: data.artists, 181 | date: data.release_date, 182 | href, 183 | imageUrl: data.images[0].url, 184 | name: data.name, 185 | }; 186 | tracks = normalizeTracks(data.tracks.items, data.images); 187 | } catch (err) { 188 | // tslint:disable-next-line 189 | console.error('Error fetching Album:', err); 190 | } 191 | } 192 | 193 | const activeTrackId = skipUnavailableTracks(tracks, 0); 194 | dispatch({ type: types.STOP_PLAY }); 195 | 196 | dispatch({ 197 | activeTrackId, 198 | playlist, 199 | tracks, 200 | type: types.ALBUM_UPDATE, 201 | }); 202 | }; 203 | 204 | export const clearPlaylistView = () => (dispatch, getState) => { 205 | dispatch({ 206 | type: types.CLEAR_PLAYLIST_VIEW, 207 | }); 208 | }; 209 | 210 | export const startPlayFromTracklist = (track = 0) => { 211 | return (dispatch, getState) => { 212 | const { tracklistView: tracklist } = getState(); 213 | const trackId = skipUnavailableTracks(tracklist, track); 214 | 215 | dispatch({ type: types.STOP_PLAY }); 216 | 217 | setTimeout(() => { 218 | dispatch({ 219 | trackId, 220 | type: types.COPY_FROM_VIEW_AND_PLAY, 221 | }); 222 | }, 0); 223 | }; 224 | }; 225 | -------------------------------------------------------------------------------- /Music/src/containers/PlayerContainer/PlayerContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import { Action, Dispatch } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | import NowPlaying from '../../components/NowPlaying'; 6 | import PlayerControls from '../../components/PlayerControls'; 7 | import VolumeControl from '../../components/VolumeControl'; 8 | 9 | import { Wrapper } from './styled'; 10 | 11 | import logo from './note.png'; 12 | 13 | import * as actions from '../../actions'; 14 | 15 | import searchPrevTrack from '../../utils/searchPrevTrack'; 16 | import skipUnavailableTracks from '../../utils/skipUnavailableTracks'; 17 | 18 | type TProps = IStateProps & IDispatchProps; 19 | 20 | export interface IPlaylist { 21 | track: { 22 | album: { 23 | images: Array<{ url: string }>; 24 | }; 25 | artists: Array<{ name: string }>; 26 | name: string; 27 | preview_url: string | null; 28 | }; 29 | } 30 | 31 | export interface IStateProps { 32 | isPaused: boolean; 33 | isPlaying: boolean; 34 | playlist: IPlaylist[]; 35 | songInd: number; 36 | } 37 | 38 | export interface IDispatchProps { 39 | pause: () => void; 40 | playNextTrack: (playlist: IPlaylist[], songInd: number) => void; 41 | playPrevTrack: (playlist: IPlaylist[], songInd: number) => void; 42 | stop: () => void; 43 | unpause: () => void; 44 | } 45 | 46 | interface IState { 47 | currentTime: number; 48 | hasNextTrack: boolean; 49 | hasPrevTrack: boolean; 50 | paused: boolean; 51 | playing: boolean; 52 | totalTime: number; 53 | } 54 | 55 | class PlayerContainer extends Component { 56 | audioEl = new Audio(); 57 | state = { 58 | currentTime: 0, 59 | hasNextTrack: false, 60 | hasPrevTrack: false, 61 | paused: false, 62 | playing: false, 63 | totalTime: 0, 64 | }; 65 | 66 | componentDidMount() { 67 | this.audioEl.addEventListener('ended', this.handleEnded); 68 | this.audioEl.addEventListener('timeupdate', this.handleTimeUpdate); 69 | this.audioEl.addEventListener('loadeddata', this.handleDataLoaded); 70 | } 71 | 72 | componentWillUnmount() { 73 | this.audioEl.removeEventListener('ended', this.handleEnded); 74 | this.audioEl.removeEventListener('timeupdate', this.handleTimeUpdate); 75 | this.audioEl.removeEventListener('loadeddata', this.handleDataLoaded); 76 | } 77 | 78 | handleDataLoaded = () => { 79 | if (this.state.totalTime !== this.audioEl.duration) { 80 | this.setState(() => ({ 81 | totalTime: this.audioEl.duration, 82 | })); 83 | } 84 | }; 85 | 86 | handleTimeUpdate = () => { 87 | const { currentTime } = this.audioEl; 88 | if (currentTime !== this.state.currentTime) { 89 | this.setState(() => ({ currentTime })); 90 | } 91 | }; 92 | 93 | playTrack = () => { 94 | const { playlist, songInd } = this.props; 95 | const { currentTime } = this.state; 96 | 97 | if (songInd === -1) { 98 | return this.pauseTrack(); 99 | } 100 | 101 | const hasNextTrack = skipUnavailableTracks(playlist, songInd + 1) !== -1; 102 | const hasPrevTrack = searchPrevTrack(playlist, songInd) !== -1; 103 | 104 | this.setState(() => ({ 105 | hasNextTrack, 106 | hasPrevTrack, 107 | })); 108 | 109 | this.audioEl.src = playlist[songInd].track.preview_url || ''; 110 | this.audioEl.volume = 0.3; 111 | this.audioEl.currentTime = currentTime; 112 | const playPromise = this.audioEl.play(); 113 | // Don't console log error 114 | playPromise.catch(noop => noop); 115 | }; 116 | 117 | stopTrack = () => { 118 | this.audioEl.pause(); 119 | this.audioEl.currentTime = 0; 120 | }; 121 | 122 | pauseTrack = () => { 123 | this.audioEl.pause(); 124 | this.props.pause(); 125 | }; 126 | 127 | handleEnded = () => { 128 | if (!this.state.hasNextTrack) { 129 | return this.props.stop(); 130 | } 131 | const { playlist, songInd, playNextTrack } = this.props; 132 | playNextTrack(playlist, songInd); 133 | }; 134 | 135 | handlePlay = () => { 136 | if (!this.props.playlist) { 137 | return; 138 | } 139 | this.props.unpause(); 140 | }; 141 | 142 | handlePause = () => { 143 | const { isPaused, pause, unpause } = this.props; 144 | isPaused ? unpause() : pause(); 145 | }; 146 | 147 | handlePrev = () => { 148 | const { playlist, songInd, playPrevTrack } = this.props; 149 | if (!this.state.hasPrevTrack) { 150 | return; 151 | } 152 | playPrevTrack(playlist, songInd); 153 | }; 154 | 155 | handleVolumeChange = (value: number) => { 156 | this.audioEl.volume = value; 157 | }; 158 | 159 | UNSAFE_componentWillReceiveProps({ 160 | isPlaying, 161 | isPaused, 162 | }: { 163 | isPlaying: boolean; 164 | isPaused: boolean; 165 | }) { 166 | const { playing, paused } = this.state; 167 | if (isPlaying !== playing || isPaused !== paused) { 168 | this.setState(({ currentTime }) => ({ 169 | currentTime: !isPlaying && !isPaused ? 0 : currentTime, 170 | paused: isPaused, 171 | playing: isPlaying, 172 | })); 173 | } 174 | } 175 | 176 | componentDidUpdate() { 177 | const { paused, playing } = this.state; 178 | if (playing && !paused && this.audioEl.paused) { 179 | return this.playTrack(); 180 | } 181 | if (playing && paused) { 182 | return this.pauseTrack(); 183 | } 184 | if (!playing && !paused) { 185 | this.stopTrack(); 186 | } 187 | } 188 | 189 | render() { 190 | const { isPlaying, isPaused, playlist, songInd } = this.props; 191 | const { hasNextTrack, hasPrevTrack } = this.state; 192 | const currentTrack = 193 | playlist && songInd !== -1 ? playlist[songInd].track : null; 194 | 195 | return ( 196 | 197 | 202 | 211 | 212 | 213 | ); 214 | } 215 | } 216 | 217 | const mapStateToProps = (state: any): IStateProps => ({ 218 | isPaused: state.isPaused, 219 | isPlaying: state.isPlaying, 220 | playlist: state.tracklist, 221 | songInd: state.activeTrackId, 222 | }); 223 | 224 | const mapDispatchToProps = (dispatch: any) => ({ 225 | pause: () => { 226 | dispatch(actions.setPause()); 227 | }, 228 | playNextTrack: (playlist: IPlaylist[], songInd: number) => { 229 | dispatch(actions.playNextTrack(playlist, songInd)); 230 | }, 231 | playPrevTrack: (playlist: IPlaylist[], songInd: number) => { 232 | dispatch(actions.playPrevTrack(playlist, songInd)); 233 | }, 234 | stop: () => dispatch(actions.stopPlay()), 235 | unpause: () => { 236 | dispatch(actions.unpause()); 237 | }, 238 | }); 239 | 240 | export { PlayerContainer }; 241 | 242 | export default connect( 243 | mapStateToProps, 244 | mapDispatchToProps 245 | )(PlayerContainer); 246 | --------------------------------------------------------------------------------