├── 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 |
23 |
24 |
25 |
26 | {title}
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------