├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app ├── fonts │ └── .gitkeep ├── images │ └── .gitkeep ├── src │ ├── actions │ │ ├── actionCreators.js │ │ └── amazingComponent.js │ ├── components │ │ ├── AmazingBox │ │ │ ├── AmazingBox.js │ │ │ └── AmazingBox.module.scss │ │ ├── AmazingComponent │ │ │ ├── AmazingComponent.js │ │ │ └── AmazingComponent.module.scss │ │ ├── AmazingInput │ │ │ ├── AmazingInput.js │ │ │ └── AmazingInput.module.scss │ │ ├── App.jsx │ │ ├── HacksmithsLogo │ │ │ ├── HacksmithsLogo.js │ │ │ ├── HacksmithsLogo.module.scss │ │ │ └── hacksmiths.png │ │ ├── Main.js │ │ └── index.js │ ├── constants │ │ └── amazingComponent.js │ ├── containers │ │ ├── MyAmazingContainer │ │ │ ├── MyAmazingContainer.js │ │ │ └── MyAmazingContainer.module.scss │ │ └── index.js │ ├── index.js │ ├── pages │ │ ├── LandingPage │ │ │ ├── LandingPage.js │ │ │ └── LandingPage.module.scss │ │ ├── NotFoundPage │ │ │ ├── NotFoundPage.js │ │ │ └── NotFoundPage.module.scss │ │ └── index.js │ ├── reducers │ │ ├── amazingComponent.js │ │ └── index.js │ ├── store │ │ ├── initialState.js │ │ └── store.js │ └── utils │ │ ├── network.js │ │ ├── request.js │ │ └── router.js ├── styles │ ├── settings.scss │ └── styles.scss └── tests │ ├── actions │ └── amazingComponent.js │ ├── components │ └── .gitkeep │ ├── reducers │ └── amazingComponent.js │ └── test_helper.js ├── index.html ├── package.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | app/build/ 2 | app/dist/ 3 | webpack.config.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "rules": { 10 | "comma-dangle": [2, "never"], 11 | "func-names": 0, 12 | "eol-last": 0 13 | }, 14 | "plugins": [ 15 | "react", 16 | ] 17 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-language=JavaScript 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | npm-debug.log 4 | .DS_Store 5 | ======= 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 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 directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan Collins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | This project will no longer be updated, in favor of focusing my efforts on the [Scalable React Boilerplate](https://github.com/RyanCCollins/scalable-react-boilerplate) and [Generator Scalable React](https://github.com/RyanCCollins/slush-generator-scalable-react). Feel free to keep using it! 3 | 4 | ![React Redux Simple Starter Logo](https://github.com/RyanCCollins/cdn/blob/master/react-redux-simple-starter/logo.png?raw=true) 5 | # Behind the boilerplate 6 | The hardest part about React is getting setup. This project aims to help anyone quickly get bootstrapped with the latest versions of React, Redux, Webpack, etc. It uses Hot Module Reloading and has a few optional add-ons like React Foundation, Redux Form, etc. 7 | 8 | It follows best practices, including the [AirBnb JS & JSX style guides](https://github.com/airbnb/javascript) and uses the FTF (file-type first) organizational pattern. 9 | 10 | ## Scalable / Feature First Boilerplate 11 | If you're looking for something with a bit more features, take a look at the [Scalable React Boilerplate](https://github.com/RyanCCollins/scalable-react-boilerplate) project! 12 | 13 | # Documentation 14 | 15 | ## Getting Started 16 | To try the example application out or to use the project, follow the instructions below. 17 | 18 | 1. **Clone repo** 19 | 20 | git clone https://github.com/RyanCCollins/react-redux-simple-starter.git 21 | 22 | 2. **Install dependencies** 23 | 24 | npm run setup 25 | 26 | 3. **Run development server** 27 | 28 | npm run start 29 | 30 | Development server should be running at http://localhost:8080/ 31 | 32 | 4. **Make build** 33 | 34 | npm run build 35 | 36 | ### File Structure 37 | * Some files left out for brevity. Please reference the files in the [React Redux Simple Starter](https://github.com/RyanCCollins/react-redux-simple-starter) project for information about the file structure. 38 | ``` 39 | . 40 | ├── README.md 41 | ├── LICENSE 42 | ├── index.html 43 | ├── package.json 44 | ├── webpack.config.js 45 | ├── app/ 46 | | ├── fonts 47 | | ├── images 48 | | ├── src 49 | | | ├── actions 50 | | | ├── components 51 | | | | ├── MyComponent 52 | | | | ├── MyOtherComponent 53 | | | | ├── App.js 54 | | | | ├── Main.js 55 | | | | └── index.js 56 | | | ├── containers 57 | | | | ├── MyContainer 58 | | | | └── index.js 59 | | | ├── pages 60 | | | ├── reducers 61 | | | ├── store 62 | | | ├── utils 63 | | | └── index.js 64 | | ├── styles 65 | | └── tests 66 | | | ├── actions 67 | | | ├── components 68 | | | ├── reducers 69 | | | └── test_helper.js 70 | ├── .eslintignore 71 | ├── .eslintrc 72 | ├── .gitattributes 73 | └── .gitignore 74 | ``` 75 | 76 | ## Scripts 77 | - **npm run setup** 78 | 79 | Installs the application's dependencies 80 | 81 | - **npm run test** 82 | 83 | Runs unit tests 84 | 85 | - **npm run test:watch** 86 | 87 | Watches for changes to run unit tests 88 | 89 | - **npm run build** 90 | 91 | Bundles the application 92 | 93 | - **npm run dev** 94 | 95 | Starts webpack development server 96 | 97 | - **npm run lint** 98 | 99 | Runs the linter 100 | 101 | - **npm run deploy** 102 | 103 | Creates the production ready files 104 | 105 | - **npm run clean** 106 | 107 | Removes the bundled code and the production ready files 108 | 109 | 110 | 111 | ## Technologies / Libraries 112 | 113 | - [Node](https://nodejs.org/en/) - JS runtime environment 114 | - [npm](https://www.npmjs.com/) - package manager 115 | - [Babel](https://babeljs.io/) - ES6 transpiler 116 | - [Webpack](https://webpack.github.io/) - module bundler & task runner 117 | - [React](https://facebook.github.io/react/) - interfaces 118 | - [react-hot-loader](https://github.com/gaearon/react-hot-loader) - hot reloading for react 119 | - [react-router](https://github.com/rackt/react-router) - react application router 120 | - [react-redux](https://github.com/rackt/react-redux) - react bindings for redux 121 | - [react-css-modules](https://github.com/gajus/react-css-modules) - React CSS Modules implement automatic mapping of CSS modules. 122 | - [react-foundation](https://github.com/nordsoftware/react-foundation) - Foundation React components, use or don't use. 123 | - [Immutable](https://github.com/facebook/immutable-js) - data structures 124 | - [Redux](https://github.com/rackt/redux) - awesome flux architecture 125 | - [Redux Form](https://github.com/erikras/redux-form)- works with React Redux to enable an html form in React to use Redux to store all of its state. 126 | - [redux-thunk](https://github.com/gaearon/redux-thunk) - thunk middleware for redux 127 | - [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) - API fetch network requests 128 | - [redux-devtools](https://github.com/gaearon/redux-devtools) - redux development tool 129 | - [SASS](http://sass-lang.com/) - styles 130 | - [ESLint](http://eslint.org/) - linter 131 | - [Mocha](http://mochajs.org/) - unit tests 132 | - [jsdom](https://github.com/tmpvar/jsdom) - vdom to test React without browser 133 | - [Expect](https://github.com/mjackson/expect) - assertion library 134 | - [Chai / Immutable](http://chaijs.com/) - assertion library for Immutable JS 135 | - A bunch of useful scripts 136 | 137 | 138 | ## Timeline / TODOS 139 | * [x] Write README file 140 | * [x] Setup Filetype First organization 141 | * [x] Add better demonstration of included libraries (run the test application) 142 | * [x] Write unit tests and setup folder structure for testing (See tests directory for examples) 143 | * [x] Migrate to Feature First file organization as noted [in this article](http://engineering.kapost.com/2016/01/organizing-large-react-applications/) and in the [React Boilerplate](https://github.com/mxstbr/react-boilerplate) (See [here](https://github.com/RyanCCollins/scalable-react-boilerplate)) 144 | * [ ] Write component tests using Jest and / or Enzyme 145 | * [ ] Write wiki / other documentation 146 | * [ ] Implement a Rails like component generator 147 | 148 | 149 | ### Acknowledgements 150 | 151 | This project is loosely based on: [This boilerplate](https://github.com/mezod/boilerplate-koa-redux-react). 152 | 153 | Thank you to @mezod for their hard work and inspiration. 154 | -------------------------------------------------------------------------------- /app/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanCCollins/react-redux-simple-starter/67fcb738b2edb5ef88d7ac577c1cd266f0e852d3/app/fonts/.gitkeep -------------------------------------------------------------------------------- /app/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanCCollins/react-redux-simple-starter/67fcb738b2edb5ef88d7ac577c1cd266f0e852d3/app/images/.gitkeep -------------------------------------------------------------------------------- /app/src/actions/actionCreators.js: -------------------------------------------------------------------------------- 1 | export const GLOBAL_ACTION_CREATOR = 'GLOBAL_ACTION_CREATOR'; 2 | -------------------------------------------------------------------------------- /app/src/actions/amazingComponent.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/amazingComponent'; 2 | 3 | export const addBox = (content) => ({ 4 | type: types.ADD_BOX, 5 | content 6 | }); 7 | 8 | export const removeBox = (index) => ({ 9 | type: types.REMOVE_BOX, 10 | index 11 | }); -------------------------------------------------------------------------------- /app/src/components/AmazingBox/AmazingBox.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './AmazingBox.module.scss'; 4 | 5 | const randomColor = Math.floor(Math.random() * 16777215).toString(16); 6 | const BoxStyle = { 7 | colorStyle: { 8 | color: `#${randomColor}` 9 | } 10 | }; 11 | 12 | class AmazingBox extends Component { 13 | constructor() { 14 | super(); 15 | this.handleRemoveItem = this.handleRemoveItem.bind(this); 16 | } 17 | handleRemoveItem() { 18 | const { 19 | onRemove, 20 | id 21 | } = this.props; 22 | onRemove(id); 23 | } 24 | render() { 25 | const { 26 | id, 27 | content 28 | } = this.props; 29 | return ( 30 |
35 |

#{id}

36 |

{content}

37 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | AmazingBox.propTypes = { 49 | content: PropTypes.string.isRequired, 50 | onRemove: PropTypes.func.isRequired, 51 | id: PropTypes.number.isRequired 52 | }; 53 | 54 | export default cssModules(AmazingBox, styles); 55 | -------------------------------------------------------------------------------- /app/src/components/AmazingBox/AmazingBox.module.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | width: 200px; 3 | height: 200px; 4 | border: 1px solid black; 5 | position: relative; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | margin: 10px; 10 | } 11 | 12 | .btnClose { 13 | position: absolute; 14 | top: 10px; 15 | right: 10px; 16 | font-size: 25px; 17 | cursor: pointer; 18 | } 19 | 20 | .number { 21 | position: absolute; 22 | left: 5px; 23 | top: 5px; 24 | font-family: 'Open Sans'; 25 | font-size: 1.8rem; 26 | } 27 | 28 | .text { 29 | text-shadow: #CCC 1px 0 10px; 30 | font-size: 1.5rem; 31 | font-family: 'Lato'; 32 | letter-spacing: 5px; 33 | text-align: center; 34 | } 35 | -------------------------------------------------------------------------------- /app/src/components/AmazingComponent/AmazingComponent.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './AmazingComponent.module.scss'; 4 | import { AmazingInput, AmazingBox } from 'components'; 5 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 6 | 7 | const AmazingComponent = ({ 8 | boxes, 9 | onAddBox, 10 | onRemoveBox 11 | }) => ( 12 |
13 |

Play with redux by adding a box to the wall

14 | 17 |
18 | 24 | {boxes.map((box, i) => 25 | 31 | )} 32 | {!boxes.length && 33 |

34 | No boxes here 😔. Why don't you add one! 35 |

36 | } 37 |
38 |
39 |
40 | ); 41 | 42 | AmazingComponent.propTypes = { 43 | boxes: PropTypes.array.isRequired, 44 | onAddBox: PropTypes.func.isRequired, 45 | onRemoveBox: PropTypes.func.isRequired 46 | }; 47 | 48 | export default cssModules(AmazingComponent, styles); 49 | -------------------------------------------------------------------------------- /app/src/components/AmazingComponent/AmazingComponent.module.scss: -------------------------------------------------------------------------------- 1 | .fullScreen { 2 | min-height: 100vh; 3 | padding: 50px; 4 | } 5 | 6 | .sectionHeader { 7 | font-size: 2rem; 8 | text-align: center; 9 | text-transform: uppercase; 10 | color: white; 11 | font-family: 'Raleway'; 12 | letter-spacing: 1px; 13 | padding: 40px; 14 | max-width: 60%; 15 | margin-left: auto; 16 | margin-right: auto; 17 | } 18 | 19 | .flex { 20 | display: flex; 21 | flex-wrap: wrap; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | .noBoxes { 27 | margin-top: 50px; 28 | font-family: 'Open Sans'; 29 | font-size: 1.4rem; 30 | color: white; 31 | } 32 | -------------------------------------------------------------------------------- /app/src/components/AmazingInput/AmazingInput.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { Button } from 'react-foundation'; 3 | import cssModules from 'react-css-modules'; 4 | import styles from './AmazingInput.module.scss'; 5 | 6 | class AmazingInput extends Component { 7 | constructor() { 8 | super(); 9 | this.handleSubmit = this.handleSubmit.bind(this); 10 | this.handleInput = this.handleInput.bind(this); 11 | this.state = { 12 | value: '' 13 | }; 14 | } 15 | handleInput(e) { 16 | this.setState({ 17 | value: e.target.value 18 | }); 19 | } 20 | handleSubmit() { 21 | const { 22 | onSubmit 23 | } = this.props; 24 | const { 25 | value 26 | } = this.state; 27 | onSubmit(value); 28 | } 29 | render() { 30 | const { 31 | value 32 | } = this.state; 33 | return ( 34 |
35 |
36 | 37 | 38 | 45 | 46 |
47 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | AmazingInput.propTypes = { 60 | onSubmit: PropTypes.func.isRequired 61 | }; 62 | 63 | export default cssModules(AmazingInput, styles); 64 | -------------------------------------------------------------------------------- /app/src/components/AmazingInput/AmazingInput.module.scss: -------------------------------------------------------------------------------- 1 | .formGroup { 2 | display: flex; 3 | margin: 20px; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | .button { 9 | margin-left: 40px !important; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as actionCreators from '../actions/actionCreators'; 4 | import Main from './Main'; 5 | 6 | // Map the global state to global props here. 7 | // See: https://egghead.io/lessons/javascript-redux-generating-containers-with-connect-from-react-redux-visibletodolist 8 | const mapStateToProps = (state) => ({ 9 | messages: state.messages, 10 | errors: state.errors 11 | }); 12 | 13 | // Map the dispatch and bind the action creators. 14 | // See: http://redux.js.org/docs/api/bindActionCreators.html 15 | function mapDispatchToProps(dispatch) { 16 | return bindActionCreators( 17 | actionCreators, 18 | dispatch 19 | ); 20 | } 21 | 22 | // Use connect both here and in your components. 23 | // See: https://egghead.io/lessons/javascript-redux-generating-containers-with-connect-from-react-redux-visibletodolist 24 | const App = connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(Main); 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /app/src/components/HacksmithsLogo/HacksmithsLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hacksmiths from './hacksmiths.png'; 3 | import styles from './HacksmithsLogo.module.scss'; 4 | import cssModules from 'react-css-modules'; 5 | 6 | const HacksmithsLogo = () => ( 7 |
8 | Hacksmiths Logo 13 |
14 | ); 15 | 16 | export default cssModules(HacksmithsLogo, styles); 17 | -------------------------------------------------------------------------------- /app/src/components/HacksmithsLogo/HacksmithsLogo.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | padding-top: 30px; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/components/HacksmithsLogo/hacksmiths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanCCollins/react-redux-simple-starter/67fcb738b2edb5ef88d7ac577c1cd266f0e852d3/app/src/components/HacksmithsLogo/hacksmiths.png -------------------------------------------------------------------------------- /app/src/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Main = (props) => ( 4 |
5 | {React.cloneElement(props.children, props)} 6 |
7 | ); 8 | 9 | export default Main; 10 | -------------------------------------------------------------------------------- /app/src/components/index.js: -------------------------------------------------------------------------------- 1 | // Import / export your componenents here. 2 | // Allows you to make modules that are easily imported as components 3 | // Like so: 4 | // import * as Components from '../components'; 5 | // import { MyComponent } from '../components'; 6 | 7 | import AmazingComponent from './AmazingComponent/AmazingComponent'; 8 | import AmazingBox from './AmazingBox/AmazingBox'; 9 | import AmazingInput from './AmazingInput/AmazingInput'; 10 | import HacksmithsLogo from './HacksmithsLogo/HacksmithsLogo'; 11 | 12 | export { 13 | AmazingComponent, 14 | AmazingBox, 15 | AmazingInput, 16 | HacksmithsLogo 17 | }; 18 | -------------------------------------------------------------------------------- /app/src/constants/amazingComponent.js: -------------------------------------------------------------------------------- 1 | export const ADD_BOX = 'ADD_BOX'; 2 | export const REMOVE_BOX = 'REMOVE_BOX'; 3 | -------------------------------------------------------------------------------- /app/src/containers/MyAmazingContainer/MyAmazingContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { AmazingComponent } from 'components'; 3 | import styles from './MyAmazingContainer.module.scss'; 4 | import cssModules from 'react-css-modules'; 5 | import * as AmazingActionCreators from '../../actions/amazingComponent'; 6 | import { connect } from 'react-redux'; 7 | import { bindActionCreators } from 'redux'; 8 | import { HacksmithsLogo } from 'components'; 9 | 10 | // Containers are used for managing state. 11 | // Whenever possible, write components as stateless functional 12 | // components and use classes for stateful containers. 13 | // There is also likely too much state here. The form 14 | // Input elements can be managed by redux form, for example. 15 | class MyAmazingContainer extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.handleAddItem = this.handleAddItem.bind(this); 19 | this.handleRemoveItem = this.handleRemoveItem.bind(this); 20 | } 21 | handleAddItem(content) { 22 | const { 23 | actions 24 | } = this.props; 25 | actions.addBox(content); 26 | } 27 | handleRemoveItem(id) { 28 | const { 29 | actions 30 | } = this.props; 31 | actions.removeBox(id); 32 | } 33 | render() { 34 | const { 35 | boxes 36 | } = this.props; 37 | return ( 38 |
39 | 40 |

React Redux Simple Starter

41 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | MyAmazingContainer.propTypes = { 52 | boxes: PropTypes.array.isRequired, 53 | actions: PropTypes.object.isRequired 54 | }; 55 | 56 | // Standard React-Redux Magic to connect your state to local props 57 | // https://github.com/reactjs/react-redux/blob/master/docs/api.md 58 | const mapStateToProps = (state) => ({ 59 | boxes: state.amazingComponent.boxes 60 | }); 61 | 62 | // More connected component magic 63 | // https://egghead.io/lessons/javascript-redux-generating-containers-with-connect-from-react-redux-visibletodolist 64 | const mapDispatchToProps = (dispatch) => ({ 65 | actions: bindActionCreators(AmazingActionCreators, dispatch) 66 | }); 67 | 68 | // CSS Module magic to connect the hashed selectors 69 | // https://github.com/gajus/react-css-modules 70 | const StyledComponent = cssModules(MyAmazingContainer, styles); 71 | 72 | export default connect( 73 | mapStateToProps, 74 | mapDispatchToProps 75 | )(StyledComponent); 76 | -------------------------------------------------------------------------------- /app/src/containers/MyAmazingContainer/MyAmazingContainer.module.scss: -------------------------------------------------------------------------------- 1 | .myAmazingContainer { 2 | background-image: linear-gradient(135deg, #ca2091 0, #a041fa 20%, #3f65fb 50%, #0fcbfa 80%, #0fe6fa 100%); 3 | min-height: 100vh; 4 | overflow: auto; 5 | } 6 | 7 | .bigTitle { 8 | text-align: center; 9 | margin-top: 20px; 10 | padding: 40px; 11 | text-transform: uppercase; 12 | color: white; 13 | max-width: 60%; 14 | margin-left: auto; 15 | margin-right: auto; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/containers/index.js: -------------------------------------------------------------------------------- 1 | import MyAmazingContainer from './MyAmazingContainer/MyAmazingContainer'; 2 | 3 | export { 4 | MyAmazingContainer 5 | }; 6 | -------------------------------------------------------------------------------- /app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import router from './utils/router'; 4 | import '../styles/styles.scss'; 5 | 6 | render(router, document.getElementById('app')); 7 | -------------------------------------------------------------------------------- /app/src/pages/LandingPage/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cssModules from 'react-css-modules'; 3 | import styles from './LandingPage.module.scss'; 4 | // Example to import a component using ES6 destructuring. 5 | import { MyAmazingContainer } from '../../containers'; 6 | 7 | // Pages map directly to Routes, i.e. one page equals on Route 8 | // Handler that maps to a route in /utils/routes 9 | const LandingPage = (props) => ( 10 |
11 | 14 |
15 | ); 16 | 17 | export default cssModules(LandingPage, styles); 18 | -------------------------------------------------------------------------------- /app/src/pages/LandingPage/LandingPage.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | width: 100% 4 | }; 5 | 6 | .header { 7 | font-size: 32px; 8 | font: 'Open Sans'; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/pages/NotFoundPage/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import styles from './NotFoundPage.module.scss'; 4 | 5 | const NotFound = () => ( 6 |
7 |

Not Found

8 |
9 | ); 10 | 11 | export default CSSModules(NotFound, styles); 12 | -------------------------------------------------------------------------------- /app/src/pages/NotFoundPage/NotFoundPage.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100vh; 3 | width: 100% 4 | }; 5 | 6 | .header { 7 | font-size: 32px; 8 | font: 'Open Sans'; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import NotFoundPage from './NotFoundPage/NotFoundPage'; 2 | import LandingPage from './LandingPage/LandingPage' 3 | 4 | export { 5 | NotFoundPage, 6 | LandingPage 7 | }; 8 | -------------------------------------------------------------------------------- /app/src/reducers/amazingComponent.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/amazingComponent'; 2 | 3 | const amazingComponent = (state = { 4 | boxes: [] 5 | }, action) => { 6 | switch (action.type) { 7 | case types.ADD_BOX: 8 | return Object.assign({}, state, { 9 | boxes: [ 10 | ...state.boxes, 11 | action.content 12 | ] 13 | }); 14 | case types.REMOVE_BOX: 15 | return Object.assign({}, state, { 16 | boxes: [ 17 | ...state.boxes.slice(0, action.index), 18 | ...state.boxes.slice(action.index + 1) 19 | ] 20 | }); 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export default amazingComponent; 27 | -------------------------------------------------------------------------------- /app/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | import { reducer as toastrReducer } from 'react-redux-toastr'; 5 | 6 | // Import the various reducers here: 7 | import amazingComponent from './amazingComponent'; 8 | 9 | const rootReducer = combineReducers({ 10 | // Apply all of the reducers here. 11 | amazingComponent, 12 | routing: routerReducer, 13 | form: formReducer, 14 | toastr: toastrReducer 15 | }); 16 | 17 | export default rootReducer; 18 | -------------------------------------------------------------------------------- /app/src/store/initialState.js: -------------------------------------------------------------------------------- 1 | const initalState = { 2 | amazingComponent: { 3 | boxes: ['Amazing', 'Super Amazing', 'Absolutely Amazing', 'Stunningly Amazing', 'Amazingly Amazing'] 4 | } 5 | }; 6 | 7 | export default initalState; 8 | -------------------------------------------------------------------------------- /app/src/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { syncHistoryWithStore } from 'react-router-redux'; 3 | import thunk from 'redux-thunk'; 4 | import { browserHistory } from 'react-router'; 5 | import createLogger from 'redux-logger'; 6 | import promiseMiddleware from 'redux-promise-middleware'; 7 | import rootReducer from '../reducers/index'; 8 | import initialState from './initialState'; 9 | 10 | /* Commonly used middlewares and enhancers */ 11 | /* See: http://redux.js.org/docs/advanced/Middleware.html*/ 12 | const loggerMiddleware = createLogger(); 13 | const middlewares = [thunk, promiseMiddleware(), loggerMiddleware]; 14 | 15 | /* Everyone should use redux dev tools */ 16 | /* https://github.com/gaearon/redux-devtools */ 17 | /* https://medium.com/@meagle/understanding-87566abcfb7a */ 18 | const enhancers = []; 19 | const devToolsExtension = window.devToolsExtension; 20 | if (typeof devToolsExtension === 'function') { 21 | enhancers.push(devToolsExtension()); 22 | } 23 | 24 | const composedEnhancers = compose( 25 | applyMiddleware(...middlewares), 26 | ...enhancers 27 | ); 28 | 29 | /* Hopefully by now you understand what a store is and how redux uses them, 30 | * But if not, take a look at: https://github.com/reactjs/redux/blob/master/docs/api/createStore.md 31 | * And https://egghead.io/lessons/javascript-redux-implementing-store-from-scratch 32 | */ 33 | const store = createStore( 34 | rootReducer, 35 | initialState, 36 | composedEnhancers, 37 | ); 38 | 39 | /* See: https://github.com/reactjs/react-router-redux/issues/305 */ 40 | export const history = syncHistoryWithStore(browserHistory, store); 41 | 42 | /* Hot reloading of reducers. How futuristic!! */ 43 | if (module.hot) { 44 | module.hot.accept('../reducers/', () => { 45 | const nextRootReducer = require('../reducers/index').default; 46 | store.replaceReducer(nextRootReducer); 47 | }); 48 | } 49 | 50 | export default store; 51 | -------------------------------------------------------------------------------- /app/src/utils/network.js: -------------------------------------------------------------------------------- 1 | // Brilliant network wrapper courteousy of 2 | // https://github.com/pburtchaell/redux-promise-middleware 3 | // Works to create requests to a network resource and wraps the requests in 4 | // A promise. This works great with redux promise middlewares 5 | // See the actions for post to see it in action located in ../actions/post.js 6 | import request from './request'; 7 | 8 | /** 9 | * @function Network 10 | * @description Factory function to create a object that can send 11 | * requests to a specific resource on the server. 12 | * @param {string} resource The resource used for config 13 | */ 14 | const Network = resource => { 15 | const buildUrl = ({ id, resource } = {}) => { 16 | const parameters = [ 17 | 'http://localhost:3000', 18 | 'api' 19 | ]; 20 | 21 | if (resource) parameters.concat([resource]); 22 | if (id) parameters.concat([id]); 23 | return parameters.join('/'); 24 | }; 25 | 26 | // Default option for every request 27 | const defaultOptions = { 28 | mode: 'cors', 29 | headers: { 30 | 'Accept': 'application/json', 31 | 'Content-Type': 'application/json' 32 | } 33 | }; 34 | 35 | return { 36 | 37 | /** 38 | * @function post 39 | * @description Make a POST request. 40 | * @param {string} path 41 | * @param {object} body 42 | * @param {object} options 43 | * @returns {promise} 44 | */ 45 | post: (path, body, options = {}) => { 46 | return request(buildUrl(path), Object.assign( 47 | options, 48 | defaultOptions, 49 | { 50 | method: 'POST', 51 | body: JSON.stringify(body) 52 | } 53 | )); 54 | }, 55 | 56 | /** 57 | * @function get 58 | * @description Make a GET request. 59 | * @param {string} path 60 | * @param {object} options 61 | * @returns {promise} 62 | */ 63 | get: (path, options = {}) => { 64 | return request(buildUrl(path), Object.assign( 65 | options, 66 | defaultOptions, 67 | { method: 'GET' } 68 | )); 69 | }, 70 | 71 | /** 72 | * @function edit 73 | * @description Make a PUT request. 74 | * @param {string} path 75 | * @param {object} body 76 | * @param {object} options 77 | * @returns {promise} 78 | */ 79 | update: (path, body, options = {}) => { 80 | return request(buildUrl(path), Object.assign( 81 | options, 82 | defaultOptions, 83 | { method: 'PUT' } 84 | )); 85 | }, 86 | 87 | /** 88 | * @function delete 89 | * @description Make a DELETE request. 90 | * @param {string} path 91 | * @param {object} options 92 | * @returns {promise} 93 | */ 94 | delete: (path, options = {}) => { 95 | return request(buildUrl(path), Object.assign( 96 | options, 97 | defaultOptions, 98 | { method: 'DELETE' } 99 | )); 100 | }, 101 | 102 | ping: () => request(buildUrl(), { method: 'GET' }) 103 | }; 104 | }; 105 | 106 | export default Network; 107 | -------------------------------------------------------------------------------- /app/src/utils/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | * @function request 4 | * @description Make a request to the server and return a promise. 5 | * @param {string} url 6 | * @param {object} options 7 | * @returns {promise} 8 | */ 9 | export default function request(url, options) { 10 | return new Promise((resolve, reject) => { 11 | if (!url) reject(new Error('URL parameter required')); 12 | if (!options) reject(new Error('Options parameter required')); 13 | 14 | fetch(url, options) 15 | .then(response => response.json()) 16 | .then(response => { 17 | if (response.errors) reject(response.errors); 18 | else resolve(response); 19 | }) 20 | .catch(reject); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/utils/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, IndexRoute } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import store, { history } from '../store/store'; 5 | import App from '../components/App'; 6 | import * as Pages from '../pages/'; 7 | import ReduxToastr from 'react-redux-toastr'; 8 | 9 | const router = ( 10 | 11 |
12 | 17 | 18 | 19 | 20 | {/* Examples of routes here. 21 | 22 | 23 | 24 | */} 25 | 26 | 27 | 28 |
29 |
30 | ); 31 | 32 | export default router; 33 | -------------------------------------------------------------------------------- /app/styles/settings.scss: -------------------------------------------------------------------------------- 1 | // Foundation for Sites Settings 2 | // Note, this is totally optional. I like using foundation, but if you don't just ignore it. 3 | // ----------------------------- 4 | // 5 | // Table of Contents: 6 | // 7 | // 1. Global 8 | // 2. Breakpoints 9 | // 3. The Grid 10 | // 4. Base Typography 11 | // 5. Typography Helpers 12 | // 6. Abide 13 | // 7. Accordion 14 | // 8. Accordion Menu 15 | // 9. Badge 16 | // 10. Breadcrumbs 17 | // 11. Button 18 | // 12. Button Group 19 | // 13. Callout 20 | // 14. Close Button 21 | // 15. Drilldown 22 | // 16. Dropdown 23 | // 17. Dropdown Menu 24 | // 18. Flex Video 25 | // 19. Forms 26 | // 20. Label 27 | // 21. Media Object 28 | // 22. Menu 29 | // 23. Meter 30 | // 24. Off-canvas 31 | // 25. Orbit 32 | // 26. Pagination 33 | // 27. Progress Bar 34 | // 28. Reveal 35 | // 29. Slider 36 | // 30. Switch 37 | // 31. Table 38 | // 32. Tabs 39 | // 33. Thumbnail 40 | // 34. Title Bar 41 | // 35. Tooltip 42 | // 36. Top Bar 43 | 44 | @import '../../node_modules/foundation-sites/scss/util/util'; 45 | 46 | // 1. Global 47 | // --------- 48 | 49 | $global-font-size: 100%; 50 | $global-width: rem-calc(1200); 51 | $global-lineheight: 1.5; 52 | $foundation-palette: ( 53 | primary: #2199e8, 54 | secondary: #777, 55 | success: #3adb76, 56 | warning: #ffae00, 57 | alert: #ec5840, 58 | ); 59 | $light-gray: #e6e6e6; 60 | $medium-gray: #cacaca; 61 | $dark-gray: #8a8a8a; 62 | $black: #0a0a0a; 63 | $white: #fefefe; 64 | $body-background: $white; 65 | $body-font-color: $black; 66 | $body-font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif; 67 | $body-antialiased: true; 68 | $global-margin: 1rem; 69 | $global-padding: 1rem; 70 | $global-weight-normal: normal; 71 | $global-weight-bold: bold; 72 | $global-radius: 0; 73 | $global-text-direction: ltr; 74 | $global-flexbox: false; 75 | $print-transparent-backgrounds: true; 76 | 77 | @include add-foundation-colors; 78 | 79 | // 2. Breakpoints 80 | // -------------- 81 | 82 | $breakpoints: ( 83 | small: 0, 84 | medium: 640px, 85 | large: 1024px, 86 | xlarge: 1200px, 87 | xxlarge: 1440px, 88 | ); 89 | $breakpoint-classes: (small medium large); 90 | 91 | // 3. The Grid 92 | // ----------- 93 | 94 | $grid-row-width: $global-width; 95 | $grid-column-count: 12; 96 | $grid-column-gutter: ( 97 | small: 20px, 98 | medium: 30px, 99 | ); 100 | $grid-column-align-edge: true; 101 | $block-grid-max: 8; 102 | 103 | // 4. Base Typography 104 | // ------------------ 105 | 106 | $header-font-family: $body-font-family; 107 | $header-font-weight: $global-weight-normal; 108 | $header-font-style: normal; 109 | $font-family-monospace: Consolas, 'Liberation Mono', Courier, monospace; 110 | $header-sizes: ( 111 | small: ( 112 | 'h1': 24, 113 | 'h2': 20, 114 | 'h3': 19, 115 | 'h4': 18, 116 | 'h5': 17, 117 | 'h6': 16, 118 | ), 119 | medium: ( 120 | 'h1': 48, 121 | 'h2': 40, 122 | 'h3': 31, 123 | 'h4': 25, 124 | 'h5': 20, 125 | 'h6': 16, 126 | ), 127 | ); 128 | $header-color: inherit; 129 | $header-lineheight: 1.4; 130 | $header-margin-bottom: 0.5rem; 131 | $header-text-rendering: optimizeLegibility; 132 | $small-font-size: 80%; 133 | $header-small-font-color: $medium-gray; 134 | $paragraph-lineheight: 1.6; 135 | $paragraph-margin-bottom: 1rem; 136 | $paragraph-text-rendering: optimizeLegibility; 137 | $code-color: $black; 138 | $code-font-family: $font-family-monospace; 139 | $code-font-weight: $global-weight-normal; 140 | $code-background: $light-gray; 141 | $code-border: 1px solid $medium-gray; 142 | $code-padding: rem-calc(2 5 1); 143 | $anchor-color: $primary-color; 144 | $anchor-color-hover: scale-color($anchor-color, $lightness: -14%); 145 | $anchor-text-decoration: none; 146 | $anchor-text-decoration-hover: none; 147 | $hr-width: $global-width; 148 | $hr-border: 1px solid $medium-gray; 149 | $hr-margin: rem-calc(20) auto; 150 | $list-lineheight: $paragraph-lineheight; 151 | $list-margin-bottom: $paragraph-margin-bottom; 152 | $list-style-type: disc; 153 | $list-style-position: outside; 154 | $list-side-margin: 1.25rem; 155 | $list-nested-side-margin: 1.25rem; 156 | $defnlist-margin-bottom: 1rem; 157 | $defnlist-term-weight: $global-weight-bold; 158 | $defnlist-term-margin-bottom: 0.3rem; 159 | $blockquote-color: $dark-gray; 160 | $blockquote-padding: rem-calc(9 20 0 19); 161 | $blockquote-border: 1px solid $medium-gray; 162 | $cite-font-size: rem-calc(13); 163 | $cite-color: $dark-gray; 164 | $keystroke-font: $font-family-monospace; 165 | $keystroke-color: $black; 166 | $keystroke-background: $light-gray; 167 | $keystroke-padding: rem-calc(2 4 0); 168 | $keystroke-radius: $global-radius; 169 | $abbr-underline: 1px dotted $black; 170 | 171 | // 5. Typography Helpers 172 | // --------------------- 173 | 174 | $lead-font-size: $global-font-size * 1.25; 175 | $lead-lineheight: 1.6; 176 | $subheader-lineheight: 1.4; 177 | $subheader-color: $dark-gray; 178 | $subheader-font-weight: $global-weight-normal; 179 | $subheader-margin-top: 0.2rem; 180 | $subheader-margin-bottom: 0.5rem; 181 | $stat-font-size: 2.5rem; 182 | 183 | // 6. Abide 184 | // -------- 185 | 186 | $abide-inputs: true; 187 | $abide-labels: true; 188 | $input-background-invalid: map-get($foundation-palette, alert); 189 | $form-label-color-invalid: map-get($foundation-palette, alert); 190 | $input-error-color: map-get($foundation-palette, alert); 191 | $input-error-font-size: rem-calc(12); 192 | $input-error-font-weight: $global-weight-bold; 193 | 194 | // 7. Accordion 195 | // ------------ 196 | 197 | $accordion-background: $white; 198 | $accordion-plusminus: true; 199 | $accordion-item-color: foreground($accordion-background, $primary-color); 200 | $accordion-item-background-hover: $light-gray; 201 | $accordion-item-padding: 1.25rem 1rem; 202 | $accordion-content-background: $white; 203 | $accordion-content-border: 1px solid $light-gray; 204 | $accordion-content-color: foreground($accordion-content-background, $body-font-color); 205 | $accordion-content-padding: 1rem; 206 | 207 | // 8. Accordion Menu 208 | // ----------------- 209 | 210 | $accordionmenu-arrows: true; 211 | $accordionmenu-arrow-color: $primary-color; 212 | 213 | // 9. Badge 214 | // -------- 215 | 216 | $badge-background: $primary-color; 217 | $badge-color: foreground($badge-background); 218 | $badge-padding: 0.3em; 219 | $badge-minwidth: 2.1em; 220 | $badge-font-size: 0.6rem; 221 | 222 | // 10. Breadcrumbs 223 | // --------------- 224 | 225 | $breadcrumbs-margin: 0 0 $global-margin 0; 226 | $breadcrumbs-item-font-size: rem-calc(11); 227 | $breadcrumbs-item-color: $primary-color; 228 | $breadcrumbs-item-color-current: $black; 229 | $breadcrumbs-item-color-disabled: $medium-gray; 230 | $breadcrumbs-item-margin: 0.75rem; 231 | $breadcrumbs-item-uppercase: true; 232 | $breadcrumbs-item-slash: true; 233 | 234 | // 11. Button 235 | // ---------- 236 | 237 | $button-padding: 0.85em 1em; 238 | $button-margin: 0 0 $global-margin 0; 239 | $button-fill: solid; 240 | $button-background: $primary-color; 241 | $button-background-hover: scale-color($button-background, $lightness: -15%); 242 | $button-color: $white; 243 | $button-color-alt: $black; 244 | $button-radius: $global-radius; 245 | $button-sizes: ( 246 | tiny: 0.6rem, 247 | small: 0.75rem, 248 | default: 0.9rem, 249 | large: 1.25rem, 250 | ); 251 | $button-opacity-disabled: 0.25; 252 | 253 | // 12. Button Group 254 | // ---------------- 255 | 256 | $buttongroup-margin: 1rem; 257 | $buttongroup-spacing: 1px; 258 | $buttongroup-child-selector: '.button'; 259 | $buttongroup-expand-max: 6; 260 | 261 | // 13. Callout 262 | // ----------- 263 | 264 | $callout-background: $white; 265 | $callout-background-fade: 85%; 266 | $callout-border: 1px solid rgba($black, 0.25); 267 | $callout-margin: 0 0 1rem 0; 268 | $callout-padding: 1rem; 269 | $callout-font-color: $body-font-color; 270 | $callout-font-color-alt: $body-background; 271 | $callout-radius: $global-radius; 272 | $callout-link-tint: 30%; 273 | 274 | // 14. Close Button 275 | // ---------------- 276 | 277 | $closebutton-position: right top; 278 | $closebutton-offset-horizontal: 1rem; 279 | $closebutton-offset-vertical: 0.5rem; 280 | $closebutton-size: 2em; 281 | $closebutton-lineheight: 1; 282 | $closebutton-color: $dark-gray; 283 | $closebutton-color-hover: $black; 284 | 285 | // 15. Drilldown 286 | // ------------- 287 | 288 | $drilldown-transition: transform 0.15s linear; 289 | $drilldown-arrows: true; 290 | $drilldown-arrow-color: $primary-color; 291 | $drilldown-background: $white; 292 | 293 | // 16. Dropdown 294 | // ------------ 295 | 296 | $dropdown-padding: 1rem; 297 | $dropdown-border: 1px solid $medium-gray; 298 | $dropdown-font-size: 1rem; 299 | $dropdown-width: 300px; 300 | $dropdown-radius: $global-radius; 301 | $dropdown-sizes: ( 302 | tiny: 100px, 303 | small: 200px, 304 | large: 400px, 305 | ); 306 | 307 | // 17. Dropdown Menu 308 | // ----------------- 309 | 310 | $dropdownmenu-arrows: true; 311 | $dropdownmenu-arrow-color: $anchor-color; 312 | $dropdownmenu-min-width: 200px; 313 | $dropdownmenu-background: $white; 314 | $dropdownmenu-border: 1px solid $medium-gray; 315 | 316 | // 18. Flex Video 317 | // -------------- 318 | 319 | $flexvideo-margin-bottom: rem-calc(16); 320 | $flexvideo-ratio: 4 by 3; 321 | $flexvideo-ratio-widescreen: 16 by 9; 322 | 323 | // 19. Forms 324 | // --------- 325 | 326 | $fieldset-border: 1px solid $medium-gray; 327 | $fieldset-padding: rem-calc(20); 328 | $fieldset-margin: rem-calc(18 0); 329 | $legend-padding: rem-calc(0 3); 330 | $form-spacing: rem-calc(16); 331 | $helptext-color: $black; 332 | $helptext-font-size: rem-calc(13); 333 | $helptext-font-style: italic; 334 | $input-prefix-color: $black; 335 | $input-prefix-background: $light-gray; 336 | $input-prefix-border: 1px solid $medium-gray; 337 | $input-prefix-padding: 1rem; 338 | $form-label-color: $black; 339 | $form-label-font-size: rem-calc(14); 340 | $form-label-font-weight: $global-weight-normal; 341 | $form-label-line-height: 1.8; 342 | $select-background: $white; 343 | $select-triangle-color: $dark-gray; 344 | $select-radius: $global-radius; 345 | $input-color: $black; 346 | $input-placeholder-color: $medium-gray; 347 | $input-font-family: inherit; 348 | $input-font-size: rem-calc(16); 349 | $input-background: $white; 350 | $input-background-focus: $white; 351 | $input-background-disabled: $light-gray; 352 | $input-border: 1px solid $medium-gray; 353 | $input-border-focus: 1px solid $dark-gray; 354 | $input-shadow: inset 0 1px 2px rgba($black, 0.1); 355 | $input-shadow-focus: 0 0 5px $medium-gray; 356 | $input-cursor-disabled: not-allowed; 357 | $input-transition: box-shadow 0.5s, border-color 0.25s ease-in-out; 358 | $input-number-spinners: true; 359 | $input-radius: $global-radius; 360 | 361 | // 20. Label 362 | // --------- 363 | 364 | $label-background: $primary-color; 365 | $label-color: foreground($label-background); 366 | $label-font-size: 0.8rem; 367 | $label-padding: 0.33333rem 0.5rem; 368 | $label-radius: $global-radius; 369 | 370 | // 21. Media Object 371 | // ---------------- 372 | 373 | $mediaobject-margin-bottom: $global-margin; 374 | $mediaobject-section-padding: $global-padding; 375 | $mediaobject-image-width-stacked: 100%; 376 | 377 | // 22. Menu 378 | // -------- 379 | 380 | $menu-margin: 0; 381 | $menu-margin-nested: 1rem; 382 | $menu-item-padding: 0.7rem 1rem; 383 | $menu-item-color-active: $white; 384 | $menu-item-background-active: map-get($foundation-palette, primary); 385 | $menu-icon-spacing: 0.25rem; 386 | 387 | // 23. Meter 388 | // --------- 389 | 390 | $meter-height: 1rem; 391 | $meter-radius: $global-radius; 392 | $meter-background: $medium-gray; 393 | $meter-fill-good: $success-color; 394 | $meter-fill-medium: $warning-color; 395 | $meter-fill-bad: $alert-color; 396 | 397 | // 24. Off-canvas 398 | // -------------- 399 | 400 | $offcanvas-size: 250px; 401 | $offcanvas-background: $light-gray; 402 | $offcanvas-zindex: -1; 403 | $offcanvas-transition-length: 0.5s; 404 | $offcanvas-transition-timing: ease; 405 | $offcanvas-fixed-reveal: true; 406 | $offcanvas-exit-background: rgba($white, 0.25); 407 | $maincontent-class: 'off-canvas-content'; 408 | $maincontent-shadow: 0 0 10px rgba($black, 0.5); 409 | 410 | // 25. Orbit 411 | // --------- 412 | 413 | $orbit-bullet-background: $medium-gray; 414 | $orbit-bullet-background-active: $dark-gray; 415 | $orbit-bullet-diameter: 1.2rem; 416 | $orbit-bullet-margin: 0.1rem; 417 | $orbit-bullet-margin-top: 0.8rem; 418 | $orbit-bullet-margin-bottom: 0.8rem; 419 | $orbit-caption-background: rgba($black, 0.5); 420 | $orbit-caption-padding: 1rem; 421 | $orbit-control-background-hover: rgba($black, 0.5); 422 | $orbit-control-padding: 1rem; 423 | $orbit-control-zindex: 10; 424 | 425 | // 26. Pagination 426 | // -------------- 427 | 428 | $pagination-font-size: rem-calc(14); 429 | $pagination-margin-bottom: $global-margin; 430 | $pagination-item-color: $black; 431 | $pagination-item-padding: rem-calc(3 10); 432 | $pagination-item-spacing: rem-calc(1); 433 | $pagination-radius: $global-radius; 434 | $pagination-item-background-hover: $light-gray; 435 | $pagination-item-background-current: $primary-color; 436 | $pagination-item-color-current: foreground($pagination-item-background-current); 437 | $pagination-item-color-disabled: $medium-gray; 438 | $pagination-ellipsis-color: $black; 439 | $pagination-mobile-items: false; 440 | $pagination-arrows: true; 441 | 442 | // 27. Progress Bar 443 | // ---------------- 444 | 445 | $progress-height: 1rem; 446 | $progress-background: $medium-gray; 447 | $progress-margin-bottom: $global-margin; 448 | $progress-meter-background: $primary-color; 449 | $progress-radius: $global-radius; 450 | 451 | // 28. Reveal 452 | // ---------- 453 | 454 | $reveal-background: $white; 455 | $reveal-width: 600px; 456 | $reveal-max-width: $global-width; 457 | $reveal-padding: $global-padding; 458 | $reveal-border: 1px solid $medium-gray; 459 | $reveal-radius: $global-radius; 460 | $reveal-zindex: 1005; 461 | $reveal-overlay-background: rgba($black, 0.45); 462 | 463 | // 29. Slider 464 | // ---------- 465 | 466 | $slider-width-vertical: 0.5rem; 467 | $slider-transition: all 0.2s ease-in-out; 468 | $slider-height: 0.5rem; 469 | $slider-background: $light-gray; 470 | $slider-fill-background: $medium-gray; 471 | $slider-handle-height: 1.4rem; 472 | $slider-handle-width: 1.4rem; 473 | $slider-handle-background: $primary-color; 474 | $slider-opacity-disabled: 0.25; 475 | $slider-radius: $global-radius; 476 | 477 | // 30. Switch 478 | // ---------- 479 | 480 | $switch-background: $medium-gray; 481 | $switch-background-active: $primary-color; 482 | $switch-height: 2rem; 483 | $switch-height-tiny: 1.5rem; 484 | $switch-height-small: 1.75rem; 485 | $switch-height-large: 2.5rem; 486 | $switch-radius: $global-radius; 487 | $switch-margin: $global-margin; 488 | $switch-paddle-background: $white; 489 | $switch-paddle-offset: 0.25rem; 490 | $switch-paddle-radius: $global-radius; 491 | $switch-paddle-transition: all 0.25s ease-out; 492 | 493 | // 31. Table 494 | // --------- 495 | 496 | $table-background: $white; 497 | $table-color-scale: 5%; 498 | $table-border: 1px solid smart-scale($table-background, $table-color-scale); 499 | $table-padding: rem-calc(8 10 10); 500 | $table-hover-scale: 2%; 501 | $table-row-hover: darken($table-background, $table-hover-scale); 502 | $table-row-stripe-hover: darken($table-background, $table-color-scale + $table-hover-scale); 503 | $table-striped-background: smart-scale($table-background, $table-color-scale); 504 | $table-stripe: even; 505 | $table-head-background: smart-scale($table-background, $table-color-scale / 2); 506 | $table-foot-background: smart-scale($table-background, $table-color-scale); 507 | $table-head-font-color: $body-font-color; 508 | $table-foot-font-color: $body-font-color; 509 | $show-header-for-stacked: false; 510 | 511 | // 32. Tabs 512 | // -------- 513 | 514 | $tab-margin: 0; 515 | $tab-background: $white; 516 | $tab-background-active: $light-gray; 517 | $tab-item-font-size: rem-calc(12); 518 | $tab-item-background-hover: $white; 519 | $tab-item-padding: 1.25rem 1.5rem; 520 | $tab-expand-max: 6; 521 | $tab-content-background: $white; 522 | $tab-content-border: $light-gray; 523 | $tab-content-color: foreground($tab-background, $primary-color); 524 | $tab-content-padding: 1rem; 525 | 526 | // 33. Thumbnail 527 | // ------------- 528 | 529 | $thumbnail-border: solid 4px $white; 530 | $thumbnail-margin-bottom: $global-margin; 531 | $thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); 532 | $thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); 533 | $thumbnail-transition: box-shadow 200ms ease-out; 534 | $thumbnail-radius: $global-radius; 535 | 536 | // 34. Title Bar 537 | // ------------- 538 | 539 | $titlebar-background: $black; 540 | $titlebar-color: $white; 541 | $titlebar-padding: 0.5rem; 542 | $titlebar-text-font-weight: bold; 543 | $titlebar-icon-color: $white; 544 | $titlebar-icon-color-hover: $medium-gray; 545 | $titlebar-icon-spacing: 0.25rem; 546 | 547 | // 35. Tooltip 548 | // ----------- 549 | 550 | $has-tip-font-weight: $global-weight-bold; 551 | $has-tip-border-bottom: dotted 1px $dark-gray; 552 | $tooltip-background-color: $black; 553 | $tooltip-color: $white; 554 | $tooltip-padding: 0.75rem; 555 | $tooltip-font-size: $small-font-size; 556 | $tooltip-pip-width: 0.75rem; 557 | $tooltip-pip-height: $tooltip-pip-width * 0.866; 558 | $tooltip-radius: $global-radius; 559 | 560 | // 36. Top Bar 561 | // ----------- 562 | 563 | $topbar-padding: 0.5rem; 564 | $topbar-background: rgba(255, 255, 255, 0.9); 565 | $topbar-dropdown-bg: $topbar-background; 566 | $topbar-submenu-background: $topbar-background; 567 | $topbar-title-spacing: 1rem; 568 | $topbar-input-width: 200px; 569 | $topbar-unstack-breakpoint: medium; 570 | -------------------------------------------------------------------------------- /app/styles/styles.scss: -------------------------------------------------------------------------------- 1 | // Delete if you don't want to use foundation and don't need any starter styles. 2 | @import "../../node_modules/foundation-sites/scss/foundation"; 3 | @import "./settings.scss"; 4 | 5 | // Global Styles defined and foundation classes 6 | // Just delete this if you don't want to use it! 7 | // NOTE: Stylesheets will be loaded by Sass loader, unless the name includes a .module 8 | // postfix, i.e.: MyModule.module.scss. This is to allow you to use the best of both worlds 9 | // i.e. css / sass modules and global styles with imports 10 | @include foundation-flex-grid; 11 | @include foundation-flex-classes; 12 | @include foundation-everything; 13 | 14 | $primary-color: rgba(0, 216, 255, .9); 15 | $off-white: #E9EAEC; 16 | $light-grey: #a1a1a1; 17 | 18 | body { 19 | font-family: Lato,sans-serif; 20 | font-size: 16px; 21 | line-height: 25px; 22 | overflow-x: hidden; 23 | } 24 | 25 | .centered { 26 | text-align: center; 27 | } 28 | 29 | .dark-grey { 30 | color: #454545; 31 | } 32 | 33 | .text-medium { 34 | font-size: 1.4rem; 35 | line-height: 1.42857143; 36 | } 37 | 38 | .section-header { 39 | font-size: 2rem; 40 | text-align: center; 41 | text-transform: uppercase; 42 | color: #454545; 43 | font-family: 'Raleway'; 44 | letter-spacing: 1px; 45 | padding: 40px; 46 | padding-top: 100px; 47 | } 48 | 49 | .section-subheader { 50 | color: $light-grey; 51 | font-size: 18px; 52 | font-style: italic; 53 | text-align: center; 54 | } 55 | 56 | span.divider { 57 | background-color: $primary-color; 58 | width: 150px; 59 | height: 3px; 60 | display: block; 61 | margin: 35px 0; 62 | margin-right: auto; 63 | margin-left: auto; 64 | } 65 | 66 | .section__off-white { 67 | background: $off-white; 68 | } 69 | 70 | .gradient-green { 71 | background-image: linear-gradient(45deg, rgba(0, 216, 255, .5), rgba(0, 255, 127,.5)); 72 | } 73 | 74 | .gradient-purple { 75 | background-image: linear-gradient(135deg, #ca2091 0, #a041fa 20%, #3f65fb 50%, #0fcbfa 80%, #0fe6fa 100%); 76 | } 77 | 78 | .gradient-blue { 79 | background: linear-gradient(to bottom, #FFE1FF 0%, #C9DAF8 100%); 80 | } 81 | .uppercase { 82 | text-transform: uppercase; 83 | } 84 | 85 | .text-white { 86 | color: white; 87 | } 88 | 89 | .paragraph-text { 90 | font-family: 'Open Sans'; 91 | font-size: 1.3rem; 92 | padding: 10px; 93 | } 94 | 95 | .icon-medium { 96 | font-size: 2rem; 97 | } 98 | 99 | .icon-small { 100 | font-size: 1.5rem; 101 | } 102 | 103 | .paper__fit { 104 | position: relative; 105 | display: inline-block; 106 | vertical-align: top; 107 | background-color: #fff; 108 | box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.24); 109 | padding: 60px; 110 | width: 100%; 111 | min-height: 200px; 112 | } 113 | 114 | .huge-space { 115 | margin-bottom: 200px; 116 | } 117 | 118 | .tilted-background:before { 119 | content: ''; 120 | position: absolute; 121 | top: -9px; 122 | left: -44px; 123 | right: -44px; 124 | height: 44px; 125 | background: #f4f5be; 126 | transform: rotate(-2deg); 127 | } 128 | 129 | .paragraph-header-text { 130 | margin: 0; 131 | font-size: 29px; 132 | line-height: 1.35; 133 | color: #333c4e; 134 | font-weight: 300; 135 | } 136 | 137 | .description-text { 138 | font-family: 'Open Sans'; 139 | text-align: justify; 140 | h4 { 141 | margin: 0; 142 | font-size: 29px; 143 | line-height: 1.35; 144 | color: #333c4e; 145 | font-weight: 300; 146 | } 147 | 148 | p { 149 | margin: 0; 150 | font-size: 23px; 151 | color: #7e8890; 152 | font-weight: 300; 153 | } 154 | } 155 | 156 | hr.section-divider { 157 | margin-top: 52px; 158 | margin-bottom: 42px; 159 | display: block; 160 | border: 0; 161 | text-align: center; 162 | } 163 | 164 | hr.section-divider:before { 165 | font-family: 'Open Sans'; 166 | font-weight: 400; 167 | font-style: italic; 168 | font-size: 28px; 169 | letter-spacing: .6em; 170 | } 171 | 172 | hr.section-divider:before { 173 | content: '...'; 174 | display: inline-block; 175 | margin-left: .6em; 176 | color: rgba(0,0,0,.6); 177 | position: relative; 178 | top: -30px; 179 | } 180 | 181 | .container { 182 | margin-right: auto; 183 | margin-left: auto; 184 | padding-left: 15px; 185 | padding-right: 15px; 186 | @media (min-width: 992px) { 187 | width: 1000px; 188 | } 189 | } 190 | 191 | .form-field { 192 | display: block; 193 | box-shadow: none; 194 | position: relative; 195 | padding: 6px 24px; 196 | border: 1px solid rgba(0,0,0,.15); 197 | margin-bottom: -1px; 198 | background-color: #fff; 199 | color: #333; 200 | opacity: 1; 201 | min-width: 300px; 202 | max-width: 400px; 203 | } 204 | 205 | label.form-field { 206 | display: block; 207 | font-size: 14px; 208 | font-size: .875rem; 209 | line-height: 24px; 210 | color: #666; 211 | cursor: pointer; 212 | } 213 | 214 | span.form-field__contents { 215 | display: block; 216 | margin-left: -24px; 217 | margin-right: -24px; 218 | } 219 | 220 | .form-field input[type=text] { 221 | display: block; 222 | width: 100%; 223 | border: none; 224 | padding: 0 24px; 225 | border-radius: 0; 226 | font-size: 16px; 227 | font-size: 1rem; 228 | line-height: 1.5; 229 | } 230 | 231 | .fadeIn-appear { 232 | opacity: 0.01; 233 | } 234 | 235 | .fadeIn-appear.fadeIn-appear-active { 236 | opacity: 1; 237 | transition: opacity .5s ease-in; 238 | } 239 | 240 | .fadeIn-enter { 241 | opacity: 0.01; 242 | } 243 | 244 | .fadeIn-enter.fadeIn-enter-active { 245 | opacity: 1; 246 | transition: opacity 500ms ease-in; 247 | } 248 | 249 | .fadeIn-leave { 250 | opacity: 1; 251 | } 252 | 253 | .fadeIn-leave.fadeIn-leave-active { 254 | opacity: 0.01; 255 | transition: opacity 600ms ease-in; 256 | } 257 | -------------------------------------------------------------------------------- /app/tests/actions/amazingComponent.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import * as actions from '../../src/actions/amazingComponent'; 3 | import * as types from '../../src/constants/amazingComponent'; 4 | 5 | describe('actions', () => { 6 | it('should create an action to add a box to the boxes list.', () => { 7 | const content = 'Amazing content!!'; 8 | const expectedAction = { 9 | type: types.ADD_BOX, 10 | content 11 | }; 12 | expect( 13 | actions.addBox(content) 14 | ).toEqual(expectedAction); 15 | }); 16 | it('should create an action to remove a box from the specified index in the boxes list.', () => { 17 | const index = 1; 18 | const expectedAction = { 19 | type: types.REMOVE_BOX, 20 | index 21 | }; 22 | expect( 23 | actions.removeBox(index) 24 | ).toEqual(expectedAction); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/tests/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyanCCollins/react-redux-simple-starter/67fcb738b2edb5ef88d7ac577c1cd266f0e852d3/app/tests/components/.gitkeep -------------------------------------------------------------------------------- /app/tests/reducers/amazingComponent.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import * as types from '../../src/constants/amazingComponent'; 3 | import reducer from '../../src/reducers/amazingComponent'; 4 | 5 | describe('amazingComponent reducer', () => { 6 | it('should return an object with an empty array under the key boxes (initial state)', () => { 7 | expect( 8 | reducer(undefined, {}) 9 | ).toEqual({ 10 | boxes: [] 11 | }); 12 | }); 13 | 14 | it('should add an item to the box array', () => { 15 | expect( 16 | reducer( 17 | { 18 | boxes: [] 19 | }, 20 | { 21 | type: types.ADD_BOX, 22 | content: 'Super awesome content' 23 | } 24 | ) 25 | ).toEqual( 26 | { 27 | boxes: [ 28 | 'Super awesome content' 29 | ] 30 | } 31 | ); 32 | }); 33 | it('should remove the box with the specified index from the box array', () => { 34 | const stateBefore = { 35 | boxes: ['Amazing!', 'Awesome!', 'Super Cool!'] 36 | }; 37 | const stateAfter = { 38 | boxes: ['Amazing!', 'Super Cool!'] 39 | }; 40 | const index = 1; 41 | expect( 42 | reducer(stateBefore, { 43 | type: types.REMOVE_BOX, 44 | index 45 | }) 46 | ).toEqual(stateAfter); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /app/tests/test_helper.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | const doc = jsdom.jsdom(''); 4 | const win = doc.defaultView; 5 | 6 | global.document = doc; 7 | global.window = win; 8 | 9 | Object.keys(window).forEach((key) => { 10 | if (!(key in global)) { 11 | global[key] = window[key]; 12 | } 13 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Redux Simple Starter 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-simple-starter", 3 | "version": "1.0.0", 4 | "description": "A simple react redux starter project", 5 | "main": "index.js", 6 | "babel": { 7 | "presets": [ 8 | "es2015", 9 | "react", 10 | "stage-0" 11 | ] 12 | }, 13 | "scripts": { 14 | "test": "`npm bin`/mocha --compilers js:babel-core/register --require ./app/tests/test_helper.js '*/tests/**/*.@(js|jsx)'", 15 | "test:watch": "npm run test -- --watch", 16 | "build": "`npm bin`/webpack", 17 | "dev": "`npm bin`/webpack-dev-server", 18 | "lint": "`npm bin`/eslint . --ext .js --ext .jsx", 19 | "deploy": "NODE_ENV=production `npm bin`/webpack -p", 20 | "start": "npm run dev", 21 | "clean": "rm -rf app/dist app/build", 22 | "setup": "npm install" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/RyanCCollins/react-redux-simple-starter.git" 27 | }, 28 | "keywords": [ 29 | "boilerplate", 30 | "redux", 31 | "react", 32 | "webpack", 33 | "sass", 34 | "css modules" 35 | ], 36 | "engines": { 37 | "node": "4.2.4", 38 | "npm": "3.8.8" 39 | }, 40 | "author": "Ryan Collins", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/RyanCCollins/react-redux-simple-starter/issues" 44 | }, 45 | "homepage": "https://github.com/RyanCCollins/react-redux-simple-starter#readme", 46 | "dependencies": { 47 | "actions": "^1.3.0", 48 | "babel-core": "^6.3.15", 49 | "babel-loader": "^6.2.0", 50 | "babel-preset-es2015": "^6.3.13", 51 | "babel-preset-react": "^6.3.13", 52 | "babel-preset-stage-0": "^6.3.13", 53 | "components": "^0.1.0", 54 | "css-loader": "^0.23.0", 55 | "expect": "^1.20.2", 56 | "file-loader": "^0.9.0", 57 | "foundation-sites": "^6.2.3", 58 | "history": "^1.14.0", 59 | "html-webpack-plugin": "^2.7.1", 60 | "immutable": "^3.7.5", 61 | "isomorphic-fetch": "^2.2.0", 62 | "node-sass": "^3.4.2", 63 | "react": "^15.1.0", 64 | "react-addons-css-transition-group": "^15.2.1", 65 | "react-dom": "^15.0.1", 66 | "react-foundation": "^0.6.8", 67 | "react-redux": "^4.4.5", 68 | "react-redux-toastr": "^3.1.4", 69 | "react-router": "^2.3.0", 70 | "react-router-redux": "^4.0.4", 71 | "redux": "^3.5.2", 72 | "redux-form": "^5.2.5", 73 | "redux-logger": "^2.6.1", 74 | "redux-promise-middleware": "^3.2.0", 75 | "redux-thunk": "^1.0.0", 76 | "resolve-url-loader": "^1.4.4", 77 | "sass-loader": "^3.1.2", 78 | "style-loader": "^0.13.0", 79 | "svg-react-loader": "^0.3.6", 80 | "webpack": "^1.12.9" 81 | }, 82 | "devDependencies": { 83 | "babel-eslint": "^5.0.0-beta4", 84 | "babel-preset-es2015": "^6.9.0", 85 | "chai": "^3.4.1", 86 | "chai-immutable": "^1.5.3", 87 | "eslint": "^1.10.3", 88 | "eslint-config-airbnb": "^4.0.0", 89 | "eslint-loader": "^1.1.1", 90 | "eslint-plugin-react": "^3.11.3", 91 | "jsdom": "^7.2.0", 92 | "mocha": "^2.3.4", 93 | "npm-install-webpack-plugin": "^4.0.3", 94 | "react-css-modules": "^3.7.6", 95 | "react-hot-loader": "^1.3.0", 96 | "redux-devtools": "^3.0.1", 97 | "webpack-dev-server": "^1.14.0" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlwebpackPlugin = require('html-webpack-plugin'); 4 | const NpmInstallPlugin = require('npm-install-webpack-plugin'); 5 | const ROOT_PATH = path.resolve(__dirname); 6 | 7 | const env = process.env.NODE_ENV || 'development'; 8 | const PORT = process.env.PORT || 8080; 9 | const HOST = '0.0.0.0'; // Set to localhost if need be. 10 | const URL = `http://${HOST}:${PORT}` 11 | 12 | 13 | module.exports = { 14 | devtool: process.env.NODE_ENV === 'production' ? '' : 'source-map', 15 | entry: [ 16 | path.resolve(ROOT_PATH,'app/src/index') 17 | ], 18 | module: { 19 | preLoaders: [ 20 | { 21 | test: /\.jsx?$/, 22 | loaders: process.env.NODE_ENV === 'production' ? [] : ['eslint'], 23 | include: path.resolve(ROOT_PATH, './app') 24 | } 25 | ], 26 | loaders: [{ 27 | test: /\.jsx?$/, 28 | exclude: /node_modules/, 29 | loaders: ['react-hot', 'babel'] 30 | }, 31 | { 32 | test: /\.svg$/, 33 | loader: 'babel!svg-react' 34 | }, 35 | { 36 | test: /\.json$/, 37 | loader: 'json' 38 | }, 39 | { 40 | test: /\.module\.scss$/, 41 | loaders: [ 42 | 'style', 43 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', 44 | 'resolve-url', 45 | 'sass' 46 | ] 47 | }, 48 | { 49 | test: /\.scss$/, 50 | exclude: /\.module\.scss$/, 51 | loaders: ["style", "css", "sass"] 52 | }, 53 | { 54 | test: /\.css$/, 55 | loader: "style-loader!css-loader" 56 | }, 57 | { 58 | test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, 59 | loader: "url-loader?mimetype=application/font-woff" 60 | }, 61 | { 62 | test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 63 | loader: "file-loader?name=[name].[ext]" 64 | }, 65 | { 66 | test: /\.(jpg|png)$/, 67 | loader: 'file?name=[path][name].[hash].[ext]' 68 | } 69 | ] 70 | }, 71 | resolve: { 72 | extensions: ['', '.js', '.jsx'], 73 | alias: { 74 | actions: path.resolve(ROOT_PATH, 'app/src/actions'), 75 | utils: path.resolve(ROOT_PATH, 'app/src/utils'), 76 | reducers: path.resolve(ROOT_PATH, 'app/src/reducers'), 77 | store: path.resolve(ROOT_PATH, 'app/src/stores'), 78 | components: path.resolve(ROOT_PATH, 'app/src/components'), 79 | containers: path.resolve(ROOT_PATH, 'app/src/containers'), 80 | constants: path.resolve(ROOT_PATH, 'app/src/constants'), 81 | pages: path.resolve(ROOT_PATH, 'app/src/pages') 82 | }, 83 | }, 84 | output: { 85 | path: process.env.NODE_ENV === 'production' ? path.resolve(ROOT_PATH, 'app/dist') : path.resolve(ROOT_PATH, 'app/build'), 86 | publicPath: '/', 87 | filename: 'bundle.js', 88 | }, 89 | devServer: { 90 | contentBase: path.resolve(ROOT_PATH, 'app/build'), 91 | historyApiFallback: true, 92 | hot: true, 93 | inline: true, 94 | progress: true, 95 | // Constants defined above take care of logic 96 | // For setting host and port 97 | host: HOST, 98 | port: PORT 99 | }, 100 | plugins: [ 101 | new webpack.HotModuleReplacementPlugin(), 102 | new NpmInstallPlugin(), 103 | new HtmlwebpackPlugin({ 104 | title: 'React Redux Simple Starter', 105 | template: 'index.html' 106 | }) 107 | ] 108 | }; 109 | --------------------------------------------------------------------------------