├── .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 |
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 |
--------------------------------------------------------------------------------