├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── screenshots └── run_through.gif ├── src ├── actions │ └── authentication.js ├── assets │ ├── favicon.ico │ └── images │ │ └── cc.png ├── components │ ├── Pages │ │ └── NotFound404.jsx │ ├── auth │ │ ├── LoginForm.jsx │ │ └── LoginModal.jsx │ ├── nav │ │ ├── TopNav.jsx │ │ └── UserMenu.jsx │ └── shared │ │ ├── Heading.jsx │ │ └── Icon.jsx ├── config │ └── routes.js ├── constants │ └── authentication.js ├── containers │ ├── Main.jsx │ ├── Page1.jsx │ └── Page2.jsx ├── index.html ├── index.jsx ├── reducers │ ├── authentication.js │ └── index.js ├── store │ └── index.js ├── styles │ ├── Auth.scss │ ├── Main.scss │ └── colors.scss └── utils │ ├── fakeApi.js │ ├── storage.js │ └── sum.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "rules": { 11 | "semi": 1 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-webpack-scss-quickstart 2 | A minimal web application with [React](https://facebook.github.io/react/), [Router](https://github.com/reactjs/react-router), [Async Redux](http://redux.js.org/docs/advanced/AsyncActions.html), and [Webpack](https://webpack.github.io/). ES6/ES2015 support using [Babel](https://babeljs.io/). SASS/SCSS styling with [PostCss](https://github.com/postcss/postcss) and [Bootstrap](http://getbootstrap.com/). 3 | 4 | ## Demo 5 | [Click here](http://react-webpack-scss-quickstart.s3-website-us-west-2.amazonaws.com/) 6 | 7 | Log in with any username/password combination. The app will remember you between sessions. 8 | 9 | ![Image](https://github.com/jogleasonjr/react-webpack-scss-quickstart/blob/master/screenshots/run_through.gif) 10 |
Logging in simulates an async authorization request. Use a library such as [Fetch](https://github.com/matthew-andrews/isomorphic-fetch) to implement your own authorization endpoint [here](https://github.com/jogleasonjr/react-webpack-scss-quickstart/blob/master/src/actions/authentication.js#L39).
11 | 12 | ## To Build and Run via CLI 13 | 14 | Dependencies: 15 | 16 | * [Python 2.7](https://www.python.org/downloads/). I haven't tested Python 3. 17 | * [Visual Studio 2015](https://www.visualstudio.com/downloads/). I used the Enterprise Edition, the Community Edition (free) will probably work. 18 | 19 | ```bash 20 | # Clone this repository 21 | git clone https://github.com/jogleasonjr/react-webpack-scss-quickstart 22 | # Go into the repository 23 | cd react-webpack-scss-quickstart 24 | # Install dependencies and run the app with Hot Reloading 25 | npm install && npm run start 26 | ``` 27 | Now navigate to [http://localhost:8182/webpack-dev-server/](http://localhost:8182/webpack-dev-server/) in your browser. 28 | 29 | ## Next Steps 30 | * Learn about managing Redux state [here](https://github.com/reactjs/redux) 31 | * Learn more about React and JSX components [here](https://facebook.github.io/react/docs/getting-started.html). 32 | 33 | ## License 34 | 35 | [MIT](https://tldrlegal.com/license/mit-license) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-webpack-scss-quickstart", 3 | "version": "0.0.1", 4 | "description": "A minimal, cross-platform web application with React and Webpack. ES6/ES2015 support using Babel. SCSS support using PostCss.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jogleasonjr/react-webpack-scss-quickstart" 9 | }, 10 | "scripts": { 11 | "start": "./node_modules/.bin/webpack-dev-server --port 8182", 12 | "build": "./node_modules/.bin/webpack -p", 13 | "deploy": "webpack && cp dist/* \\\\20.20.20.31\\c$\\tfx", 14 | "test": "jest" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "autoprefixer": "^6.3.3", 20 | "babel-core": "^6.5.2", 21 | "babel-eslint": "^6.0.0-beta.6", 22 | "babel-loader": "^6.2.2", 23 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 24 | "babel-polyfill": "^6.7.4", 25 | "babel-preset-es2015": "^6.5.0", 26 | "babel-preset-react": "^6.5.0", 27 | "css-loader": "^0.23.1", 28 | "eslint": "^2.4.0", 29 | "eslint-loader": "^1.2.1", 30 | "file-loader": "^0.8.5", 31 | "font-awesome": "^4.5.0", 32 | "html-webpack-plugin": "2.10.0", 33 | "node-sass": "3.4.2", 34 | "postcss-import": "^8.0.2", 35 | "postcss-loader": "0.8.2", 36 | "postcss-url": "^5.1.1", 37 | "precss": "^1.4.0", 38 | "redux-devtools": "^3.1.1", 39 | "sass-loader": "^3.2.0", 40 | "style-loader": "^0.13.0", 41 | "webpack": "^1.12.13", 42 | "webpack-dev-server": "^1.14.1" 43 | }, 44 | "dependencies": { 45 | "bootstrap": "^3.3.6", 46 | "es6-promise": "^3.1.2", 47 | "isomorphic-fetch": "^2.2.1", 48 | "react": "^15.0.1", 49 | "react-bootstrap": "^0.29.2", 50 | "react-dom": "^15.0.1", 51 | "react-redux": "^4.4.1", 52 | "react-router": "^2.0.1", 53 | "react-router-bootstrap": "^0.20.1", 54 | "redux": "^3.3.1", 55 | "redux-form": "^4.2.2", 56 | "redux-thunk": "^2.0.1", 57 | "url-loader": "^0.5.7" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /screenshots/run_through.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogleasonjr/react-webpack-scss-quickstart/4883137c71a55813c297423fe510cf27dc8a14c7/screenshots/run_through.gif -------------------------------------------------------------------------------- /src/actions/authentication.js: -------------------------------------------------------------------------------- 1 | // for actions, see: http://redux.js.org/docs/basics/Actions.html 2 | // for some conventions, see: https://github.com/acdlite/flux-standard-action 3 | 4 | import AuthConstants from '../constants/authentication'; 5 | import {Promise} from 'es6-promise'; 6 | import Storage from '../utils/storage'; 7 | import api from '../utils/fakeApi.js'; 8 | 9 | const PROFILE_STORAGE_KEY = 'PROFILE_STORAGE_KEY'; 10 | 11 | export const loginPrompt = () => { 12 | var profile = Storage.getJSON(PROFILE_STORAGE_KEY); 13 | if (profile) { 14 | return loginSuccess(profile); 15 | } 16 | else { 17 | return { 18 | type: AuthConstants.LOG_IN_PROMPT, 19 | payload: {} 20 | }; 21 | } 22 | }; 23 | 24 | export const loginCancel = () => ({ 25 | type: AuthConstants.LOG_IN_CANCEL, 26 | payload: {} 27 | }); 28 | 29 | export const loginRequest = () => ({ 30 | type: AuthConstants.LOG_IN_REQUEST 31 | }); 32 | 33 | export const loginSuccess = (user) => ({ 34 | type: AuthConstants.LOG_IN_SUCCESS, 35 | payload: { 36 | user 37 | } 38 | }); 39 | 40 | export const loginError = (status) => ({ 41 | type: AuthConstants.LOG_IN_ERROR, 42 | payload: new Error(status), 43 | error: true 44 | }); 45 | 46 | export const logout = () => { 47 | localStorage.removeItem(PROFILE_STORAGE_KEY); 48 | 49 | return { 50 | type: AuthConstants.LOG_OUT 51 | }; 52 | }; 53 | 54 | export const login = (formData) => { 55 | 56 | const {username, password} = formData; 57 | return (dispatch) => { 58 | dispatch(loginRequest()); 59 | 60 | api.getToken(username, password) 61 | .then(authResponse => { 62 | var token = authResponse.access_token; 63 | return api.getUserProfile(token); 64 | }) 65 | .then(profile => { 66 | Storage.setJSON(PROFILE_STORAGE_KEY, profile); 67 | dispatch(loginSuccess(profile)); 68 | }) 69 | .catch(errorStatusCode => { 70 | switch (errorStatusCode) { 71 | case 400: 72 | dispatch(loginError("Username or password is incorrect.")); 73 | break; 74 | default: 75 | dispatch(loginError("Unknown error: " + errorStatusCode)); 76 | break; 77 | } 78 | }); 79 | }; 80 | }; 81 | 82 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogleasonjr/react-webpack-scss-quickstart/4883137c71a55813c297423fe510cf27dc8a14c7/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/cc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jogleasonjr/react-webpack-scss-quickstart/4883137c71a55813c297423fe510cf27dc8a14c7/src/assets/images/cc.png -------------------------------------------------------------------------------- /src/components/Pages/NotFound404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Heading from './../shared/Heading'; 3 | //import '../../styles/main.scss'; 4 | 5 | export default class Main extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return (
); 12 | } 13 | } -------------------------------------------------------------------------------- /src/components/auth/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {reduxForm} from 'redux-form'; 3 | import {Input} from 'react-bootstrap'; 4 | 5 | const LoginForm = (props) => { 6 | 7 | const {fields: {username, password}, onSubmit, isLoggingIn} = props; 8 | 9 | return ( 10 |
11 |
12 | {username.touched && username.error &&
{username.error}
} 13 | 16 |
17 | 18 |
19 | ); 20 | }; 21 | 22 | const validate = (values) => { 23 | const errors = {}; 24 | 25 | if (!values.username) { 26 | errors.username = 'Required'; 27 | } else if (values.username.length < 3) { 28 | errors.username = 'Must be at least 3 characters'; 29 | } 30 | 31 | return errors; 32 | }; 33 | 34 | export default reduxForm({ 35 | form: 'login', 36 | fields: ['username', 'password'], 37 | validate 38 | }, 39 | state => ({ 40 | initialValues: { 41 | username: state.global.storedUserName, 42 | password: state.global.storedPassword 43 | } 44 | }) 45 | )(LoginForm); -------------------------------------------------------------------------------- /src/components/auth/LoginModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Modal, Button} from 'react-bootstrap'; 3 | import Icon from '../shared/Icon'; 4 | import LoginForm from './LoginForm'; 5 | 6 | 7 | export default class LoginModal extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleKeyPress = this.handleKeyPress.bind(this); 11 | this.handleSubmit = this.handleSubmit.bind(this); 12 | this.cancelClicked = this.cancelClicked.bind(this); 13 | } 14 | 15 | handleKeyPress(e) { 16 | if (e.key === 'Enter') { 17 | this.handleSubmit(); 18 | } 19 | } 20 | 21 | handleSubmit() { 22 | this.refs.loginForm.submit(); 23 | } 24 | 25 | cancelClicked(e) { 26 | e.preventDefault(); 27 | this.props.loginCancel(); 28 | }; 29 | 30 | render() { 31 | 32 | const {loginRequired, login, isLoggingIn, applicationName, environmentName, loginError} = this.props; 33 | if (loginError) console.log(loginError); 34 | const loginText = isLoggingIn ? "Logging in..." : "Login"; 35 | return ( 36 |
37 | 38 | 39 | 41 | Log in to {applicationName} {environmentName} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | { loginError ?
50 | {loginError.toString()} 51 |
: null } 52 | { isLoggingIn ? : null } 53 | 54 | 56 |
57 |
58 |
59 | ); 60 | } 61 | } -------------------------------------------------------------------------------- /src/components/nav/TopNav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {LinkContainer, IndexLinkContainer} from 'react-router-bootstrap'; 3 | import {Nav, Navbar, NavItem} from 'react-bootstrap'; 4 | import Icon from './../shared/Icon'; 5 | import UserMenu from './UserMenu'; 6 | 7 | export default (props) => ( 8 | 9 | 10 | 11 | {props.applicationName} 12 | 13 | 14 | 15 | 16 | 20 | 23 | 24 | 25 | ); -------------------------------------------------------------------------------- /src/components/nav/UserMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {NavItem, NavDropdown, MenuItem} from 'react-bootstrap'; 3 | import Icon from '../shared/Icon'; 4 | 5 | export default ({user, loginPrompt, logout, isLoggingIn, loginError, environmentName}) => { 6 | 7 | const logoutClicked = (e) => { 8 | e.preventDefault(); 9 | logout(); 10 | }; 11 | 12 | const loginClicked = (e) => { 13 | e.preventDefault(); 14 | loginPrompt(); 15 | }; 16 | 17 | if (user) { 18 | return ( 19 | {user.name}} id="basic-nav-dropdown"> 20 | Action 21 | Another action 22 | Something else here 23 | 24 | 25 | Logout 26 | 27 | 28 | ); 29 | } 30 | else { 31 | const navIcon = isLoggingIn ? 'spinner fa-spin' : loginError? 'exclamation-triangle' : 'sign-in'; 32 | const navText = isLoggingIn ? 'Logging in' : loginError? 'Error logging in!' :'Login'; 33 | return ( 34 | 35 | {navText} {environmentName} 36 | 37 | ); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/shared/Heading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (props) => ( 4 |

{props.text}

5 | ); 6 | -------------------------------------------------------------------------------- /src/components/shared/Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (props) => ( 4 | 5 | 6 | 7 | ); -------------------------------------------------------------------------------- /src/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route, IndexRoute } from 'react-router'; 3 | import Main from '../containers/Main'; 4 | import Page1 from '../containers/Page1'; 5 | import Page2 from '../containers/Page2'; 6 | import NotFound404 from '../components/pages/NotFound404'; 7 | 8 | export default ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); -------------------------------------------------------------------------------- /src/constants/authentication.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'fbjs/lib/keyMirror'; 2 | 3 | // use keyMirror here so we can cleanly refactor action names 4 | // throughout the whole app. 5 | const actionTypes = keyMirror({ 6 | LOG_IN_PROMPT: null, 7 | LOG_IN_CANCEL: null, 8 | LOG_IN_REQUEST: null, 9 | LOG_IN_SUCCESS: null, 10 | LOG_IN_ERROR: null, 11 | LOG_OUT: null 12 | }); 13 | 14 | export default { 15 | ...actionTypes 16 | }; -------------------------------------------------------------------------------- /src/containers/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import TopNav from './../components/nav/TopNav'; 4 | import {login, logout, loginPrompt, loginCancel} from '../actions/authentication'; 5 | import LoginModal from '../components/auth/LoginModal'; 6 | 7 | class Main extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentDidMount() { 13 | this.props.loginPrompt(); 14 | } 15 | 16 | 17 | render() { 18 | return ( 19 |
20 | 21 | 22 | 23 |
24 |
25 | {this.props.children} 26 |
27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | const mapDispatchToProps = (dispatch) => { 34 | return { 35 | loginCancel: () => { 36 | dispatch(loginCancel()); 37 | }, 38 | 39 | loginPrompt: () => { 40 | dispatch(loginPrompt()); 41 | }, 42 | 43 | login: (username, password) => { 44 | dispatch(login(username, password)); 45 | }, 46 | 47 | logout: () => { 48 | dispatch(logout()); 49 | } 50 | }; 51 | }; 52 | 53 | const mapStateToProps = (state) => { 54 | return { 55 | applicationName: state.global.applicationName, 56 | environmentName: state.global.environmentName, 57 | 58 | //everything from auth 59 | ...state.authentication 60 | }; 61 | }; 62 | 63 | export default connect(mapStateToProps, mapDispatchToProps)(Main); 64 | -------------------------------------------------------------------------------- /src/containers/Page1.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import Heading from '../components/shared/Heading'; 4 | import Icon from './../components/shared/Icon'; 5 | 6 | class Page1 extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | const {user} = this.props; 13 | const username = user ? user.name : 'World'; 14 | 15 | return ( 16 |
17 | 18 |
); 19 | } 20 | } 21 | 22 | const mapStateToProps = (state) => { 23 | return { user: state.authentication.user}; 24 | }; 25 | 26 | export default connect(mapStateToProps)(Page1); 27 | -------------------------------------------------------------------------------- /src/containers/Page2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Heading from '../components/shared/Heading'; 3 | import Icon from './../components/shared/Icon'; 4 | import {connect} from 'react-redux'; 5 | 6 | class Page2 extends React.Component { 7 | 8 | render() { 9 | const {user} = this.props; 10 | const username = user ? user.name : 'cruel world'; 11 | 12 | return ( 13 |
14 | 15 |
); 16 | } 17 | } 18 | 19 | const mapStateToProps = (state) => { 20 | return {user: state.authentication.user}; 21 | }; 22 | 23 | export default connect(mapStateToProps)(Page2); 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World ❤ React & Webpack 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import {Router, hashHistory} from 'react-router'; 4 | import routes from './config/routes'; 5 | import {Provider} from 'react-redux'; 6 | import "!style!css!sass!./styles/main.scss"; 7 | 8 | import store from './store'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('react-root') 15 | ); 16 | -------------------------------------------------------------------------------- /src/reducers/authentication.js: -------------------------------------------------------------------------------- 1 | import AuthConstants from '../constants/authentication'; 2 | 3 | const initialState = { 4 | isLoggingIn: false, 5 | loginRequired: false 6 | }; 7 | 8 | const authentication = (state = initialState, action) => { 9 | switch (action.type) { 10 | 11 | case AuthConstants.LOG_IN_PROMPT: 12 | { 13 | return { 14 | ...state, 15 | loginRequired: true 16 | }; 17 | } 18 | 19 | case AuthConstants.LOG_IN_CANCEL: 20 | { 21 | return { 22 | ...state, 23 | loginRequired: false, 24 | loginError: false 25 | }; 26 | } 27 | 28 | 29 | case AuthConstants.LOG_IN_REQUEST: 30 | { 31 | return { 32 | ...state, 33 | isLoggingIn: true, 34 | loginError: false 35 | }; 36 | } 37 | 38 | case AuthConstants.LOG_IN_SUCCESS: 39 | { 40 | return { 41 | ...state, 42 | isLoggingIn: false, 43 | loginRequired: false, 44 | user: action.payload.user 45 | }; 46 | } 47 | 48 | case AuthConstants.LOG_IN_ERROR: 49 | { 50 | return { 51 | ...state, 52 | isLoggingIn: false, 53 | loginError: action.payload 54 | }; 55 | } 56 | 57 | case AuthConstants.LOG_OUT: 58 | { 59 | return { 60 | ...state, 61 | isLoggingIn: false, 62 | user: null 63 | }; 64 | } 65 | 66 | default: 67 | { 68 | return state; 69 | } 70 | } 71 | }; 72 | 73 | export default authentication; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | // combine reducers here 2 | import {combineReducers} from 'redux'; 3 | import {reducer as formReducer} from 'redux-form'; 4 | import authentication from './authentication'; 5 | 6 | export default combineReducers( 7 | { 8 | authentication, 9 | form: formReducer, 10 | 11 | // this is just for the initial state 12 | // in the store.js 13 | // see: http://stackoverflow.com/a/33678198/5906146 14 | global: (state = {}) => state 15 | } 16 | ); -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import reducers from '../reducers'; 3 | import thunkMiddleware from 'redux-thunk'; 4 | 5 | 6 | const initialState = { 7 | global: { 8 | applicationName: "Redux App Template", 9 | environmentName: "v1.0" 10 | } 11 | }; 12 | 13 | const reduxDevToolsExtension = window.devToolsExtension ? window.devToolsExtension() : f => f; 14 | 15 | const store = createStore( 16 | reducers, 17 | initialState, 18 | compose( 19 | applyMiddleware(thunkMiddleware), 20 | reduxDevToolsExtension 21 | ) 22 | ); 23 | 24 | export default store; -------------------------------------------------------------------------------- /src/styles/Auth.scss: -------------------------------------------------------------------------------- 1 | @import './colors'; 2 | 3 | .loginModal { 4 | 5 | //width: 400px; 6 | //left: 50%; 7 | //margin-top: 50px; 8 | //margin-left: -200px; 9 | 10 | 11 | .modal-content { 12 | //width: 350px; 13 | //text-align: center 14 | } 15 | 16 | .btn-primary { 17 | background-color: $accent; 18 | border-color: $accent-dark; 19 | } 20 | 21 | .icon .fa { 22 | margin-right: 0.5em; 23 | font-size: 1.5em; 24 | vertical-align: middle; 25 | } 26 | 27 | .loginError { 28 | color: red; 29 | } 30 | 31 | .input { 32 | } 33 | } -------------------------------------------------------------------------------- /src/styles/Main.scss: -------------------------------------------------------------------------------- 1 | @import './../../node_modules/font-awesome/css/font-awesome.min.css'; 2 | @import './../../node_modules/bootstrap/dist/css/bootstrap.min.css'; 3 | @import 'colors'; 4 | 5 | @import './Auth'; 6 | 7 | .fa { 8 | color: $accent; 9 | } 10 | 11 | body { 12 | background-color: $white; 13 | padding-top: 20px; 14 | padding-bottom: 20px; 15 | } 16 | 17 | .jumbotron { 18 | background-color: transparent; 19 | h1 { 20 | font-size: 1.8em; 21 | } 22 | p { 23 | font-size: 1.2em; 24 | } 25 | } -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | $accent: #d20c37; 2 | $accent-dark: #ab0f30; 3 | 4 | $white: #f5f5f5; 5 | $black: #121212; -------------------------------------------------------------------------------- /src/utils/fakeApi.js: -------------------------------------------------------------------------------- 1 | import {Promise} from 'es6-promise'; 2 | 3 | export default { 4 | 5 | status(response) { 6 | if (response.status >= 200 && response.status < 300) { 7 | return Promise.resolve(response); 8 | } else { 9 | return Promise.reject(response.status); 10 | } 11 | }, 12 | 13 | json(response) { 14 | return response.json(); 15 | }, 16 | 17 | // fake a token using the username 18 | getToken(username, password) { 19 | 20 | var p = new Promise((resolve) => 21 | 22 | setTimeout(() => { 23 | resolve({ 24 | access_token: username 25 | }); 26 | }, 500)); 27 | 28 | return p; 29 | }, 30 | 31 | getUserProfile(token) { 32 | var p = new Promise((resolve) => 33 | 34 | setTimeout(() => { 35 | resolve({ 36 | accessToken: token, 37 | name: token 38 | }); 39 | }, 500)); 40 | 41 | return p; 42 | } 43 | }; -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | get(key) { 4 | return localStorage[key]; 5 | }, 6 | 7 | set(key, value) { 8 | localStorage[key] = value; 9 | }, 10 | 11 | remove(key) { 12 | localStorage.removeItem(key); 13 | }, 14 | 15 | getJSON(key) { 16 | 17 | var value = this.get(key); 18 | try { 19 | return value != undefined ? JSON.parse(value) : null; 20 | } catch (e) { 21 | throw `unable to convert key "${key}" with value "${value}" to JSON`; 22 | } 23 | }, 24 | 25 | setJSON(key, json) { 26 | var savedValue = JSON.stringify(json); 27 | this.set(key, savedValue); 28 | } 29 | }; -------------------------------------------------------------------------------- /src/utils/sum.js: -------------------------------------------------------------------------------- 1 | // calm down. this class is just for testing the test runner :) 2 | export default (a, b) => a + b; 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const postcssImport = require('postcss-import'); 5 | const postcssUrl = require('postcss-url'); 6 | const autoprefixer = require('autoprefixer'); 7 | const precss = require('precss'); 8 | 9 | const PATHS = { 10 | src: path.join(__dirname, 'src'), 11 | dist: path.join(__dirname, 'dist') 12 | } 13 | 14 | 15 | module.exports = { 16 | 17 | // This enables the creation of source maps, 18 | // which improve the debuggability of the application 19 | // by allowing you to see where an error was raised. 20 | devtool: "source-map", 21 | 22 | entry: PATHS.src, 23 | 24 | // Location and filename pattern of the 25 | // final build output files. 26 | output: { 27 | path: PATHS.dist, 28 | filename: "bundle.js" 29 | }, 30 | 31 | devServer: { 32 | //content from here will be automatically served from here 33 | contentBase: "dist/", 34 | // and appears to come relative to this path 35 | publicPath: "/", 36 | 37 | historyApiFallback: true 38 | }, 39 | 40 | 41 | module: { 42 | 43 | // Performs linting on code for quality checks 44 | preLoaders: [ 45 | { 46 | test: /(\.js$|\.jsx$)/, 47 | include: PATHS.src, 48 | loader: "eslint" 49 | } 50 | ], 51 | 52 | // Performs transformations 53 | loaders: [ 54 | { 55 | // Post-css loader and its plugins. 56 | test: /\.scss$/, 57 | include: PATHS.src, 58 | loaders: [ 59 | 'style',// inserts raw css into styles elements. 60 | 'css', // css-loader parses css files resolves url() expressions. 61 | 'sass', // sass-loader for sass compilation 62 | 'postcss' // for whatever we have defined in postcss( ) below 63 | ] 64 | }, 65 | { 66 | // for jsx 67 | test: /\.jsx?$/, 68 | include: PATHS.src, 69 | loader: 'babel', 70 | query: { 71 | plugins: ['transform-object-rest-spread'], 72 | presets: ['react', 'es2015'] 73 | } 74 | }, 75 | 76 | // for font-awesome 77 | { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff" }, 78 | { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" } 79 | ] 80 | }, 81 | postcss: function(webpack) { 82 | return [autoprefixer, precss, postcssImport({ addDependencyTo: webpack }), postcssUrl({})]; 83 | }, 84 | eslint: { 85 | configFile: '.eslintrc' 86 | }, 87 | 88 | // Defines where we can load modules from, 89 | // and the extensions we care about. The '' 90 | // empty string allows the requiring of arbitrary 91 | // extensions, e.g. require('./somefile.ext'). 92 | // Specifiying extensions such as '.js' allows 93 | // requiring without extensions, 94 | // e.g. require('underscore') 95 | resolve: { 96 | modulesDirectories: ['node_modules'], 97 | extensions: ['', '.js', '.jsx'] 98 | }, 99 | 100 | plugins: [ 101 | new webpack.HotModuleReplacementPlugin(), 102 | new HtmlWebpackPlugin({ 103 | favicon: 'src/assets/favicon.ico', 104 | template: 'src/index.html', 105 | inject: true 106 | }) 107 | ] 108 | } --------------------------------------------------------------------------------