├── spotify-clone
├── src
│ ├── index.css
│ ├── components
│ │ ├── Container
│ │ │ ├── Container.css
│ │ │ └── Container.js
│ │ ├── Title&Subtitle
│ │ │ ├── TitleSubtitle.css
│ │ │ └── TitleSubtitle.js
│ │ ├── Footer
│ │ │ ├── Footer.css
│ │ │ └── Footer.js
│ │ ├── DisplayList
│ │ │ ├── DisplayList.css
│ │ │ └── DisplayList.js
│ │ ├── SongList
│ │ │ ├── SongList.css
│ │ │ └── SongList.js
│ │ ├── Header
│ │ │ ├── Header.css
│ │ │ └── Header.js
│ │ └── NavBar
│ │ │ ├── NavBar.css
│ │ │ └── NavBar.js
│ ├── App.css
│ ├── reducers
│ │ ├── index.js
│ │ ├── albums.js
│ │ ├── user.js
│ │ └── song.js
│ ├── App.test.js
│ ├── config
│ │ ├── utils.js
│ │ ├── store.js
│ │ └── AudioController.js
│ ├── index.js
│ ├── Pages
│ │ ├── AboutPage.js
│ │ ├── AlbumsPage.js
│ │ ├── SongsPage.js
│ │ ├── HomePage.js
│ │ └── Login.js
│ ├── actions
│ │ ├── albums.js
│ │ ├── user.js
│ │ └── song.js
│ └── App.js
├── README.md
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── .gitignore
└── package.json
└── readme.md
/spotify-clone/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/spotify-clone/README.md:
--------------------------------------------------------------------------------
1 | Bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
2 |
--------------------------------------------------------------------------------
/spotify-clone/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardkabre/ReactSpotifyClone/HEAD/spotify-clone/public/favicon.ico
--------------------------------------------------------------------------------
/spotify-clone/src/components/Container/Container.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 5% 8%;
3 | width: 100%;
4 | height: 100%;
5 | overflow-y: scroll
6 | }
--------------------------------------------------------------------------------
/spotify-clone/src/App.css:
--------------------------------------------------------------------------------
1 | .content {
2 | height: 100%;
3 | padding: 0;
4 | background: rgb(0,0,0);
5 | background: linear-gradient(0deg, rgba(0,0,0,1) 15%, rgba(57,57,57,1) 100%);
6 | }
7 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/Title&Subtitle/TitleSubtitle.css:
--------------------------------------------------------------------------------
1 |
2 | .title {
3 | color: white;
4 | font-weight: 600;
5 | font-size: 3.5em;
6 | }
7 | .subtitle {
8 | color: rgba(255, 255, 255, 0.788);
9 | }
--------------------------------------------------------------------------------
/spotify-clone/src/components/Container/Container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Container.css';
3 |
4 | const Container = ({ children }) =>
{children}
;
5 | export default Container;
6 |
--------------------------------------------------------------------------------
/spotify-clone/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 |
4 | import user from './user';
5 | import song from './song';
6 | import albums from './albums';
7 |
8 | export default combineReducers({
9 | user: user,
10 | song: song,
11 | albums: albums
12 | });
13 |
--------------------------------------------------------------------------------
/spotify-clone/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 |
--------------------------------------------------------------------------------
/spotify-clone/src/config/utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 |
4 | const PrivateRoute = ({ component: Component, user, ...rest }) => (
5 | (user ? : )} />
6 | );
7 |
8 | export default PrivateRoute;
9 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/Title&Subtitle/TitleSubtitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './TitleSubtitle.css';
3 |
4 | const TitleSubtitle = ({ title, subtitle }) => (
5 |
6 |
{title}
7 |
{subtitle}
8 |
9 | );
10 |
11 | export default TitleSubtitle;
12 |
--------------------------------------------------------------------------------
/spotify-clone/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | import { Provider } from 'react-redux';
6 | import store from './config/store';
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
--------------------------------------------------------------------------------
/spotify-clone/.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.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Spotify clone
2 |
3 | * Spotify client built with React + Redux + React Router.
4 | * Online music playing functionality
5 | * Access with your Spotify user and get your most recently played songs, albums and songs saved.
6 | * Use it live at: https://gerardkabre.github.io/ReactSpotifyClone/
7 |
8 | 
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spotify-clone/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": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/spotify-clone/src/config/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '../reducers/index';
4 |
5 | import logger from 'redux-logger';
6 |
7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
8 | const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk, logger)));
9 |
10 | export default store;
11 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/Footer/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | background-color: #282828;
3 | height: 100%;
4 | width: 100%;
5 | padding: 1%;
6 | color: rgb(228, 228, 228);
7 | display: flex;
8 | justify-content: space-between;
9 | }
10 | .footer__SongDetails {
11 | flex: 1;
12 | }
13 | .footer__Button {
14 | flex: 1;
15 | text-align: center;
16 | }
17 | .footer__Button:hover {
18 | cursor: pointer;
19 | }
20 | .footer__name {
21 | flex: 1;
22 | text-align: right;
23 | margin-top: 1.5%;
24 | font-weight: lighter;
25 | color: rgba(34, 219, 99, 0.76);
26 | }
27 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/DisplayList/DisplayList.css:
--------------------------------------------------------------------------------
1 | .displayList {
2 | padding: 0;
3 | display: flex;
4 | max-width: 100%;
5 | flex-wrap: wrap;
6 | }
7 | .displayListItem {
8 | color: white;
9 | list-style: none;
10 | margin: 10px;
11 | max-width: 200px;
12 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
13 | font-weight: lighter;
14 | text-align: center;
15 | }
16 | .displayListItem:hover {
17 | background-color: rgba(255, 255, 255, 0.085);
18 | cursor: pointer;
19 | }
20 |
21 | .displayListItem img {
22 | max-width: 100%;
23 | margin-bottom: 5px;
24 | }
--------------------------------------------------------------------------------
/spotify-clone/src/components/SongList/SongList.css:
--------------------------------------------------------------------------------
1 | .songList {
2 | width: 100%;
3 | margin: 50px 0px 50px 0px;
4 | }
5 |
6 | .songList__item {
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: space-between;
10 | color: white;
11 | border-top: rgba(255, 255, 255, 0.125) 1px solid;
12 | font-size: 1em;
13 | padding-left: 20px;
14 | padding-top: 10px;
15 | }
16 |
17 | .songList__item:hover {
18 | background-color: rgba(255, 255, 255, 0.085);
19 | cursor: pointer;
20 | }
21 |
22 | .songList__item p {
23 | flex: 3;
24 | }
25 |
26 | .songList__item__title {
27 | border-top: white 0px solid;
28 | font-weight: lighter;
29 | }
--------------------------------------------------------------------------------
/spotify-clone/src/components/DisplayList/DisplayList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AudioController from '../../config/AudioController';
3 |
4 | import './DisplayList.css';
5 |
6 | const DisplayList = ({ data, album, play }) => {
7 | const renderAlbumList = () => (
8 |
9 | {data.map(item => (
10 | play(item.album.tracks.items[0])} >
11 |
12 | {item.album.name}
13 |
14 | ))}
15 |
16 | );
17 |
18 | if (!data) return 'loading...';
19 | return renderAlbumList();
20 | };
21 |
22 | export default AudioController(DisplayList);
23 |
--------------------------------------------------------------------------------
/spotify-clone/src/reducers/albums.js:
--------------------------------------------------------------------------------
1 | import { ALBUM_FETCH_REQUEST } from '../actions/albums';
2 | import { ALBUM_FETCH_SUCCESS } from '../actions/albums';
3 | import { ALBUM_FETCH_ERROR } from '../actions/albums';
4 |
5 | const initialState = {
6 | list: [],
7 | isFetching: false,
8 | hasFetched: false,
9 | error: null
10 | }
11 |
12 | export default (state = initialState, action) => {
13 | switch (action.type) {
14 | case ALBUM_FETCH_REQUEST:
15 | return { ...state, isFetching: true };
16 | case ALBUM_FETCH_SUCCESS:
17 | return { ...state, isFetching: false, hasFetched: true, list: action.albums };
18 | case ALBUM_FETCH_ERROR:
19 | return { ...state, isFetching: false, error: action.albums.list };
20 | default:
21 | return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/Header/Header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | justify-content: space-between;
6 | padding: 1% 10%;
7 | color: white;
8 | background-color: #282828;
9 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
10 | }
11 |
12 | .header h1 {
13 | font-size: 1em;
14 | font-weight: lighter;
15 | }
16 |
17 | .header__Profile {
18 | align-items: center;
19 | flex: 2;
20 | display: flex;
21 | justify-content: space-around;
22 | }
23 |
24 | .header__Profile h2 {
25 | font-size: 1em;
26 | font-weight: lighter;
27 | }
28 |
29 | .header__Profile > img {
30 | width: 40px;
31 | height: 40px;
32 | border-radius: 100px;
33 | }
34 |
35 | .header__BackButton {
36 | flex: 8;
37 | }
38 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/NavBar/NavBar.css:
--------------------------------------------------------------------------------
1 | .navBar {
2 | background-color: #181818;
3 | list-style: none;
4 | height: 100%;
5 | padding: 20px 0px 0px 20px;
6 | text-align: left;
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: space-between;
10 | }
11 |
12 | .navBar > div > li > a {
13 | display: inline-block;
14 | margin-bottom: 6px;
15 | padding-left: 20px;
16 | color: rgb(214, 212, 212);
17 | font-weight: 500;
18 | font-size: 1em;
19 | }
20 |
21 | .navBar > div > li > a:hover {
22 | color: rgb(245, 241, 241);
23 | text-decoration: none;
24 | }
25 |
26 | .NavBar__title {
27 | margin-top: 10%;
28 | display: inline-block;
29 | padding-left: 20px;
30 | color: rgba(187, 185, 185, 0.596);
31 | }
32 |
33 | .navBar__image {
34 | max-width: 100%;
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/spotify-clone/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "http://gerardkabre.github.io/ReactSpotifyClone",
3 | "name": "spotify-clone",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "prop-types": "^15.6.1",
8 | "react": "^16.3.2",
9 | "react-bootstrap": "^0.32.1",
10 | "react-dom": "^16.3.2",
11 | "react-icons": "^2.2.7",
12 | "react-redux": "^5.0.7",
13 | "react-router": "^4.2.0",
14 | "react-router-dom": "^4.2.2",
15 | "react-scripts": "1.1.4",
16 | "redux": "^4.0.0",
17 | "redux-logger": "^3.0.6",
18 | "redux-thunk": "^2.2.0"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test --env=jsdom",
24 | "eject": "react-scripts eject",
25 | "predeploy": "npm run build",
26 | "deploy": "gh-pages -d build"
27 | },
28 | "devDependencies": {
29 | "gh-pages": "^1.1.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/SongList/SongList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './SongList.css';
3 |
4 | const SongList = ({ data, onItemClick }) => {
5 | if (data === undefined) return LOADING
;
6 | return (
7 |
21 | );
22 | };
23 |
24 | export default SongList;
25 |
--------------------------------------------------------------------------------
/spotify-clone/src/Pages/AboutPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Container from '../components/Container/Container';
4 | import TitleSubtitle from '../components/Title&Subtitle/TitleSubtitle';
5 |
6 | const AboutPage = () => (
7 |
8 |
9 |
10 |
This is an open source app showcasing a spotify 'clone' with basic functionality,
11 | uses your spotify user and retrives your recently palyed songs, albums, all the songs
12 | you have saved, and allows you to play a sample of them form your browser.
13 |
14 |
Built with React, Redux, Thunk, React-Router and React-bootstrap
15 |
You can see the code here: Github
16 |
17 |
18 | );
19 |
20 | export default AboutPage;
21 |
--------------------------------------------------------------------------------
/spotify-clone/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import { FETCH_TOKEN_REQUESTED, FETCH_USER_REQUESTED } from '../actions/user';
2 | import { FETCH_TOKEN_SUCCESS } from '../actions/user';
3 | import { FETCH_USER_SUCCESS } from '../actions/user';
4 | import { FETCH_USER_ERROR } from '../actions/user';
5 |
6 | const initialState = {
7 | isLoggedIn: false,
8 | tokenRequested: false,
9 | tokenSuccess: false,
10 | user: null,
11 | token: null,
12 | error: null
13 | };
14 |
15 | export default (state = initialState, action) => {
16 | switch (action.type) {
17 | case FETCH_TOKEN_REQUESTED:
18 | return { ...state, tokenRequested: true };
19 | case FETCH_TOKEN_SUCCESS:
20 | return { ...state, tokenRequested: false, isLoggedIn: true, token: action.token };
21 | case FETCH_USER_REQUESTED:
22 | return { ...state, tokenSuccess: false };
23 | case FETCH_USER_SUCCESS:
24 | return { ...state, user: action.user };
25 | case FETCH_USER_ERROR:
26 | return { ...state, error: action.err };
27 | default:
28 | return state;
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/spotify-clone/src/actions/albums.js:
--------------------------------------------------------------------------------
1 | export const ALBUM_FETCH_REQUEST = 'ALBUM_FETCH_REQUEST';
2 | export const ALBUM_FETCH_SUCCESS = 'ALBUM_FETCH_SUCCESS';
3 | export const ALBUM_FETCH_ERROR = 'ALBUM_FETCH_ERROR';
4 |
5 | export function fetchAlbums() {
6 | return {
7 | type: ALBUM_FETCH_REQUEST
8 | };
9 | }
10 |
11 | export function fetchAlbumsSuccess(albums) {
12 | return {
13 | type: ALBUM_FETCH_SUCCESS,
14 | albums
15 | };
16 | }
17 |
18 | export function fetchAlbumsError(err) {
19 | return {
20 | type: ALBUM_FETCH_ERROR,
21 | err
22 | };
23 | }
24 |
25 | export function getAlbums(token) {
26 | return dispatch => {
27 | dispatch(fetchAlbums());
28 | return fetch('https://api.spotify.com/v1/me/albums', {
29 | method: 'GET',
30 | headers: {
31 | 'Content-Type': 'application/x-www-form-urlencoded',
32 | Authorization: `Bearer ${token}`
33 | }
34 | })
35 | .then(response => response.json())
36 | .then(response => dispatch(fetchAlbumsSuccess(response)))
37 | .catch(err => dispatch(fetchAlbumsError(err)));
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/spotify-clone/src/Pages/AlbumsPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { connect } from 'react-redux';
4 |
5 | import Container from '../components/Container/Container';
6 | import TitleSubtitle from '../components/Title&Subtitle/TitleSubtitle';
7 | import DisplayList from '../components/DisplayList/DisplayList';
8 |
9 | import { getAlbums } from '../actions/albums';
10 |
11 | class AlbumsPage extends Component {
12 | componentDidMount() {
13 | if (!this.props.hasFetchedAlbums) this.props.dispatch(getAlbums(this.props.token));
14 | }
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | const mapStateToProps = state => {
27 | return {
28 | isLoggedIn: state.user.isLoggedIn,
29 | token: state.user.token,
30 | albums: state.albums.list.items,
31 | hasFetchedAlbums: state.albums.HasFetched
32 | };
33 | };
34 |
35 | export default connect(mapStateToProps)(AlbumsPage);
36 |
--------------------------------------------------------------------------------
/spotify-clone/src/Pages/SongsPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { connect } from 'react-redux';
4 |
5 | import AudioController from '../config/AudioController';
6 |
7 | import Container from '../components/Container/Container';
8 | import TitleSubtitle from '../components/Title&Subtitle/TitleSubtitle';
9 | import SongList from '../components/SongList/SongList';
10 |
11 | import { fetchSongs } from '../actions/song';
12 |
13 | class SongsPage extends Component {
14 | componentDidMount() {
15 | this.props.dispatch(fetchSongs(this.props.token));
16 | }
17 |
18 | render() {
19 | return (
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | const mapStateToProps = state => {
29 | return {
30 | token: state.user.token,
31 | songs: state.song.songs
32 | };
33 | };
34 |
35 | const SongsPageConnected = connect(mapStateToProps)(SongsPage);
36 | export default AudioController(SongsPageConnected);
37 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Header.css';
3 |
4 | const Header = ({ user }) => {
5 | if (user)
6 | return (
7 |
8 |
15 | {user.images[0] ? (
16 |
17 |
18 |
{user.display_name}
19 |
20 | ) : (
21 |
22 |
{user.id}
23 |
24 | )}
25 |
26 | );
27 | else {
28 | return (
29 |
30 |
31 |
32 |
33 |
Log in
34 |
35 |
36 | );
37 | }
38 | };
39 |
40 | export default Header;
41 |
--------------------------------------------------------------------------------
/spotify-clone/src/config/AudioController.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Higher Order Component that gives audio control options to the components that need them.
3 | * Returns a component connected to redux (to be able to dispatch actions), with the audio
4 | * methods that dispatch those actions.
5 | */
6 |
7 | import React from 'react';
8 | import { bindActionCreators } from 'redux';
9 | import { connect } from 'react-redux';
10 |
11 | import { playSong } from '../actions/song';
12 | import { pauseSong } from '../actions/song';
13 | import { resumeSong } from '../actions/song';
14 |
15 | export default function AudioController(WrappedComponent) {
16 | return connect(null, mapDispatchToProps)(
17 | class extends React.Component {
18 | play = song => {
19 | this.props.playSong(song);
20 | };
21 | pause = () => {
22 | this.props.pauseSong();
23 | };
24 | resume = () => {
25 | this.props.resumeSong();
26 | };
27 |
28 | render() {
29 | return ;
30 | }
31 | }
32 | );
33 | }
34 |
35 | const mapDispatchToProps = dispatch => {
36 | return bindActionCreators(
37 | {
38 | playSong,
39 | pauseSong,
40 | resumeSong
41 | },
42 | dispatch
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/NavBar/NavBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Nav, NavItem } from 'react-bootstrap';
3 |
4 | import { Link } from 'react-router-dom';
5 |
6 | import './NavBar.css';
7 |
8 | const albumImage = songDetails => {
9 | if (songDetails) {
10 | if (songDetails.album) {
11 | return ;
12 | }
13 | }
14 | };
15 |
16 | const NavBar = ({ location, songDetails }) => (
17 |
18 |
19 |
Your library
20 |
21 | Recently played
22 |
23 |
24 | Albums
25 |
26 |
27 | Songs
28 |
29 |
About
30 |
31 | About this app
32 |
33 |
34 | {albumImage(songDetails)}
35 |
36 | );
37 |
38 | export default NavBar;
39 |
--------------------------------------------------------------------------------
/spotify-clone/src/Pages/HomePage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { connect } from 'react-redux';
4 |
5 | import AudioController from '../config/AudioController';
6 |
7 | import Container from '../components/Container/Container';
8 | import TitleSubtitle from '../components/Title&Subtitle/TitleSubtitle';
9 | import SongList from '../components/SongList/SongList';
10 |
11 | import { fetchTokenRequested } from '../actions/user';
12 | import { fetchTokenSuccess } from '../actions/user';
13 | import { getUser } from '../actions/user';
14 | import { fetchRecentlyPlayed } from '../actions/song';
15 |
16 | class HomePage extends Component {
17 | componentDidMount() {
18 | this.props.dispatch(fetchRecentlyPlayed(this.props.token));
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => {
32 | return {
33 | isLoggedIn: state.user.isLoggedIn,
34 | token: state.user.token,
35 | songs: state.song.songs,
36 | hasFetchedSongs: state.song.hasFetchedRecentSongs
37 | };
38 | };
39 |
40 | const HomePageConnected = connect(mapStateToProps)(HomePage);
41 | export default AudioController(HomePageConnected);
42 |
--------------------------------------------------------------------------------
/spotify-clone/src/actions/user.js:
--------------------------------------------------------------------------------
1 | export const FETCH_TOKEN_REQUESTED = 'FETCH_TOKEN_REQUESTED';
2 | export const FETCH_TOKEN_SUCCESS = 'FETCH_TOKEN_SUCCESS';
3 | export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';
4 | export const FETCH_USER_ERROR = 'FETCH_USER_ERROR';
5 | export const FETCH_USER_REQUESTED = 'FETCH_USER_REQUESTED';
6 |
7 | export function fetchTokenRequested() {
8 | return {
9 | type: FETCH_TOKEN_REQUESTED
10 | };
11 | }
12 |
13 | export function fetchTokenSuccess(token) {
14 | return {
15 | type: FETCH_TOKEN_SUCCESS,
16 | token
17 | };
18 | }
19 |
20 | export function fetchUser() {
21 | return {
22 | type: FETCH_USER_REQUESTED
23 | };
24 | }
25 |
26 | export function fetchUserSuccess(user) {
27 | return {
28 | type: FETCH_USER_SUCCESS,
29 | user
30 | };
31 | }
32 |
33 | export function fetchUserError(err) {
34 | return {
35 | type: FETCH_USER_ERROR,
36 | err
37 | };
38 | }
39 |
40 | export function getUser(accessToken) {
41 | return dispatch => {
42 | dispatch(fetchUser());
43 | return fetch('https://api.spotify.com/v1/me', {
44 | method: 'GET',
45 | headers: {
46 | 'Content-Type': 'application/x-www-form-urlencoded',
47 | Authorization: `Bearer ${accessToken}`
48 | }
49 | })
50 | .then(res => res.json())
51 | .then(res => dispatch(fetchUserSuccess(res)))
52 | .catch(err => dispatch(fetchUserError(err)));
53 | };
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/spotify-clone/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AudioController from '../../config/AudioController';
3 |
4 | import FaPlay from 'react-icons/lib/fa/play';
5 | import FaPause from 'react-icons/lib/fa/pause';
6 |
7 | import './Footer.css';
8 |
9 | const noSongSelected = resume => (
10 |
11 |
12 |
Select a song
13 |
no artist selected
14 |
15 |
alert('Select a song first')}>
16 |
17 |
18 |
19 |
Made by gerard cabrerizo
20 |
21 | );
22 |
23 | const songPaused = (resume, songDetails) => (
24 |
25 |
26 |
{songDetails.name}
27 |
{songDetails.artists[0].name}
28 |
29 |
resume()}>
30 |
31 |
32 |
33 |
Made by gerard cabrerizo
34 |
35 | );
36 |
37 | const Footer = ({ isPlaying, songDetails, pause, resume }) => {
38 | if (!isPlaying && !songDetails) return noSongSelected(resume);
39 | if (!isPlaying) return songPaused(resume, songDetails);
40 | return (
41 |
42 |
43 |
{songDetails.name}
44 |
{songDetails.artists[0].name}
45 |
46 |
47 |
pause()}>
48 |
49 |
50 |
51 |
Made by gerard cabrerizo
52 |
53 | );
54 | };
55 |
56 | export default AudioController(Footer);
57 |
--------------------------------------------------------------------------------
/spotify-clone/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/spotify-clone/src/Pages/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { connect } from 'react-redux';
4 |
5 | import { Redirect, withRouter } from 'react-router-dom';
6 |
7 | import Container from '../components/Container/Container';
8 | import TitleSubtitle from '../components/Title&Subtitle/TitleSubtitle';
9 |
10 | import { fetchTokenSuccess } from '../actions/user';
11 | import { fetchTokenRequested } from '../actions/user';
12 | import { getUser } from '../actions/user';
13 |
14 | const authUrl =
15 | 'https://accounts.spotify.com/authorize?client_id=7601320fe8e34a45b95e142c37e48b52&scope=playlist-read-private%20playlist-read-collaborative%20playlist-modify-public%20user-read-recently-played%20playlist-modify-private%20ugc-image-upload%20user-follow-modify%20user-follow-read%20user-library-read%20user-library-modify%20user-read-private%20user-read-email%20user-top-read%20user-read-playback-state&response_type=token&redirect_uri=https://gerardkabre.github.io/ReactSpotifyClone/';
16 |
17 | class Login extends Component {
18 | extractHashFromUrl() {
19 | let hashParams = {};
20 | let e,
21 | r = /([^&;=]+)=?([^&;]*)/g,
22 | q = window.location.hash.substring(1);
23 | while ((e = r.exec(q))) hashParams[e[1]] = decodeURIComponent(e[2]);
24 |
25 | if (hashParams.access_token) {
26 | this.props.dispatch(fetchTokenSuccess(hashParams.access_token));
27 | return true;
28 | } else return false;
29 | }
30 |
31 | componentDidMount() {
32 | if (!this.props.isLoggedIn) {
33 | if (!this.extractHashFromUrl()) {
34 | window.location.href = authUrl;
35 | }
36 | }
37 | }
38 |
39 | componentWillReceiveProps(nextProps) {
40 | if (!this.props.isLoggedIn) {
41 | if (nextProps.token) {
42 | this.props.dispatch(getUser(nextProps.token));
43 | }
44 | }
45 | }
46 | render() {
47 | if (this.props.isLoggedIn) return ;
48 | return (
49 |
50 |
51 |
52 | );
53 | }
54 | }
55 |
56 | const mapStateToProps = state => {
57 | return {
58 | isLoggedIn: state.user.isLoggedIn,
59 | token: state.user.token,
60 | tokenRequested: state.user.tokenRequested,
61 | tokenSuccess: state.user.tokenSuccess
62 | };
63 | };
64 |
65 | const LoginConnected = connect(mapStateToProps)(Login);
66 | export default withRouter(LoginConnected);
67 |
--------------------------------------------------------------------------------
/spotify-clone/src/reducers/song.js:
--------------------------------------------------------------------------------
1 | import { FETCH_RECENTLY_PLAYED_REQUEST, RESUME_SONG } from '../actions/song';
2 | import { FETCH_RECENTLY_PLAYED_SUCCESS } from '../actions/song';
3 | import { FETCH_RECENTLY_PLAYED_ERROR } from '../actions/song';
4 | import { FETCH_ALL_SONGS_REQUEST } from '../actions/song';
5 | import { FETCH_ALL_SONGS_SUCCESS } from '../actions/song';
6 | import { FETCH_ALL_SONGS_ERROR } from '../actions/song';
7 | import { PLAY_SONG } from '../actions/song';
8 | import { PAUSE_SONG } from '../actions/song';
9 | import { STOP_SONG } from '../actions/song';
10 |
11 | const initialState = {
12 | isFetching: false,
13 | hasFetchedRecentSongs: false,
14 | hasFetchedAllSongs: false,
15 | songs: [],
16 | allSongs: [],
17 | error: null,
18 | isPlaying: false,
19 | songDetails: null,
20 | songId: null,
21 | playButton: false,
22 | resumeButton: false,
23 | pauseButton: false
24 | };
25 |
26 | export default (state = initialState, action) => {
27 | switch (action.type) {
28 | case FETCH_RECENTLY_PLAYED_REQUEST:
29 | return {
30 | ...state,
31 | isFetching: true
32 | };
33 | case FETCH_RECENTLY_PLAYED_SUCCESS:
34 | return {
35 | ...state,
36 | isFetching: false,
37 | hasFetchedRecentSongs: true,
38 | songs: action.songs
39 | };
40 | case FETCH_RECENTLY_PLAYED_ERROR:
41 | return {
42 | ...state,
43 | isFetching: false,
44 | hasFetched: false,
45 | error: action.err
46 | };
47 | case FETCH_ALL_SONGS_REQUEST:
48 | return {
49 | ...state,
50 | isFetching: true
51 | };
52 | case FETCH_ALL_SONGS_SUCCESS:
53 | return {
54 | ...state,
55 | isFetching: false,
56 | hasFetchedAllSongs: true,
57 | hasFetchedRecentSongs: false,
58 | songs: action.songs
59 | };
60 | case FETCH_ALL_SONGS_ERROR:
61 | case PLAY_SONG:
62 | return {
63 | ...state,
64 | isPlaying: true,
65 | songDetails: action.song,
66 | songId: action.song.id,
67 | playButton: true,
68 | resumeButton: false,
69 | pauseButton: false
70 | };
71 | case PAUSE_SONG:
72 | return {
73 | ...state,
74 | isPlaying: false,
75 | pauseButton: true,
76 | playButton: false,
77 | resumeButton: false
78 | };
79 | case RESUME_SONG:
80 | return {
81 | ...state,
82 | resumeButton: true,
83 | isPlaying: true,
84 | pauseButton: false,
85 | playButton: false
86 | };
87 | default:
88 | return state;
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/spotify-clone/src/actions/song.js:
--------------------------------------------------------------------------------
1 | export const FETCH_RECENTLY_PLAYED_REQUEST = 'FETCH_RECENTLY_PLAYED_REQUEST';
2 | export const FETCH_RECENTLY_PLAYED_SUCCESS = 'FETCH_RECENTLY_PLAYED_SUCCESS';
3 | export const FETCH_RECENTLY_PLAYED_ERROR = 'FETCH_RECENTLY_PLAYED_ERROR';
4 | export const FETCH_ALL_SONGS_REQUEST = 'FETCH_ALL_SONGS_REQUEST';
5 | export const FETCH_ALL_SONGS_SUCCESS = 'FETCH_ALL_SONGS_SUCCESS';
6 | export const FETCH_ALL_SONGS_ERROR = 'FETCH_ALL_SONGS_ERROR';
7 | export const PLAY_SONG = 'PLAY_SONG';
8 | export const PAUSE_SONG = 'PAUSE_SONG';
9 | export const RESUME_SONG = 'RESUME_SONG';
10 |
11 |
12 | export function fetchRecentlyPlayedRequest() {
13 | return {
14 | type: FETCH_RECENTLY_PLAYED_REQUEST
15 | };
16 | }
17 |
18 | export function fetchRecentlyPlayedSuccess(songs) {
19 | return {
20 | type: FETCH_RECENTLY_PLAYED_SUCCESS,
21 | songs
22 | };
23 | }
24 |
25 | export function fetchRecentlyPlayedError(err) {
26 | return {
27 | type: FETCH_RECENTLY_PLAYED_ERROR,
28 | err
29 | };
30 | }
31 |
32 | export const fetchRecentlyPlayed = accessToken => {
33 | return dispatch => {
34 | dispatch(fetchRecentlyPlayedRequest());
35 | return fetch('https://api.spotify.com/v1/me/player/recently-played', {
36 | method: 'GET',
37 | headers: {
38 | 'Content-Type': 'application/x-www-form-urlencoded',
39 | Authorization: `Bearer ${accessToken}`
40 | }
41 | })
42 | .then(res => res.json())
43 | .then(res => dispatch(fetchRecentlyPlayedSuccess(res)))
44 | .catch(err => dispatch(fetchRecentlyPlayedError(err)));
45 | };
46 | };
47 |
48 |
49 | export function fetchAllSongsRequest() {
50 | return {
51 | type: FETCH_ALL_SONGS_REQUEST
52 | };
53 | }
54 |
55 | export function fetchAllSongsSuccess(songs) {
56 | return {
57 | type: FETCH_ALL_SONGS_SUCCESS,
58 | songs
59 | };
60 | }
61 |
62 | export function fetchAllSongsError(err) {
63 | return {
64 | type: FETCH_ALL_SONGS_ERROR,
65 | err
66 | };
67 | }
68 |
69 | export const fetchSongs = accessToken => {
70 | return dispatch => {
71 | dispatch(fetchAllSongsRequest());
72 | return fetch('https://api.spotify.com/v1/me/tracks?limit=50', {
73 | method: 'GET',
74 | headers: {
75 | 'Content-Type': 'application/x-www-form-urlencoded',
76 | Authorization: `Bearer ${accessToken}`
77 | }
78 | })
79 | .then(res => res.json())
80 | .then(res => dispatch(fetchAllSongsSuccess(res)))
81 | .catch(err => dispatch(fetchAllSongsError(err)));
82 | };
83 | };
84 |
85 |
86 | export const playSong = song => {
87 | return {
88 | type: 'PLAY_SONG',
89 | song
90 | };
91 | };
92 |
93 | export const pauseSong = () => {
94 | return {
95 | type: 'PAUSE_SONG'
96 | };
97 | };
98 |
99 | export const resumeSong = () => {
100 | return {
101 | type: 'RESUME_SONG'
102 | };
103 | };
104 |
--------------------------------------------------------------------------------
/spotify-clone/src/App.js:
--------------------------------------------------------------------------------
1 | /*
2 | * audioControl function is all the logic related to playing songs in the browser.
3 | * Looks in the state if an audio control button is pulsed and updates the audio object
4 | * of the app accordingly. Components get the methods related to interacting with the audio
5 | * controller thanks at the HOC AudioController, who gives them action dispatchers
6 | * to avoid having to pass down the same methods component after component.
7 | */
8 |
9 | import React, { Component } from 'react';
10 | import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom';
11 | import { connect } from 'react-redux';
12 |
13 | import { Grid, Row, Col } from 'react-bootstrap';
14 | import NavBar from './components/NavBar/NavBar.js';
15 | import Header from './components/Header/Header.js';
16 | import Footer from './components/Footer/Footer.js';
17 |
18 | import AboutPage from './Pages/AboutPage';
19 | import Login from './Pages/Login';
20 |
21 | import SongsPage from './Pages/SongsPage.js';
22 | import HomePage from './Pages/HomePage.js';
23 | import AlbumsPage from './Pages/AlbumsPage.js';
24 |
25 | import PrivateRoute from './config/utils.js';
26 |
27 | import './App.css';
28 |
29 | class App extends Component {
30 | audioControl = () => {
31 | if (this.props.playButton) {
32 | if (this.audio) this.audio.pause();
33 | this.audio = new Audio(this.props.songDetails.preview_url);
34 | this.audio.play();
35 | }
36 | if (this.props.pauseButton) {
37 | this.audio.pause();
38 | }
39 | if (this.props.resumeButton) {
40 | this.audio.play();
41 | }
42 | };
43 |
44 | render() {
45 | this.audioControl();
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | const mapStateToProps = state => {
75 | return {
76 | user: state.user.user,
77 | isPlaying: state.song.isPlaying,
78 | songDetails: state.song.songDetails,
79 | playButton: state.song.playButton,
80 | pauseButton: state.song.pauseButton,
81 | resumeButton: state.song.resumeButton
82 | };
83 | };
84 |
85 | export default connect(mapStateToProps)(App);
86 |
--------------------------------------------------------------------------------