├── .gitignore ├── public └── index.html ├── client ├── components │ ├── Error.js │ ├── Login.js │ ├── App.js │ └── User.js ├── actions │ └── actions.js ├── reducers │ └── index.js ├── index.js ├── style.less └── log_in.svg ├── webpack ├── prod.config.js └── dev.config.js ├── server ├── app.js └── routes.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/bundle.js 3 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Name of App 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/components/Error.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | /** 4 | * Our error page 5 | * Displays the error 6 | */ 7 | export default class Login extends Component { 8 | render() { 9 | // injected via react-router 10 | const { errorMsg } = this.props.params; 11 | return ( 12 |
13 |

An Error Occured

14 |

{errorMsg}

15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import loginSVG from '../log_in.svg'; 3 | 4 | /** 5 | * Our login page 6 | * Has a login button that hit's the login url 7 | */ 8 | export default class Login extends Component { 9 | render() { 10 | return ( 11 |
12 |

Here's our login page!

13 | 14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | /** 4 | * Main app component 5 | * Has a header and then render's the page content 6 | */ 7 | export default class SpotifyLogin extends Component { 8 | render() { 9 | // injected via react router 10 | const {children} = this.props; 11 | return ( 12 |
13 |

Example Spotify + React + React-Router Login Flow

14 |
15 |

This is an example of the Authorization Code flow using routes.

16 | {children} 17 |
18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | import Spotify from 'spotify-web-api-js'; 2 | const spotifyApi = new Spotify(); 3 | 4 | // our constants 5 | export const SPOTIFY_TOKENS = 'SPOTIFY_TOKENS'; 6 | export const SPOTIFY_ME_BEGIN = 'SPOTIFY_ME_BEGIN'; 7 | export const SPOTIFY_ME_SUCCESS = 'SPOTIFY_ME_SUCCESS'; 8 | export const SPOTIFY_ME_FAILURE = 'SPOTIFY_ME_FAILURE'; 9 | 10 | /** set the app's access and refresh tokens */ 11 | export function setTokens({accessToken, refreshToken}) { 12 | if (accessToken) { 13 | spotifyApi.setAccessToken(accessToken); 14 | } 15 | return { type: SPOTIFY_TOKENS, accessToken, refreshToken }; 16 | } 17 | 18 | /* get the user's info from the /me api */ 19 | export function getMyInfo() { 20 | return dispatch => { 21 | dispatch({ type: SPOTIFY_ME_BEGIN}); 22 | spotifyApi.getMe().then(data => { 23 | dispatch({ type: SPOTIFY_ME_SUCCESS, data: data }); 24 | }).catch(e => { 25 | dispatch({ type: SPOTIFY_ME_FAILURE, error: e }); 26 | }); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'babel-polyfill', 7 | path.join(__dirname, '../client/index'), 8 | ], 9 | output: { 10 | path: path.join(__dirname, '../public/'), 11 | filename: 'bundle.js', 12 | publicPath: '/', 13 | }, 14 | module: { 15 | loaders: [ 16 | { test: /\.svg$/, loaders: ['raw-loader']}, 17 | // take all less files, compile them, and bundle them in with our js bundle 18 | { test: /\.less$/, loader: 'style!css!autoprefixer?browsers=last 2 version!less' }, 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | query: { 24 | presets: ['es2015', 'react'], 25 | }, 26 | }, 27 | ], 28 | }, 29 | plugins: [ 30 | new webpack.DefinePlugin({ 31 | 'process.env': { 32 | // Useful to reduce the size of client-side libraries, e.g. react 33 | NODE_ENV: JSON.stringify('production'), 34 | }, 35 | }), 36 | // optimizations 37 | new webpack.optimize.DedupePlugin(), 38 | new webpack.optimize.OccurenceOrderPlugin(), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | compress: { 41 | warnings: false, 42 | }, 43 | }), 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | SPOTIFY_TOKENS, SPOTIFY_ME_BEGIN, SPOTIFY_ME_SUCCESS, SPOTIFY_ME_FAILURE 3 | } from '../actions/actions'; 4 | 5 | /** The initial state; no tokens and no user info */ 6 | const initialState = { 7 | accessToken: null, 8 | refreshToken: null, 9 | user: { 10 | loading: false, 11 | country: null, 12 | display_name: null, 13 | email: null, 14 | external_urls: {}, 15 | followers: {}, 16 | href: null, 17 | id: null, 18 | images: [], 19 | product: null, 20 | type: null, 21 | uri: null, 22 | } 23 | }; 24 | 25 | /** 26 | * Our reducer 27 | */ 28 | export default function reduce(state = initialState, action) { 29 | switch (action.type) { 30 | // when we get the tokens... set the tokens! 31 | case SPOTIFY_TOKENS: 32 | const {accessToken, refreshToken} = action; 33 | return Object.assign({}, state, {accessToken, refreshToken}); 34 | 35 | // set our loading property when the loading begins 36 | case SPOTIFY_ME_BEGIN: 37 | return Object.assign({}, state, { 38 | user: Object.assign({}, state.user, {loading: true}) 39 | }); 40 | 41 | // when we get the data merge it in 42 | case SPOTIFY_ME_SUCCESS: 43 | return Object.assign({}, state, { 44 | user: Object.assign({}, state.user, action.data, {loading: false}) 45 | }); 46 | 47 | // currently no failure state :( 48 | case SPOTIFY_ME_FAILURE: 49 | return state; 50 | 51 | default: 52 | return state; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** Module dependencies. */ 4 | const express = require('express'); 5 | const bodyParser = require('body-parser'); 6 | const cookieParser = require('cookie-parser'); 7 | const path = require('path'); 8 | const logger = require('morgan'); 9 | const routes = require('./routes'); 10 | 11 | const port = process.env.PORT || 3000; 12 | 13 | // configure the express server 14 | const app = express(); 15 | 16 | // if we're developing, use webpack middleware for module hot reloading 17 | if (process.env.NODE_ENV !== 'production') { 18 | console.log('==> 🌎 using webpack'); 19 | 20 | // load and configure webpack 21 | const webpack = require('webpack'); 22 | const webpackDevMiddleware = require('webpack-dev-middleware'); 23 | const webpackHotMiddleware = require('webpack-hot-middleware'); 24 | const config = require('../webpack/dev.config'); 25 | 26 | // setup middleware 27 | const compiler = webpack(config); 28 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 29 | app.use(webpackHotMiddleware(compiler)); 30 | } 31 | 32 | app.set('port', port); 33 | app.use(logger('dev')) 34 | .use(cookieParser()) 35 | .use(bodyParser.json()) 36 | .use(bodyParser.urlencoded({ extended: false })) 37 | .use(express.static(path.resolve(__dirname, '../public'))) 38 | .use('/', routes); 39 | 40 | // Start her up, boys 41 | app.listen(app.get('port'), () => { 42 | console.log('Express server listening on port ' + app.get('port')); 43 | }); 44 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | 'babel-polyfill', 9 | path.join(__dirname, '../client/index'), 10 | ], 11 | output: { 12 | path: path.join(__dirname, '../public/'), 13 | filename: 'bundle.js', 14 | publicPath: '/', 15 | }, 16 | module: { 17 | loaders: [ 18 | { test: /\.svg$/, loaders: ['raw-loader']}, 19 | // take all less files, compile them, and bundle them in with our js bundle 20 | { test: /\.less$/, loader: 'style!css!autoprefixer?browsers=last 2 version!less' }, 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | loader: 'babel-loader', 25 | query: { 26 | presets: ['es2015', 'react'], 27 | plugins: [['react-transform', { 28 | transforms: [{ 29 | transform: 'react-transform-hmr', 30 | imports: ['react'], 31 | // this is important for Webpack HMR: 32 | locals: ['module'] 33 | }], 34 | }]], 35 | }, 36 | }, 37 | ], 38 | }, 39 | plugins: [ 40 | new webpack.DefinePlugin({ 41 | 'process.env': { 42 | NODE_ENV: JSON.stringify('development'), 43 | }, 44 | }), 45 | new webpack.optimize.OccurenceOrderPlugin(), 46 | new webpack.HotModuleReplacementPlugin(), 47 | new webpack.NoErrorsPlugin(), 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import { Provider } from 'react-redux'; 6 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 7 | import { syncHistory, routeReducer } from 'react-router-redux'; 8 | import { createHistory } from 'history'; 9 | import reducer from './reducers'; 10 | import App from './components/App'; 11 | import Login from './components/Login'; 12 | import User from './components/User'; 13 | import Error from './components/Error'; 14 | 15 | // load our css. there probably is a better way to do this 16 | // but for now this is our move 17 | require('./style.less'); 18 | 19 | // Sync dispatched route actions to the history 20 | const reduxRouterMiddleware = syncHistory(hashHistory) 21 | const createStoreWithMiddleware = applyMiddleware( 22 | thunk, 23 | reduxRouterMiddleware 24 | )(createStore) 25 | const store = createStoreWithMiddleware(reducer) 26 | 27 | class Root extends Component { 28 | render() { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | // render town 44 | const rootElement = document.getElementById('root'); 45 | render(, rootElement); 46 | -------------------------------------------------------------------------------- /client/components/User.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | getMyInfo, 5 | setTokens, 6 | } from '../actions/actions'; 7 | 8 | /** 9 | * Our user page 10 | * Displays the user's information 11 | */ 12 | class User extends Component { 13 | /** When we mount, get the tokens from react-router and initiate loading the info */ 14 | componentDidMount() { 15 | // params injected via react-router, dispatch injected via connect 16 | const {dispatch, params} = this.props; 17 | const {accessToken, refreshToken} = params; 18 | dispatch(setTokens({accessToken, refreshToken})); 19 | dispatch(getMyInfo()); 20 | } 21 | 22 | /** Render the user's info */ 23 | render() { 24 | const { accessToken, refreshToken, user } = this.props; 25 | const { loading, display_name, images, id, email, external_urls, href, country, product } = user; 26 | const imageUrl = images[0] ? images[0].url : ""; 27 | // if we're still loading, indicate such 28 | if (loading) { 29 | return

Loading...

; 30 | } 31 | return ( 32 |
33 |

{`Logged in as ${display_name}`}

34 |
35 | 36 |
    37 |
  • Display name{display_name}
  • 38 |
  • Id{id}
  • 39 |
  • Email{email}
  • 40 |
  • Spotify URI{external_urls.spotify}
  • 41 |
  • Link{href}
  • 42 |
  • Profile Image{imageUrl}
  • 43 |
  • Country{country}
  • 44 |
  • Product{product}
  • 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default connect(state => state)(User); 53 | -------------------------------------------------------------------------------- /client/style.less: -------------------------------------------------------------------------------- 1 | @accent-color: #648F00; 2 | @error-color: #FB654A; 3 | @secondary-text-color: #c1c1c1; 4 | 5 | // get the page ready 6 | * { outline: none; box-sizing: border-box; } 7 | body { padding: 0; margin: 0; } 8 | html, body, .spotify-login { height: 100%; width: 100%; position: absolute; top: 0; right: 0; } 9 | body, button, input { 10 | font-family: "HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; 11 | font-size: 1em; 12 | } 13 | h1, h2, h3, h4, h5, h6 {margin: 0; font-family: "HelveticaNeue-UltraLight", "Helvetica Neue UltraLight", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;} 14 | h1 { font-size: 2.25em; font-weight: normal;} 15 | h2 { font-size: 1.75em; font-weight: normal; margin-bottom: 20px;} 16 | h3 { font-size: 1.5em; font-weight: normal;} 17 | h4 { font-size: 1.25em; font-weight: normal;} 18 | h5 { font-size: 1em; font-weight: normal;} 19 | h6 { font-size: 1em; font-weight: normal;} 20 | ul { list-style: none; margin: 0; padding: 0; } 21 | 22 | a { 23 | &:visited, &:link { 24 | color: @secondary-text-color; 25 | } 26 | &:hover, &:active { 27 | color: @accent-color; 28 | } 29 | } 30 | 31 | // let the real CSS begin 32 | .spotify-login { 33 | h1 { 34 | background-color: @accent-color; 35 | color: #fff; 36 | padding: 20px 50px; 37 | } 38 | .page-content { 39 | padding: 10px 50px 50px 50px; 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | .login { 44 | margin-left: auto; 45 | margin-right: auto; 46 | margin-top: 50px; 47 | text-align: center; 48 | .login-btn .spotify-btn { 49 | .main { 50 | fill: @accent-color; 51 | } 52 | &:hover .main { 53 | fill: black; 54 | } 55 | } 56 | } 57 | .error { 58 | color: @error-color; 59 | &, h2 { font-weight: bold; } 60 | } 61 | .user { 62 | .user-content { 63 | display: flex; 64 | align-items: center; 65 | img { 66 | width: 200px; 67 | height: 200px; 68 | } 69 | ul { 70 | li { 71 | display: flex; 72 | padding: 3px 0px; 73 | span:first-child { 74 | width: 150px; 75 | text-align: right; 76 | margin-right: 20px; 77 | } 78 | span:last-child { 79 | flex: 1; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Spotify = require('spotify-web-api-node'); 4 | const querystring = require('querystring'); 5 | const express = require('express'); 6 | const router = new express.Router(); 7 | 8 | // configure the express server 9 | const CLIENT_ID = process.env.client_id; 10 | const CLIENT_SECRET = process.env.client_secret; 11 | const REDIRECT_URI = process.env.redirect_uri || 'http://localhost:3000/callback'; 12 | const STATE_KEY = 'spotify_auth_state'; 13 | // your application requests authorization 14 | const scopes = ['user-read-private', 'user-read-email']; 15 | 16 | // configure spotify 17 | const spotifyApi = new Spotify({ 18 | clientId: CLIENT_ID, 19 | clientSecret: CLIENT_SECRET, 20 | redirectUri: REDIRECT_URI 21 | }); 22 | 23 | /** Generates a random string containing numbers and letters of N characters */ 24 | const generateRandomString = N => (Math.random().toString(36)+Array(N).join('0')).slice(2, N+2); 25 | 26 | /** 27 | * The /login endpoint 28 | * Redirect the client to the spotify authorize url, but first set that user's 29 | * state in the cookie. 30 | */ 31 | router.get('/login', (_, res) => { 32 | const state = generateRandomString(16); 33 | res.cookie(STATE_KEY, state); 34 | res.redirect(spotifyApi.createAuthorizeURL(scopes, state)); 35 | }); 36 | 37 | /** 38 | * The /callback endpoint - hit after the user logs in to spotifyApi 39 | * Verify that the state we put in the cookie matches the state in the query 40 | * parameter. Then, if all is good, redirect the user to the user page. If all 41 | * is not good, redirect the user to an error page 42 | */ 43 | router.get('/callback', (req, res) => { 44 | const { code, state } = req.query; 45 | const storedState = req.cookies ? req.cookies[STATE_KEY] : null; 46 | // first do state validation 47 | if (state === null || state !== storedState) { 48 | res.redirect('/#/error/state mismatch'); 49 | // if the state is valid, get the authorization code and pass it on to the client 50 | } else { 51 | res.clearCookie(STATE_KEY); 52 | // Retrieve an access token and a refresh token 53 | spotifyApi.authorizationCodeGrant(code).then(data => { 54 | const { expires_in, access_token, refresh_token } = data.body; 55 | 56 | // Set the access token on the API object to use it in later calls 57 | spotifyApi.setAccessToken(access_token); 58 | spotifyApi.setRefreshToken(refresh_token); 59 | 60 | // use the access token to access the Spotify Web API 61 | spotifyApi.getMe().then(({ body }) => { 62 | console.log(body); 63 | }); 64 | 65 | // we can also pass the token to the browser to make requests from there 66 | res.redirect(`/#/user/${access_token}/${refresh_token}`); 67 | }).catch(err => { 68 | res.redirect('/#/error/invalid token'); 69 | }); 70 | } 71 | }); 72 | 73 | module.exports = router; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-library-tone", 3 | "version": "0.0.1", 4 | "description": "Analyze the tone of your Spotify library with IBM Watson", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "better-npm-run start", 8 | "build": "better-npm-run build", 9 | "dev": "better-npm-run dev", 10 | "lint": "eslint --ignore-path .gitignore ." 11 | }, 12 | "betterScripts": { 13 | "build": { 14 | "command": "webpack --config webpack/prod.config.js --progress --colors", 15 | "env": { 16 | "NODE_ENV": "production" 17 | } 18 | }, 19 | "start": { 20 | "command": "node --harmony_destructuring server/app.js", 21 | "env": { 22 | "NODE_ENV": "production" 23 | } 24 | }, 25 | "dev": { 26 | "command": "node --harmony_destructuring server/app.js", 27 | "env": { 28 | "NODE_ENV": "development" 29 | } 30 | } 31 | }, 32 | "dependencies": { 33 | "better-npm-run": "0.0.6", 34 | "bluebird": "^3.2.1", 35 | "body-parser": "^1.14.2", 36 | "clone": "^1.0.2", 37 | "cookie-parser": "^1.4.1", 38 | "eslint": "^1.10.3", 39 | "eslint-config-airbnb": "^1.0.2", 40 | "eslint-plugin-react": "^3.16.1", 41 | "express": "^4.13.4", 42 | "history": "^1.17.0", 43 | "morgan": "^1.6.1", 44 | "object-assign": "^4.0.1", 45 | "querystring": "^0.2.0", 46 | "react": "^0.14.7", 47 | "react-dom": "^0.14.7", 48 | "react-hot-loader": "^1.3.0", 49 | "react-redux": "^4.2.1", 50 | "react-router": "^2.0.0-rc5", 51 | "react-router-redux": "^3.0.0", 52 | "react-transform-hmr": "^1.0.2", 53 | "redbox-react": "^1.2.2", 54 | "redux": "^3.2.1", 55 | "redux-thunk": "^1.0.3", 56 | "spotify-web-api-js": "^0.15.0", 57 | "spotify-web-api-node": "^2.2.0", 58 | "style-loader": "^0.13.0" 59 | }, 60 | "devDependencies": { 61 | "autoprefixer": "^6.3.0", 62 | "autoprefixer-loader": "^3.2.0", 63 | "babel": "^6.3.26", 64 | "babel-core": "^6.4.5", 65 | "babel-loader": "^6.2.1", 66 | "babel-plugin-react-transform": "^2.0.0", 67 | "babel-polyfill": "^6.3.14", 68 | "babel-preset-es2015": "^6.3.13", 69 | "babel-preset-react": "^6.3.13", 70 | "css-loader": "^0.23.1", 71 | "eslint": "^1.10.3", 72 | "eslint-config-airbnb": "^1.0.2", 73 | "eslint-plugin-react": "^3.11.2", 74 | "less": "^2.5.3", 75 | "less-loader": "^2.2.2", 76 | "raw-loader": "^0.5.1", 77 | "react-hot-loader": "^1.3.0", 78 | "react-transform-catch-errors": "^1.0.1", 79 | "react-transform-hmr": "^1.0.1", 80 | "redbox-react": "^1.2.0", 81 | "redux-devtools": "^3.0.1", 82 | "redux-devtools-dock-monitor": "^1.0.1", 83 | "redux-devtools-log-monitor": "^1.0.2", 84 | "redux-logger": "^2.4.0", 85 | "style-loader": "^0.13.0", 86 | "webpack": "^1.12.12", 87 | "webpack-dev-middleware": "^1.5.1", 88 | "webpack-dev-server": "^1.14.1", 89 | "webpack-hot-middleware": "^2.6.4" 90 | }, 91 | "engines": { 92 | "node": ">5.0.0" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /client/log_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 39 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Authorization With React + React-Router 2 | 3 | This is an example application demonstrating authenticating a user 4 | [against the Spotify Web API][sag], using [React][r] and [React-Router][rr] 5 | and [Redux][rx] and [React-Router-Redux][rrr]. 6 | 7 | ## Similarities to Spotify's [Web Auth Examples][wae] 8 | 9 | This example is a variation on the `authorization_code` demo from Spotify's 10 | [Web Auth Examples][wae]. The main difference is the client code; whereas their 11 | example is contained in one `index.html` file, this example shows how to do the 12 | same thing with React and React-Router. 13 | 14 | The other difference is the updated server code. Instead of using `request` 15 | directly (and XHR in the browser), this example interfaces with Spotify through 16 | the [Spotify Web API Node Module][swn] (and [Spotify Web Api Client][swj] in the 17 | browser). It also uses fun ES6 goodness. I opened a [pull request][spr] with 18 | them to update their server code to what you see here. 19 | 20 | ## Client Code Structure 21 | 22 | The client code is built with [React][r] and [React-Router][rr] and [Redux][rx] 23 | and [React-Router-Redux][rrr]. phew! 24 | 25 | The only real config this requires is in `client/index.js`: 26 | 27 | ~~~js 28 | class Root extends Component { 29 | render() { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | } 43 | ~~~ 44 | 45 | Here, we initialize redux with our store, initialize react router with its 46 | history object. Everything else is a fairly traditional React app - the 47 | components are in `client/components`, the actions are in `client/actions`, 48 | and the reducer is in `client/reducers`. 49 | 50 | ## Server Code Structure 51 | 52 | Under the `server` directory are two files `app.js` and `routes.js`. `app.js` 53 | handles all the setup, and all the routes are in, well, `routes.js`. 54 | 55 | ## Application Flow 56 | 57 | The basic flow is this: client hits `/login`, gets redirected to Spotify's auth 58 | url, then gets redirected to `/callback`. If all is good and dandy, we send the 59 | client to `/#/user/${access_token}/${refresh_token}` which triggers the User 60 | page to load via React-Router. If all ain't good, we redirect the client to 61 | `/#/error/${error message}` which triggers the Error page to load via 62 | React-Router. 63 | 64 | Once the client has the tokens, it requests information from spotify directly 65 | through use of the [Spotify Web API Client][swj]. This happens in 66 | `client/actions`, and the resulting data is interpreted through our reducer. 67 | Once the client has the data, `User.js` defines how it renders. 68 | 69 | ## Set Up 70 | 71 | Make sure you create your application, get your id and secret, and register 72 | your callback url - `localhost:3000/callback` is what I used - by following 73 | [Spotify's Getting Started Guide][sgs]. 74 | 75 | ## Running 76 | 77 | The first thing you'll need to do is set your applications client id, client 78 | secret, and callback url. You can do this via the environment variables 79 | `client_id`, `client_secret`, and `redirect_uri`. Or by typing them into the 80 | code in `server/routes.js`. Fun tip: because we're using [Better NPM Run][bnr], 81 | you can set these in your `package.json` - head over there to see an example. 82 | 83 | There are three scripts - `start`, `dev`, and `build`. 84 | 85 | To run the production bundle: 86 | 87 | ~~~bash 88 | $ npm run build 89 | $ npm start 90 | ~~~ 91 | 92 | To run in dev mode (with hot reloading, and un-minified source maps): 93 | 94 | ~~~bash 95 | $ npm run dev 96 | ~~~ 97 | 98 | ## Further Reading 99 | 100 | The application structure is a simplified version of my 101 | [React + Redux + Webpack Boilerplate][bp] for better ease of understanding. 102 | It can certainly be awesome-ified (and maybe a little more complicated) by 103 | doing some of the fun tricks in there. 104 | 105 | - [Spotify's Getting Started Guide][sgs] 106 | - [Spotify's Web API Authorization Guide][sag] 107 | - [Spotify Web API Node][swn] 108 | - [Spotify Web API JS/Client][swj] 109 | - [Spotify's Web API Auth Exampls][wae] 110 | - [My Pull Request enhancing Spotify's examples][spr] 111 | - [React Router][rr] 112 | - [React Router Redux][rrr] 113 | - [React][r] 114 | - [Redux][rx] 115 | - [Better NPM Run][bnr] 116 | - [React + Redux + Webpack Boilerplate][bp] 117 | 118 | [sgs]: https://developer.spotify.com/web-api/tutorial/ 119 | [sag]: https://developer.spotify.com/web-api/authorization-guide/ 120 | [swn]: https://github.com/JMPerez/spotify-web-api-node 121 | [swj]: https://github.com/JMPerez/spotify-web-api-js 122 | [wae]: https://github.com/spotify/web-api-auth-examples 123 | [spr]: https://github.com/spotify/web-api-auth-examples/pull/7 124 | [rr]: https://github.com/rackt/react-router 125 | [rrr]: https://github.com/rackt/react-router-redux 126 | [r]: https://facebook.github.io/react/ 127 | [rx]: http://redux.js.org/ 128 | [bnr]: https://www.npmjs.com/package/better-npm-run 129 | [bp]: https://github.com/kauffecup/react-redux-webpack-boilerplate 130 | --------------------------------------------------------------------------------