├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── bin └── webpack-dev-server.js ├── build ├── webpack-dev-server.js └── webpack │ ├── _base.js │ ├── development.js │ ├── development_hot.js │ └── production.js ├── config └── index.js ├── db.json ├── package.json ├── src ├── actions │ └── actions.js ├── components │ └── Athlete.js ├── containers │ ├── AthleteApp.js │ ├── DevTools.js │ ├── DevToolsWindow.js │ └── Root.js ├── helpers │ └── auth.js ├── index.html ├── index.js ├── layouts │ └── CoreLayout.js ├── reducers │ └── index.js ├── routes │ └── index.js ├── store │ └── configureStore.js ├── styles │ ├── _base.scss │ ├── core.scss │ └── vendor │ │ └── _normalize.scss ├── utils │ └── index.js └── views │ ├── AthleteView.js │ └── HomeView.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage" : 0 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | # Indentation style 8 | # Possible values - tab, space 9 | indent_style = space 10 | 11 | # Indentation size in single-spaced characters 12 | # Possible values - an integer, tab 13 | indent_size = 2 14 | 15 | # Line ending file format 16 | # Possible values - lf, crlf, cr 17 | end_of_line = lf 18 | 19 | # File character encoding 20 | # Possible values - latin1, utf-8, utf-16be, utf-16le 21 | charset = utf-8 22 | 23 | # Denotes whether to trim whitespace at the end of lines 24 | # Possible values - true, false 25 | trim_trailing_whitespace = true 26 | 27 | # Denotes whether file should end with a newline 28 | # Possible values - true, false 29 | insert_final_newline = true 30 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | *.spec.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "airbnb", 3 | "env" : { 4 | "browser" : true 5 | }, 6 | "globals" : { 7 | "__DEV__" : false, 8 | "__PROD__" : false, 9 | "__DEBUG__" : false, 10 | "__DEBUG_NW__" : false 11 | }, 12 | "rules": { 13 | "comma-dangle" : [1, "never"], 14 | "func-names" : 0, 15 | "key-spacing" : 0, 16 | "space-before-function-paren" : [2, "always"], 17 | "no-else-return" : 0, 18 | "no-multi-spaces" : 0, 19 | "quotes" : [2, "single"], 20 | "jsx-quotes" : [2, "prefer-single"], 21 | "one-var" : 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | .DS_STORE 3 | *.log 4 | 5 | node_modules 6 | 7 | dist 8 | src/credentials.js 9 | ======= 10 | # Logs 11 | logs 12 | *.log 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directory 35 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 36 | node_modules 37 | >>>>>>> 54e3d496f4fb7beb4ae4f506001cf3b0acb1f87e 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ~~Loosely based~~ Built upon [@davezuko's kit](https://github.com/davezuko/react-redux-starter-kit) in combination with the official Redux docs. 2 | ## Mock API 3 | For development purposes, a mock REST API server can be run using json-server. 4 | 5 | Install it with `npm install -g json-server` 6 | 7 | In a new terminal, run it with `json-server -p 4000 db.json` where `db.json` is the file with mock data. 8 | You can view the json-server page at http://localhost:4000, but it isn't really necessary. 9 | 10 | 11 | 12 | ## Getting Started 13 | ### `npm start` 14 | Runs the webpack build system with webpack-dev-server (http://localhost:3000) 15 | 16 | ### `npm run dev:nw` 17 | Same as `npm run start` but opens the debug tools in a new window. 18 | 19 | _Note:_ You need to enable popups in Chrome or you will get an error. 20 | 21 | ### `npm run compile` 22 | Runs the webpack build system with the current NODE_ENV and compiles the application to the `./dist` directory. Production builds will fail on eslint errors (but not on warnings). 23 | 24 | 25 | ## Structure 26 | 27 | 28 | ``` 29 | . 30 | ├── bin # Build/Start scripts 31 | ├── build # All build-related configuration 32 | │ ├── webpack # Environment-specific configuration files for Webpack 33 | ├── config # Project configuration settings 34 | ├── src # Application source code 35 | ├── components # Generic React Components (generally Dumb components) 36 | ├── containers # Components that provide context (e.g. Redux Providers) 37 | ├── layouts # Components that dictate major page structure 38 | ├── reducers # Redux reducers 39 | ├── routes # Application route definitions 40 | ├── stores # Redux store configuration 41 | ├── utils # Generic utilities 42 | ├── views # Components that live at a route 43 | └── index.js # Application bootstrap and rendering 44 | ``` 45 | -------------------------------------------------------------------------------- /bin/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | 3 | const devServer = require('../build/webpack-dev-server'); 4 | const config = require('../config'); 5 | 6 | const host = config.get('webpack_host'); 7 | const port = config.get('webpack_port'); 8 | devServer.listen(port, host, function () { 9 | console.log(`Webpack dev server running at ${host}: ${port}`); // eslint-disable-line 10 | }); 11 | -------------------------------------------------------------------------------- /build/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import config from '../config'; 4 | import webpackConfig from './webpack/development_hot'; 5 | 6 | const paths = config.get('utils_paths'); 7 | 8 | const server = new WebpackDevServer(webpack(webpackConfig), { 9 | contentBase : paths.project(config.get('dir_src')), 10 | hot : true, 11 | quiet : false, 12 | noInfo : false, 13 | lazy : false, 14 | stats : { 15 | colors : true 16 | }, 17 | historyApiFallback : true 18 | }); 19 | 20 | export default server; 21 | -------------------------------------------------------------------------------- /build/webpack/_base.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import config from '../../config'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | 5 | const paths = config.get('utils_paths'); 6 | 7 | const webpackConfig = { 8 | name : 'client', 9 | target : 'web', 10 | entry : { 11 | app : [ 12 | paths.project(config.get('dir_src')) 13 | ], 14 | vendor : config.get('vendor_dependencies') 15 | }, 16 | output : { 17 | filename : '[name].[hash].js', 18 | path : paths.project(config.get('dir_dist')), 19 | publicPath : '/' 20 | }, 21 | plugins : [ 22 | new webpack.DefinePlugin(config.get('globals')), 23 | new webpack.optimize.OccurrenceOrderPlugin(), 24 | new webpack.optimize.DedupePlugin(), 25 | new HtmlWebpackPlugin({ 26 | template : paths.src('index.html'), 27 | hash : true, 28 | filename : 'index.html', 29 | inject : 'body' 30 | }) 31 | ], 32 | resolve : { 33 | extensions : ['', '.js', '.jsx'], 34 | alias : config.get('utils_aliases') 35 | }, 36 | module : { 37 | loaders : [ 38 | { 39 | test : /\.(js|jsx)$/, 40 | exclude : /node_modules/, 41 | loader : 'babel', 42 | query : { 43 | stage : 0, 44 | optional : ['runtime'], 45 | env : { 46 | development : { 47 | plugins : ['react-transform'], 48 | extra : { 49 | 'react-transform' : { 50 | transforms : [{ 51 | transform : 'react-transform-catch-errors', 52 | imports : ['react', 'redbox-react'] 53 | }] 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | { 61 | test : /\.scss$/, 62 | loaders : [ 63 | 'style-loader', 64 | 'css-loader', 65 | 'autoprefixer?browsers=last 2 version', 66 | 'sass-loader' 67 | ] 68 | }, 69 | /* eslint-disable */ 70 | { test: /\.woff(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff" }, 71 | { test: /\.woff2(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2" }, 72 | { test: /\.ttf(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream" }, 73 | { test: /\.eot(\?.*)?$/, loader: "file-loader?prefix=fonts/&name=[path][name].[ext]" }, 74 | { test: /\.svg(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml" }, 75 | /* eslint-enable */ 76 | { test: /\.json$/, loader: 'json-loader'} 77 | ] 78 | }, 79 | sassLoader : { 80 | includePaths : paths.src('styles') 81 | }, 82 | /* add polyfills */ 83 | node : { 84 | net: 'empty', 85 | fs: 'empty', 86 | tls: 'empty' 87 | 88 | } 89 | }; 90 | 91 | const commonChunkPlugin = new webpack.optimize.CommonsChunkPlugin( 92 | 'vendor', '[name].[hash].js' 93 | ); 94 | 95 | webpackConfig.plugins.push(commonChunkPlugin); 96 | 97 | export default webpackConfig; 98 | -------------------------------------------------------------------------------- /build/webpack/development.js: -------------------------------------------------------------------------------- 1 | import webpackConfig from './_base'; 2 | 3 | webpackConfig.devtool = 'source-map'; 4 | 5 | export default webpackConfig; 6 | -------------------------------------------------------------------------------- /build/webpack/development_hot.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import config from '../../config'; 3 | import webpackConfig from './development'; 4 | 5 | webpackConfig.entry.app.push( 6 | `webpack-dev-server/client?${config.get('webpack_public_path')}`, 7 | `webpack/hot/dev-server` 8 | ); 9 | 10 | webpackConfig.plugins.push( 11 | new webpack.HotModuleReplacementPlugin(), 12 | new webpack.NoErrorsPlugin() 13 | ); 14 | 15 | // Apply the react-transform HMR plugin to Babel only when it's enabled 16 | webpackConfig.module.loaders = webpackConfig.module.loaders.map(loader => { 17 | if (/js(?!on)/.test(loader.test)) { 18 | loader.query.env.development.extra['react-transform'].transforms.push({ 19 | transform : 'react-transform-hmr', 20 | imports : ['react'], 21 | locals : ['module'] 22 | }); 23 | } 24 | 25 | return loader; 26 | }); 27 | 28 | export default webpackConfig; 29 | -------------------------------------------------------------------------------- /build/webpack/production.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import webpackConfig from './_base'; 4 | 5 | webpackConfig.module.loaders = webpackConfig.module.loaders.map(loader => { 6 | if (/css/.test(loader.test)) { 7 | const [first, ...rest] = loader.loaders; 8 | 9 | loader.loader = ExtractTextPlugin.extract(first, rest.join('!')); 10 | delete loader.loaders; 11 | } 12 | 13 | return loader; 14 | }); 15 | 16 | webpackConfig.plugins.push( 17 | new ExtractTextPlugin('[name].[contenthash].css'), 18 | new webpack.optimize.UglifyJsPlugin({ 19 | compress : { 20 | 'unused' : true, 21 | 'dead_code' : true 22 | } 23 | }) 24 | ); 25 | 26 | export default webpackConfig; 27 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { argv } from 'yargs'; 3 | import dotenv from 'dotenv'; 4 | 5 | dotenv.load(); 6 | const config = new Map(); 7 | 8 | //---------------------------- 9 | // User Configuration 10 | //---------------------------- 11 | config.set('dir_src', 'src'); 12 | config.set('dir_dist', 'dist'); 13 | 14 | config.set('webpack_host', 'localhost'); 15 | config.set('webpack_port', process.env.PORT || 3000); // eslint-disable-true 16 | 17 | config.set('vendor_dependencies', [ 18 | 'history', 19 | 'react', 20 | 'react-redux', 21 | 'react-router', 22 | 'redux', 23 | 'redux-router' 24 | ]); 25 | 26 | //--------------------------- 27 | // Internal Configuration 28 | //--------------------------- 29 | 30 | // Environment 31 | 32 | config.set('env', process.env.NODE_ENV); 33 | config.set('globals', { 34 | 'process.env' : { 35 | 'NODE_ENV' : JSON.stringify(config.get('env')) 36 | }, 37 | 'NODE_ENV' : config.get('env'), 38 | '__DEV__' : config.get('env') === 'development', 39 | '__PROD__' : config.get('env') === 'production', 40 | '__DEBUG__' : config.get('env') === 'development' && !argv.no_debug, 41 | '__DEBUG_NW__' : !!argv.nw 42 | }); 43 | 44 | // Webpack 45 | 46 | config.set('webpack_public_path', 47 | `http://${config.get('webpack_host')}:${config.get('webpack_port')}/` 48 | ); 49 | 50 | // Project 51 | 52 | config.set('path_project', path.resolve(__dirname, '../')); 53 | 54 | // Utilities 55 | const paths = (() => { 56 | const base = [config.get('path_project')]; 57 | const resolve = path.resolve; 58 | 59 | const project = (...args) => resolve.apply(resolve, [...base, ...args]); 60 | 61 | return { 62 | project : project, 63 | src : project.bind(null, config.get('dir_src')), 64 | dist : project.bind(null, config.get('dir_dist')) 65 | }; 66 | })(); 67 | 68 | config.set('utils_paths', paths); 69 | config.set('utils_aliases', [ 70 | 'actions', 71 | 'components', 72 | 'constants', 73 | 'containers', 74 | 'layouts', 75 | 'reducers', 76 | 'routes', 77 | 'services', 78 | 'styles', 79 | 'utils', 80 | 'views' 81 | ].reduce((acc, dir) => ((acc[dir] = paths.src(dir)) && acc), {})); 82 | 83 | export default config; 84 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "athletes": [ 3 | { 4 | "id": 0, 5 | "first_name": "Bo", 6 | "last_name": "Jackson", 7 | "city": "Oakland", 8 | "state": "California", 9 | "weapon": "bat" 10 | }, 11 | { 12 | "id": 1, 13 | "first_name": "Michael", 14 | "last_name": "Jordan", 15 | "city": "Chicago", 16 | "state": "Illinois", 17 | "weapon": "balls" 18 | }, 19 | { 20 | "id": 2, 21 | "first_name": "Wayne", 22 | "last_name": "Gretzky", 23 | "city": "Los Angeles", 24 | "state": "California", 25 | "weapon": "hockey stick" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "version": "1.0.0", 4 | "description": "Based on https://github.com/davezuko/react-redux-starter-kit and the Redux docs", 5 | "main": "index.js", 6 | "dependencies": { 7 | "babel": "^5.8.23", 8 | "dotenv": "^1.2.0", 9 | "history": "^1.9.0", 10 | "isomorphic-fetch": "^2.2.0", 11 | "react": "^0.14.0", 12 | "react-dom": "^0.14.0", 13 | "react-redux": "^4.0.0", 14 | "react-router": "1.0.0-rc1", 15 | "redux": "^3.0.0", 16 | "redux-router": "^1.0.0-beta3", 17 | "redux-thunk": "^1.0.0", 18 | "request-promise": "^1.0.2", 19 | "yargs": "^3.18.0" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^6.0.3", 23 | "autoprefixer-loader": "^3.1.0", 24 | "babel-eslint": "^4.1.1", 25 | "babel-loader": "^5.0.0", 26 | "babel-plugin-react-transform": "^1.1.0", 27 | "babel-runtime": "^5.8.20", 28 | "css-loader": "^0.21.0", 29 | "eslint": "^1.0.0", 30 | "eslint-config-airbnb": "0.1.0", 31 | "eslint-plugin-react": "^3.3.1", 32 | "extract-text-webpack-plugin": "^0.8.0", 33 | "file-loader": "^0.8.4", 34 | "html-webpack-plugin": "^1.6.1", 35 | "json-loader": "^0.5.3", 36 | "node-sass": "^3.3.3", 37 | "react-addons-test-utils": "^0.14.0", 38 | "react-transform-catch-errors": "^1.0.0", 39 | "react-transform-hmr": "^1.0.0", 40 | "redbox-react": "^1.0.4", 41 | "redux-devtools": "3.0.0-beta-3", 42 | "redux-devtools-dock-monitor": "^1.0.0-beta-3", 43 | "redux-devtools-log-monitor": "^1.0.0-beta-3", 44 | "sass-loader": "^3.0.0", 45 | "style-loader": "^0.13.0", 46 | "url-loader": "^0.5.6", 47 | "webpack": "^1.11.0", 48 | "webpack-dev-server": "^1.10.1" 49 | }, 50 | "scripts": { 51 | "clean": "rm -rf dist", 52 | "lint": "eslint ./src/**/*.js", 53 | "compile": "webpack", 54 | "start": "npm run dev", 55 | "dev": "NODE_ENV=development node --harmony bin/webpack-dev-server", 56 | "dev:nw": "npm run dev -- --nw", 57 | "dev:no-debug": "npm run dev -- --no_debug", 58 | "test": "echo \"Error: no test specified\" && exit 1", 59 | "deploy": "npm run lint && npm run compile" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "" 64 | }, 65 | "author": "", 66 | "license": "ISC", 67 | "bugs": { 68 | "url": "" 69 | }, 70 | "homepage": "" 71 | } 72 | -------------------------------------------------------------------------------- /src/actions/actions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | export const REQUEST_ATHLETES = 'REQUEST_ATHLETES' 4 | export const RECEIVE_ATHLETES = 'RECEIVE_ATHLETES' 5 | 6 | 7 | function requestAthletes(){ 8 | return { 9 | type: REQUEST_ATHLETES 10 | } 11 | } 12 | 13 | function receiveAthletes(json) { 14 | return { 15 | type: RECEIVE_ATHLETES, 16 | athletes: json, 17 | receivedAt: Date.now() 18 | } 19 | } 20 | 21 | export function fetchAthletes() { 22 | 23 | return dispatch => { 24 | dispatch(requestAthletes()) 25 | return fetch(`http://localhost:4000/athletes`) 26 | .then(req => req.json()) 27 | .then(json => dispatch(receiveAthletes(json))) 28 | } 29 | } 30 | 31 | function shouldFetchAthletes(state) { 32 | console.log('shouldFetchAthletes called') 33 | console.trace() 34 | 35 | const athletes = state.athletes.items 36 | 37 | return !athletes.length 38 | } 39 | 40 | export function fetchAthletesIfNeeded() { 41 | return (dispatch, getState) => { 42 | if (shouldFetchAthletes(getState())) { 43 | return dispatch(fetchAthletes()) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Athlete.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class Athlete extends Component { 5 | 6 | render() { 7 | const { items } = this.props 8 | var props = this.props 9 | console.log('Ath component this.props:' + JSON.stringify(this.props)) 10 | 11 | return ( 12 |
13 |

Hi from the AthleteComponent

14 |

15 | {JSON.stringify(props)} 16 |

17 |
18 | ) 19 | } 20 | } 21 | 22 | Athlete.propTypes = { 23 | items: PropTypes.object 24 | } 25 | -------------------------------------------------------------------------------- /src/containers/AthleteApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Router, Route, Link } from 'react-router' 4 | import { pushState } from 'redux-router' 5 | import { requestAthletes, receiveAthletes, fetchAthletes, fetchAthletesIfNeeded } from '../actions/actions' 6 | 7 | class AthleteApp extends Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentDidMount() { 13 | const { dispatch } = this.props 14 | dispatch(fetchAthletes()) 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (nextProps !== this.props) { 19 | const { dispatch } = nextProps 20 | dispatch(fetchAthletesIfNeeded()) 21 | } 22 | } 23 | 24 | renderItems(items) { 25 | return items.map((item) => { 26 | return ( 27 |

{item.first_name}

28 | ) 29 | }) 30 | } 31 | 32 | render() { 33 | const { items, isFetching, lastUpdated } = this.props 34 | return ( 35 |
36 | {items.length > 0 && 37 |
{this.renderItems(items)}
38 | } 39 |

40 | {lastUpdated && 41 | 42 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}. 43 | 44 | } 45 |

46 | {!isFetching && items.length === 0 && 47 |

No data to display!

48 | } 49 | {isFetching && 50 |
51 |

Fetching Items

52 |
53 | } 54 |
55 | ) 56 | } 57 | } 58 | 59 | AthleteApp.propTypes = { 60 | items: PropTypes.array.isRequired, 61 | isFetching: PropTypes.bool.isRequired, 62 | lastUpdated: PropTypes.number, 63 | dispatch: PropTypes.func.isRequired 64 | } 65 | 66 | 67 | 68 | function mapStateToProps({router, athletes}) { 69 | 70 | var isFetching = athletes.isFetching 71 | var items = athletes.items 72 | var lastUpdated = athletes.lastUpdated 73 | 74 | return { 75 | isFetching, 76 | items, 77 | lastUpdated 78 | } 79 | 80 | } 81 | 82 | export default connect(mapStateToProps)(AthleteApp) 83 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/containers/DevToolsWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | 5 | export default createDevTools( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import routes from '../routes'; 4 | import { ReduxRouter } from 'redux-router'; 5 | import DevTools from './DevTools'; 6 | import { createDevToolsWindow } from '../utils'; 7 | import configureStore from '../store/configureStore'; 8 | import AthleteApp from './AthleteApp'; 9 | 10 | const store = configureStore() 11 | 12 | export default class Root extends React.Component { 13 | static propTypes = { 14 | store : React.PropTypes.object.isRequired, 15 | debug : React.PropTypes.bool, 16 | debugExternal : React.PropTypes.bool 17 | } 18 | 19 | static defaultProps = { 20 | debug : false, 21 | debugExternal : false 22 | } 23 | 24 | renderDevTools () { 25 | if (!this.props.debug) { 26 | return null; 27 | } 28 | 29 | return this.props.debugExternal ? 30 | createDevToolsWindow(this.props.store) : ; 31 | } 32 | 33 | render () { 34 | return ( 35 |
36 | 37 |
38 | 39 | {routes} 40 | 41 | {this.renderDevTools()} 42 |
43 |
44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/helpers/auth.js: -------------------------------------------------------------------------------- 1 | import { AUTH_URL } from '../constants/auth'; 2 | import { email, password, token } from '../credentials'; 3 | 4 | 5 | let rp = require('request-promise'); 6 | 7 | 8 | function getAthletes() { 9 | let reqOptions = { 10 | url: 'http://localhost:4000/athletes', 11 | json: true 12 | }; 13 | 14 | return new Promise(function(resolve, reject){ 15 | rp(reqOptions) 16 | .then(function(response) { 17 | console.log(response) 18 | resolve(response); 19 | }) 20 | .catch(function(err) { 21 | reject(err); 22 | }) 23 | }) 24 | 25 | } 26 | 27 | 28 | class Auth { 29 | getToken () { 30 | return (console.log('maybe this works' + getAthletes())); 31 | } 32 | } 33 | 34 | export default new Auth(); 35 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React + Redux 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './containers/Root'; 4 | import configureStore from './store/configureStore'; 5 | 6 | const target = document.getElementById('root'); 7 | const store = configureStore(window.__INITIAL_STATE__, __DEBUG__); 8 | 9 | const node = ( 10 | 13 | ); 14 | 15 | ReactDOM.render(node, target); 16 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'styles/core.scss'; 3 | 4 | export default class CoreLayout extends React.Component { 5 | static propTypes = { 6 | children : React.PropTypes.element 7 | } 8 | 9 | render () { 10 | return ( 11 |
12 |
13 | {this.props.children} 14 |
15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerStateReducer } from 'redux-router'; 3 | import { REQUEST_ATHLETES, RECEIVE_ATHLETES } from '../actions/actions' 4 | 5 | function athletes(state = { 6 | isFetching: false, 7 | items: [] 8 | }, action) { 9 | switch (action.type) { 10 | case REQUEST_ATHLETES: 11 | return Object.assign({}, state, { 12 | isFetching: true 13 | }) 14 | case RECEIVE_ATHLETES: 15 | return Object.assign({}, state, { 16 | isFetching: false, 17 | items: action.athletes, 18 | lastUpdated: action.receivedAt 19 | }) 20 | default: 21 | return state 22 | } 23 | } 24 | 25 | 26 | 27 | // export default combineReducers({ 28 | // router: routerStateReducer, 29 | // athletes: athletes 30 | // 31 | // }); 32 | 33 | let router = routerStateReducer; 34 | const rootReducer = combineReducers({ 35 | router, 36 | athletes 37 | }) 38 | 39 | export default rootReducer 40 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import CoreLayout from 'layouts/CoreLayout'; 4 | import HomeView from 'views/HomeView'; 5 | import AthleteView from 'views/AthleteView'; 6 | 7 | export default ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import rootReducer from '../reducers'; 2 | import thunk from 'redux-thunk'; 3 | import routes from '../routes'; 4 | import { reduxReactRouter } from 'redux-router'; 5 | import createHistory from 'history/lib/createBrowserHistory'; 6 | import DevTools from 'containers/DevTools'; 7 | import { 8 | applyMiddleware, 9 | compose, 10 | createStore 11 | } from 'redux'; 12 | 13 | export default function configureStore (initialState, debug = false) { 14 | let createStoreWithMiddleware; 15 | 16 | const middleware = applyMiddleware(thunk); 17 | 18 | if (debug) { 19 | createStoreWithMiddleware = compose( 20 | middleware, 21 | reduxReactRouter({ routes, createHistory }), 22 | DevTools.instrument() 23 | ); 24 | } else { 25 | createStoreWithMiddleware = compose( 26 | middleware, 27 | reduxReactRouter({ routes, createHistory }) 28 | ); 29 | } 30 | 31 | const store = createStoreWithMiddleware(createStore)( 32 | rootReducer, initialState 33 | ); 34 | 35 | if (module.hot) { 36 | module.hot.accept('../reducers', () => { 37 | const nextRootReducer = require('../reducers/index'); 38 | 39 | store.replaceReducer(nextRootReducer); 40 | }); 41 | } 42 | return store; 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | // Settings go here 2 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import 'vendor/normalize'; 3 | -------------------------------------------------------------------------------- /src/styles/vendor/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import DevTools from 'containers/DevToolsWindow'; 5 | 6 | export function createConstants (...constants) { 7 | return constants.reduce((acc, constant) => { 8 | acc[constant] = constant; 9 | return acc; 10 | }, {}); 11 | } 12 | 13 | export function createReducer (initialState, reducerMap) { 14 | return (state = initialState, action) => { 15 | const reducer = reducerMap[action.type]; 16 | 17 | return reducer ? reducer(state, action.payload) : state; 18 | }; 19 | } 20 | 21 | export function createDevToolsWindow (store) { 22 | const win = window.open( 23 | null, 24 | 'redux-devtools', // give it a name so it reuses the same window 25 | `width=400,height=${window.outerHeight},menubar=no,location=no,resizable=yes,scrollbars=no,status=no` 26 | ); 27 | 28 | // reload in case it's reusing the same window with the old content 29 | win.location.reload(); 30 | 31 | // wait a little bit for it to reload, then render 32 | setTimeout(() => { 33 | // Wait for the reload to prevent: 34 | // "Uncaught Error: Invariant Violation: _registerComponent(...): Target container is not a DOM element." 35 | win.document.write('
'); 36 | win.document.body.style.margin = '0'; 37 | 38 | ReactDOM.render( 39 | 40 | 41 | 42 | , win.document.getElementById('react-devtools-root') 43 | ); 44 | }, 10); 45 | } 46 | -------------------------------------------------------------------------------- /src/views/AthleteView.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes }from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import { Router, Route, Link } from 'react-router' 5 | import { pushState } from 'redux-router' 6 | import * as athleteActions from '../actions/actions' 7 | import Athlete from '../components/Athlete' 8 | 9 | const mapStateToProps = (state) => ({ 10 | athletes : state.athletes.items, 11 | routerState : state.router 12 | }) 13 | 14 | const mapDispatchToProps = (dispatch) => ({ 15 | actions : bindActionCreators(athleteActions, dispatch) 16 | }) 17 | 18 | export class AthleteView extends Component { 19 | constructor(props){ 20 | super(props) 21 | } 22 | 23 | componentDidMount() { 24 | console.log('Mounted an AthleteView') 25 | console.log('size of athlete array after component mount:' + this.props.athletes.length) 26 | 27 | } 28 | 29 | // This method doesn't seem to be called ever... 30 | componentWillReceiveProps(nextProps) { 31 | console.log('Ath View ComponentWillReceiveProps') 32 | if (nextProps !== this.props) { 33 | this.props.actions.fetchAthletesIfNeeded() 34 | } 35 | } 36 | 37 | componentWillMount() { 38 | console.log('Component Will Mount') 39 | if (this.props.athletes.length === 0) { 40 | console.log('Atheletes array is empty, we should fetch data') 41 | this.props.actions.fetchAthletesIfNeeded() 42 | } 43 | //console.log('Component will mount props:' + JSON.stringify(this.props, null, 2)) 44 | } 45 | 46 | 47 | static propTypes = { 48 | athlete: React.PropTypes.object 49 | } 50 | 51 | render () { 52 | const { id } = this.props.params; 53 | return ( 54 |
55 |

Athlete page!

56 | 57 |
58 | ) 59 | } 60 | } 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(AthleteView) 63 | -------------------------------------------------------------------------------- /src/views/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import AthleteApp from '../containers/AthleteApp'; 5 | import fetchAthletes from '../actions/actions' 6 | 7 | const actionCreators = { 8 | fetchAthletes : () => ({ type : 'REQUEST_ATHLETES' }) 9 | }; 10 | 11 | const mapStateToProps = (state) => ({ 12 | athletes : state.athletes.items, 13 | routerState : state.router 14 | }); 15 | const mapDispatchToProps = (dispatch) => ({ 16 | actions : bindActionCreators(actionCreators, dispatch) 17 | }); 18 | 19 | export class HomeView extends React.Component { 20 | 21 | static propTypes = { 22 | actions : React.PropTypes.object, 23 | athletes : React.PropTypes.array 24 | } 25 | 26 | render () { 27 | return ( 28 |
29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | export default connect(mapStateToProps, mapDispatchToProps)(HomeView); 36 | 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | 3 | const config = require('./config'); 4 | module.exports = require('./build/webpack/' + config.get('env')); 5 | 6 | --------------------------------------------------------------------------------