├── .gitignore ├── .travis.yml ├── demo_images ├── Screen Shot 0030-02-18 at 0.43.40.png ├── Screen Shot 0030-02-18 at 0.43.44.png └── Screen Shot 0030-02-18 at 0.44.06.png ├── src ├── components │ ├── App │ │ └── index.js │ ├── Stream │ │ ├── index.js │ │ └── presenter.js │ └── Callback │ │ └── index.js ├── actions │ ├── index.js │ ├── track.js │ └── auth.js ├── constants │ ├── actionTypes.js │ └── auth.js ├── reducers │ ├── index.js │ ├── auth.js │ └── track.js ├── stores │ └── configureStore.js └── index.js ├── index.html ├── webpack.config.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm test -------------------------------------------------------------------------------- /demo_images/Screen Shot 0030-02-18 at 0.43.40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chienkira/react-redux-spotify/HEAD/demo_images/Screen Shot 0030-02-18 at 0.43.40.png -------------------------------------------------------------------------------- /demo_images/Screen Shot 0030-02-18 at 0.43.44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chienkira/react-redux-spotify/HEAD/demo_images/Screen Shot 0030-02-18 at 0.43.44.png -------------------------------------------------------------------------------- /demo_images/Screen Shot 0030-02-18 at 0.44.06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chienkira/react-redux-spotify/HEAD/demo_images/Screen Shot 0030-02-18 at 0.44.06.png -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function App({ children }) { 4 | return
{children}
5 | } 6 | 7 | export default App 8 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import {auth} from './auth' 2 | import {setTracks} from './track' 3 | import {playTrack} from './track' 4 | 5 | export { 6 | auth, 7 | setTracks, 8 | playTrack 9 | } -------------------------------------------------------------------------------- /src/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ME_SET = 'ME_SET' 2 | export const TRACKS_SET = 'TRACKS_SET' 3 | export const TRACK_PLAY = 'TRACK_PLAY' 4 | export const SPOTIFY_AUTH = 'SPOTIFY_AUTH' -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux' 2 | import {routerReducer} from 'react-router-redux' 3 | import auth from './auth' 4 | import track from './track' 5 | 6 | export default combineReducers({ 7 | auth, 8 | track, 9 | routing: routerReducer 10 | }) -------------------------------------------------------------------------------- /src/actions/track.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/actionTypes' 2 | 3 | export function setTracks(tracks) { 4 | return { 5 | type: actionTypes.TRACKS_SET, 6 | tracks 7 | } 8 | } 9 | 10 | export function playTrack(track) { 11 | return { 12 | type: actionTypes.TRACK_PLAY, 13 | track 14 | } 15 | } -------------------------------------------------------------------------------- /src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/actionTypes' 2 | 3 | const initialState = {}; 4 | 5 | export default function (state = initialState, action) { 6 | switch (action.type) { 7 | case actionTypes.ME_SET: 8 | return setMe(state, action) 9 | } 10 | return state 11 | } 12 | 13 | function setMe(state, action) { 14 | const { user } = action; 15 | return { ...state, user }; 16 | } -------------------------------------------------------------------------------- /src/constants/auth.js: -------------------------------------------------------------------------------- 1 | export const CLIENT_ID = '0573fe35f306498d912be65c3eddd64b'; 2 | export const REDIRECT_URI = `${window.location.protocol}//${window.location.host}/callback`; 3 | export const SCOPES = [ 4 | 'user-read-private', 5 | 'user-read-email', 6 | 'user-library-read', 7 | 'playlist-read-private', 8 | 'user-read-recently-played', 9 | 'user-read-currently-playing'] 10 | export const STATE = 'state-attemp-musyc-auth' -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React redux spotify 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/stores/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { createLogger } from 'redux-logger' 3 | import thunk from 'redux-thunk' 4 | import { browserHistory } from 'react-router'; 5 | import { routerMiddleware } from 'react-router-redux'; 6 | import rootReducer from '../reducers/index' 7 | 8 | const logger = createLogger() 9 | const router = routerMiddleware(browserHistory); 10 | 11 | const createStoreWithMiddleware = applyMiddleware(thunk, router, logger)(createStore); 12 | 13 | export default function configureStore(initialState) { 14 | return createStoreWithMiddleware(rootReducer, initialState) 15 | } -------------------------------------------------------------------------------- /src/reducers/track.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../constants/actionTypes' 2 | 3 | const initialState = { 4 | tracks: [], 5 | activeTrack: null 6 | } 7 | 8 | export default function (state = initialState, action) { 9 | switch (action.type) { 10 | case actionTypes.TRACKS_SET: 11 | return setTracks(state, action) 12 | case actionTypes.TRACK_PLAY: 13 | return setPlay(state, action) 14 | } 15 | return state 16 | } 17 | 18 | function setTracks(state, action) { 19 | const {tracks} = action 20 | return { ...state, tracks } 21 | } 22 | 23 | function setPlay(state, action) { 24 | const {track} = action 25 | return { ...state, activeTrack: track } 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: [ 5 | 'react-hot-loader/patch', 6 | './src/index.js' 7 | ], 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.(js|jsx)$/, 12 | exclude: /node_modules/, 13 | use: ['babel-loader'] 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: ['*', '.js', '.jsx'] 19 | }, 20 | output: { 21 | path: __dirname + '/dist', 22 | publicPath: '/', 23 | filename: 'bundle.js' 24 | }, 25 | plugins: [ 26 | new webpack.HotModuleReplacementPlugin() 27 | ], 28 | devServer: { 29 | contentBase: './', 30 | hot: true, 31 | historyApiFallback: true 32 | } 33 | }; -------------------------------------------------------------------------------- /src/components/Stream/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux'; 4 | import * as actions from '../../actions' 5 | import Stream from './presenter'; 6 | 7 | function mapStateToProps(state) { 8 | const {user} = state.auth 9 | const {tracks} = state.track 10 | const {activeTrack} = state.track 11 | return { 12 | user, 13 | tracks, 14 | activeTrack 15 | } 16 | } 17 | 18 | function mapDispatchToProps(dispatch) { 19 | return { 20 | onAuth: bindActionCreators(actions.auth, dispatch), 21 | onPlay: bindActionCreators(actions.playTrack, dispatch) 22 | } 23 | } 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(Stream); -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import {setTracks} from '../actions/track' 2 | import * as actionTypes from '../constants/actionTypes' 3 | 4 | export function auth() { 5 | 6 | function setMe(user) { 7 | return { 8 | type: actionTypes.ME_SET, 9 | user 10 | }; 11 | } 12 | 13 | return function (dispatch) { 14 | dispatch(fetchMe(window.spotifyApi)) 15 | dispatch(fetchStream(window.spotifyApi)) 16 | } 17 | 18 | function fetchMe(spotifyApi) { 19 | return function (dispatch) { 20 | spotifyApi.getMe() 21 | .then(function (data) { 22 | dispatch(setMe(data.body)) 23 | }, function (err) { 24 | console.error(err) 25 | }) 26 | } 27 | } 28 | 29 | function fetchStream(spotifyApi) { 30 | return function (dispatch) { 31 | spotifyApi.getMySavedTracks() 32 | .then(function(data) { 33 | dispatch(setTracks(data.body.items)) 34 | }, function(err) { 35 | console.error(err); 36 | }); 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-spotify 2 | 3 | *Please check newer repository at here: https://github.com/chienkira/musyc* 4 | 5 | This repository should give you an entry point for a React + Redux + Spotify boilerplate project with multiple possible extensions. 6 | 7 | If you only search for a seed project for your React + Redux + Spotify app, here is it! 8 | 9 | ## Features 10 | [Base boilerplate: https://github.com/rwieruch/minimal-react-webpack-babel-setup] 11 | 1. Login with Spotify account 12 | 2. List your track on Spotify 13 | 3. Select one and play it (use browser build-in player) 14 | 15 | ## Get Started 16 | 17 | 1. git clone 18 | 2. npm install 19 | 3. npm start 20 | 21 | ## Contribute 22 | 23 | I am open for feedback. 24 | 25 | ## Demo 26 | 27 | react redux spotify 28 | react redux spotify 29 | react redux spotify 30 | -------------------------------------------------------------------------------- /src/components/Callback/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import QueryString from 'query-string' 3 | 4 | class Callback extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this._handleAuthCallback = this.handleAuthCallback(this.props.dispatch); 9 | } 10 | 11 | componentDidMount() { 12 | window.setTimeout(this._handleAuthCallback, 1) 13 | } 14 | 15 | render() { 16 | return

From Musyc: This page should close soon...

17 | } 18 | 19 | handleAuthCallback(dispatch) { 20 | const queryString = QueryString.parse(location.hash) 21 | 22 | if (queryString.error) { 23 | // error 24 | // state 25 | console.log('Spotify access request has been denied' + queryString.error) 26 | } else { 27 | // We should get below 28 | // access_token 29 | // token_type 30 | // expires_in 31 | // state 32 | opener.spotifyApi.setAccessToken(queryString.access_token); 33 | opener.onAuth() 34 | } 35 | window.close() 36 | } 37 | 38 | } 39 | 40 | export default Callback; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import SpotifyWebApi from 'spotify-web-api-node'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | import { Provider } from 'react-redux'; 7 | import configureStore from './stores/configureStore'; 8 | import * as actions from './actions'; 9 | import App from './components/App'; 10 | import Callback from './components/Callback'; 11 | import Stream from './components/Stream/'; 12 | import { CLIENT_ID, REDIRECT_URI } from './constants/auth'; 13 | 14 | window.spotifyApi = new SpotifyWebApi({ 15 | clientId: CLIENT_ID, 16 | redirectUri: REDIRECT_URI 17 | }); 18 | 19 | const store = configureStore() 20 | 21 | const history = syncHistoryWithStore(browserHistory, store) 22 | 23 | ReactDOM.render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | , 33 | document.getElementById('app') 34 | ); 35 | 36 | module.hot.accept(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-react-webpack-babel-setup", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --colors --config ./webpack.config.js --port 8081", 8 | "build": "webpack --progress --config ./webpack.config.js -p", 9 | "test": "echo \"No test specified\" && exit 0" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "babel": { 15 | "presets": [ 16 | "env", 17 | "react", 18 | "stage-2" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.23.1", 23 | "babel-loader": "^7.1.2", 24 | "babel-preset-react": "^6.23.0", 25 | "babel-preset-stage-2": "^6.22.0", 26 | "react-hot-loader": "^3.1.3", 27 | "webpack": "^3.10.0", 28 | "webpack-dev-server": "^2.4.1" 29 | }, 30 | "dependencies": { 31 | "babel-preset-env": "^1.6.1", 32 | "history": "^4.7.2", 33 | "query-string": "^5.1.0", 34 | "react": "^16.2.0", 35 | "react-dom": "^16.2.0", 36 | "react-redux": "^5.0.6", 37 | "react-router": "^3.0.2", 38 | "react-router-redux": "^4.0.8", 39 | "redux": "^3.7.2", 40 | "redux-logger": "^3.0.6", 41 | "redux-thunk": "^2.2.0", 42 | "spotify-web-api-node": "^2.5.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Stream/presenter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {SCOPES, STATE} from '../../constants/auth' 4 | 5 | class Stream extends React.Component { 6 | 7 | componentDidUpdate() { 8 | const audioElement = ReactDOM.findDOMNode(this.refs.audio); 9 | if (!audioElement) { return; } 10 | 11 | const { activeTrack } = this.props; 12 | 13 | if (activeTrack) { 14 | audioElement.play(); 15 | } else { 16 | audioElement.pause(); 17 | } 18 | } 19 | 20 | openAuthWindow(onAuth) { 21 | let authorizeUrl = window.spotifyApi.createAuthorizeURL(SCOPES, STATE, true) 22 | authorizeUrl = authorizeUrl.replace(/response_type=code/gi, 'response_type=token') 23 | 24 | // register auth dispatch function to window so authorize child window can call it after 25 | window.onAuth = onAuth 26 | this.popupCenter(authorizeUrl, 'Musyc', 400, 600) 27 | } 28 | 29 | 30 | popupCenter(url, title, w, h) { 31 | // Fixes dual-screen position Most browsers Firefox 32 | const dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left; 33 | const dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top; 34 | 35 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; 36 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; 37 | 38 | const left = ((width / 2) - (w / 2)) + dualScreenLeft; 39 | const top = ((height / 2) - (h / 2)) + dualScreenTop; 40 | const newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); 41 | 42 | // Puts focus on the newWindow 43 | if (window.focus) { 44 | newWindow.focus(); 45 | } 46 | 47 | return newWindow; 48 | } 49 | 50 | render() { 51 | const {user, tracks = [], activeTrack, onAuth, onPlay} = this.props; 52 | return ( 53 |
54 |
55 | { 56 | user ? 57 |
{user.display_name}
: 58 | 61 | } 62 |
63 |
64 |
65 | { 66 | tracks.map((track, key) => { 67 | return ( 68 |
69 | {key + 1} {track.track.name} 70 |   71 |
72 | ); 73 | }) 74 | } 75 |
76 | { 77 | activeTrack ? 78 |
79 |
: 81 | null 82 | } 83 |
84 | ) 85 | } 86 | } 87 | 88 | export default Stream; --------------------------------------------------------------------------------