├── .eslintignore ├── app ├── vendors │ └── index.js ├── assets │ ├── images │ │ └── profile_picture.png │ ├── fonts │ │ └── glyphicons │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ ├── glyphicons-halflings-regular.woff2 │ │ │ └── glyphicons-halflings-regular.svg │ ├── static_content │ │ └── index.js │ ├── stylesheets │ │ ├── base.styl │ │ ├── normalize.styl │ │ └── glyphicons.styl │ └── index.template.html ├── auth │ ├── types.js │ ├── auth_token.js │ ├── actions.js │ ├── api.js │ └── reducer.js ├── combinedReducer.js ├── middleware │ ├── README.md │ └── async_actions_middleware.js ├── views │ ├── containers │ │ ├── home_container.jsx │ │ ├── secured_content_container.jsx │ │ └── application_container.jsx │ ├── login │ │ ├── styles.styl │ │ ├── index.jsx │ │ └── login.jsx │ └── routes │ │ └── index.jsx ├── utils │ ├── create_sync_actions_types.js │ ├── create_async_actions_types.js │ ├── env.js │ ├── matches_action.js │ ├── custom_promise.js │ ├── immutable_helpers.js │ └── api.js ├── store.js └── index.jsx ├── scripts └── pre-push ├── .gitignore ├── WEBPACK.md ├── Makefile ├── .eslintrc ├── README.md ├── package.json ├── webpack.config.prod.js └── webpack.config.dev.js /.eslintignore: -------------------------------------------------------------------------------- 1 | app/vendors 2 | -------------------------------------------------------------------------------- /app/vendors/index.js: -------------------------------------------------------------------------------- 1 | import "react"; 2 | import "react-router"; 3 | import "lodash"; 4 | -------------------------------------------------------------------------------- /scripts/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "* Linting the code" 4 | make lint && exit 0 5 | echo "Linting failed!" 6 | exit 1 7 | -------------------------------------------------------------------------------- /app/assets/images/profile_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/frontend-boilerplate/master/app/assets/images/profile_picture.png -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/frontend-boilerplate/master/app/assets/fonts/glyphicons/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/frontend-boilerplate/master/app/assets/fonts/glyphicons/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/frontend-boilerplate/master/app/assets/fonts/glyphicons/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/frontend-boilerplate/master/app/assets/fonts/glyphicons/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/auth/types.js: -------------------------------------------------------------------------------- 1 | import createAsyncActionsTypes from "app/utils/create_async_actions_types"; 2 | 3 | const AsyncTypes = createAsyncActionsTypes([ 4 | "AUTHENTICATE" 5 | ]); 6 | 7 | export default {...AsyncTypes}; 8 | -------------------------------------------------------------------------------- /app/combinedReducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from "redux"; 2 | 3 | import authReducer from "app/auth/reducer"; 4 | 5 | const rootReducer = combineReducers({ 6 | auth: authReducer, 7 | }); 8 | 9 | export default rootReducer; 10 | -------------------------------------------------------------------------------- /app/middleware/README.md: -------------------------------------------------------------------------------- 1 | # Redux Middleware 2 | 3 | This folder contains the middleware that we use for Redux, for more information on how 4 | to build custom middleware please refer to [docs](http://rackt.github.io/redux/docs/advanced/Middleware.html) 5 | -------------------------------------------------------------------------------- /app/views/containers/home_container.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | 3 | 4 | export default class HomeContainer extends Component { 5 | 6 | render () { 7 | return
Home!
; 8 | } 9 | } 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/static_content/index.js: -------------------------------------------------------------------------------- 1 | // Require MANUALLY the content that should make it to the bundle without 2 | // hashing its name, for things like favico or robots 3 | // require("file?name=[name].[ext]!./favicon.ico"); 4 | // require("file?name=[name].[ext]!./robots.txt"); -------------------------------------------------------------------------------- /app/views/login/styles.styl: -------------------------------------------------------------------------------- 1 | .loginContainer 2 | padding 10px 3 | width 400px 4 | margin auto 5 | margin-top 50px 6 | border 1px solid gray 7 | text-align left 8 | background-image url("app/assets/images/profile_picture.png") 9 | background-size cover 10 | -------------------------------------------------------------------------------- /app/auth/auth_token.js: -------------------------------------------------------------------------------- 1 | 2 | export function storeToken (token) { 3 | return window.localStorage.setItem("token", token); 4 | } 5 | 6 | export function getToken () { 7 | return window.localStorage.getItem("token"); 8 | } 9 | 10 | export function isTokenSet () { 11 | return window.localStorage.getItem("token") ? true : false; 12 | } 13 | -------------------------------------------------------------------------------- /app/auth/actions.js: -------------------------------------------------------------------------------- 1 | import Types from "./types"; 2 | 3 | import { 4 | fetchSession as fetchSessionCall, 5 | authenticate as authenticateCall 6 | } from "./api"; 7 | 8 | 9 | export function authenticate (email, password) { 10 | return { 11 | type: Types.AUTHENTICATE, 12 | callAPI: () => authenticateCall({email, password}) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/views/containers/secured_content_container.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {isTokenSet} from "app/auth/auth_token"; 3 | 4 | export default class SecuredContentContainer extends Component { 5 | 6 | componentWillMount() { 7 | if (!isTokenSet()) { 8 | this.props.history.pushState(null, "/login"); 9 | } 10 | } 11 | 12 | render () { 13 | return this.props.children; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/utils/create_sync_actions_types.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Simple helper for creating sync actions types. 4 | * It just retrieves an object wiht the type as key and the type as value. 5 | */ 6 | export default function createSyncActionsTypes (types) { 7 | if (!Array.isArray(types)) { 8 | throw new Error("Expecting types to be an array of constants"); 9 | } 10 | 11 | let augmentedTypes = {}; 12 | 13 | types.forEach( type => { 14 | augmentedTypes[type] = type; 15 | }); 16 | 17 | return augmentedTypes; 18 | } 19 | -------------------------------------------------------------------------------- /app/auth/api.js: -------------------------------------------------------------------------------- 1 | import Api from "app/utils/api"; 2 | 3 | export default { 4 | 5 | authenticate ({email, password}) { 6 | return Api.post({ 7 | path: "/authenticate", 8 | body: {email: email, password: password}, 9 | ignoreAuthFailure: true, 10 | parse: function(res) { 11 | if (res.body.errorMessage) { 12 | this.fail({errorMessage: res.body.errorMessage}); 13 | } 14 | if (res.body.token && res.body.user) { 15 | this.done(res.body); 16 | } 17 | } 18 | }); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/utils/create_async_actions_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple helper for creating async action types. 3 | * For each type provided an object with the request / done / fail 4 | * version is gonna be provided. 5 | */ 6 | export default function createAsyncActionsTypes (types) { 7 | if (!Array.isArray(types)) { 8 | throw new Error("Expecting types to be an array of constants"); 9 | } 10 | 11 | let augmentedTypes = {}; 12 | 13 | types.forEach( type => { 14 | augmentedTypes[type] = { 15 | request: `${type}_REQUEST`, 16 | done: `${type}_DONE`, 17 | fail: `${type}_FAIL` 18 | }; 19 | }); 20 | 21 | return augmentedTypes; 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | build 30 | 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /WEBPACK.md: -------------------------------------------------------------------------------- 1 | # Webpack 2 | 3 | We use [webpack](http://webpack.github.io/) as our main building tool. 4 | 5 | The following techinques are being used from webpack for enabling a better Development Experience (DX). 6 | 7 | ### Loaders for all dependencies in our project. 8 | 9 | Everything in the project gets required through webpack. This means 10 | that we use the 'require' (or ES6 import) statement for requiring ALL the dependencies we might have in the project (including fonts, images, css, etc). 11 | To be able to do this we configure webpack with different loaders that know how to handle the different file types. 12 | 13 | Refer to webpack config for more details and explanation. 14 | 15 | We have different webpack configurations to enable different builds (development, production, test). 16 | -------------------------------------------------------------------------------- /app/utils/env.js: -------------------------------------------------------------------------------- 1 | // Use singletons since this is only analyzed when the bundle is loaded, so 2 | // no harm, which also allow us to export a literal instead of needing a func. 3 | let isTesting, isDev, isProd; 4 | 5 | getEnvValues(); 6 | updateSingletonEnvValues(); 7 | 8 | 9 | function getEnvValues () { 10 | isTesting = process && process.env.NODE_ENV === "testing"; 11 | isDev = process && process.env.NODE_ENV === "development"; 12 | isProd = process && process.env.NODE_ENV === "production"; 13 | } 14 | 15 | function updateSingletonEnvValues () { 16 | module.exports.isTest = isTesting; 17 | module.exports.isTesting = isTesting; 18 | module.exports.isDev = isDev; 19 | module.exports.isDevelopment = isDev; 20 | module.exports.isProd = isProd; 21 | module.exports.isProduction = isProd; 22 | } 23 | -------------------------------------------------------------------------------- /app/views/containers/application_container.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | 4 | import {initializeSession} from "app/auth/actions"; 5 | import {isTokenSet} from "app/auth/auth_token"; 6 | 7 | const select = (state) => ({ 8 | isInitializingSession: state.auth.isInitializingSession, 9 | sessionValid: state.auth.sessionValid 10 | }); 11 | 12 | /** 13 | * Entry point for the whole App this includes secured and not secured content. 14 | * Application gets composed by redux therefore we can access to all the redux 15 | * sugar from here after. 16 | */ 17 | @connect(select) 18 | export default class ApplicationContainer extends React.Component { 19 | 20 | render () { 21 | return ( 22 |
{this.props.children}
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | node ./node_modules/webpack-dev-server/bin/webpack-dev-server --port 9898 --host 0.0.0.0 --config webpack.config.dev.js --hot --progress --inline 3 | 4 | run-prod: 5 | WEBPACK_DEV_SERVER=true node ./node_modules/webpack-dev-server/bin/webpack-dev-server --port 9898 --config webpack.config.prod.js --progress --inline 6 | 7 | install-githooks: 8 | rm -f .git/hooks/pre-push 9 | ln -s ../../scripts/pre-push ./.git/hooks/pre-push 10 | chmod +x .git/hooks/pre-push 11 | 12 | lint: 13 | ./node_modules/.bin/eslint ./app 14 | 15 | # Production build 16 | bundle-prod: 17 | # IMPORTANT --bail will ensure that the process exits with an error code 18 | # causing any other command consuming this to fail if there is an error bundling. 19 | node ./node_modules/webpack/bin/webpack --config webpack.config.prod.js -p --progress --bail 20 | 21 | -------------------------------------------------------------------------------- /app/utils/matches_action.js: -------------------------------------------------------------------------------- 1 | import {isString} from "lodash"; 2 | 3 | /** 4 | * Returns true if there is any match, either a sync action 5 | * or the action provided matches one of the sub types of an async action. 6 | * This means that matchesAction(Types.ASYNC_ACTION) will match 7 | * ASYNC_ACTION.request, ASYNC_ACTION.done, ASYNC_ACTION.fail. 8 | * 9 | */ 10 | export default function matchesAction(action, actionTest) { 11 | if (isString(actionTest)) { 12 | if (action.type === actionTest) { 13 | return true; 14 | } else { 15 | return false; 16 | } 17 | } 18 | 19 | if (action.type === actionTest.request) { 20 | return true; 21 | } 22 | 23 | if (action.type === actionTest.done) { 24 | return true; 25 | } 26 | 27 | if (action.type === actionTest.fail) { 28 | return true; 29 | } 30 | 31 | return false; 32 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/base.styl: -------------------------------------------------------------------------------- 1 | @import '~app/assets/stylesheets/normalize' 2 | @import '~app/assets/stylesheets/glyphicons' 3 | 4 | :global { 5 | 6 | html { 7 | box-sizing: border-box; 8 | // Changing to this value so the rems are easier to calculate 9 | // 1.4rem = 14px 10 | // 2.4rem = 24px 11 | font-size: 62.5%; 12 | position: relative; 13 | } 14 | 15 | *, *:before, *:after { 16 | box-sizing: inherit; 17 | } 18 | 19 | body { 20 | position: relative; 21 | font-family: 'ProximaNova', 'Helvetica Neue', Helvetica, Arial, sans-serif; 22 | font-size: 1.4rem; 23 | // background-color: #F0EFEF; 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | cursor: pointer; 29 | } 30 | 31 | a, a:visited, a:hover, a:active { 32 | color: inherit; 33 | } 34 | 35 | button { 36 | cursor: pointer; 37 | } 38 | 39 | * { 40 | box-sizing: border-box; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from "redux"; 2 | import createLogger from "redux-logger"; 3 | import combinedReducer from "./combinedReducer"; 4 | import asyncActionsMiddleware from "app/middleware/async_actions_middleware"; 5 | 6 | 7 | let createStoreWithMiddleware = applyMiddleware( 8 | asyncActionsMiddleware, 9 | createLogger({ 10 | predicate: (getState, action) => process.env.NODE_ENV !== "production" 11 | }) 12 | )(createStore); 13 | 14 | 15 | export default function configureStore(initialState) { 16 | const store = createStoreWithMiddleware(combinedReducer, initialState); 17 | 18 | if (module.hot) { 19 | // Enable Webpack hot module replacement for reducers 20 | module.hot.accept("./combinedReducer", () => { 21 | const nextCombinedReducer = require("./combinedReducer"); 22 | store.replaceReducer(nextCombinedReducer); 23 | }); 24 | } 25 | 26 | return store; 27 | } 28 | -------------------------------------------------------------------------------- /app/views/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Router, Route, Link, Redirect} from "react-router"; 3 | 4 | import ApplicationContainer from "app/views/containers/application_container"; 5 | import SecuredContentContainer from "app/views/containers/secured_content_container"; 6 | import LoginContainer from "app/views/login"; 7 | import HomeContainer from "app/views/containers/home_container"; 8 | 9 | 10 | export default function renderRoutes(store, history) { 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /app/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import ReactDOM from "react-dom"; 3 | import {Router, Route, Link} from "react-router"; 4 | import createHashHistory from "history/lib/createHashHistory"; 5 | import {Provider} from "react-redux"; 6 | 7 | import renderRoutes from "app/views/routes"; 8 | import configureStore from "app/store"; 9 | 10 | // Apply the base styles for ALL the app 11 | import "app/assets/stylesheets/base"; 12 | 13 | // Make sure the static_content gets added to the bundle 14 | import "app/assets/static_content"; 15 | 16 | const store = configureStore(); 17 | 18 | class Root extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | this.history = createHashHistory(); 23 | } 24 | 25 | render () { 26 | return ( 27 | 28 | {renderRoutes(store, this.history)} 29 | 30 | ) 31 | } 32 | } 33 | 34 | 35 | ReactDOM.render(, document.getElementById("reactApplication")) 36 | -------------------------------------------------------------------------------- /app/auth/reducer.js: -------------------------------------------------------------------------------- 1 | import Types from "./types"; 2 | import {storeToken} from "./auth_token"; 3 | import matchesAction from "app/utils/matches_action"; 4 | import * as ih from "app/utils/immutable_helpers"; 5 | 6 | const initialState = ih.immutable({ 7 | authenticating: false, 8 | authenticationError: null, 9 | user: null 10 | }); 11 | 12 | 13 | export default function reducer (state = initialState, action) { 14 | 15 | if (matchesAction(action, Types.AUTHENTICATE.request)) { 16 | state = ih.set(state, "authenticating", true); 17 | } 18 | 19 | if (matchesAction(action, Types.AUTHENTICATE.done)) { 20 | const token = action.apiResponse.token; 21 | storeToken(token); 22 | 23 | state = ih.set(state, "authenticating", false); 24 | state = ih.set(state, "user", action.apiResponse.user); 25 | } 26 | 27 | if (matchesAction(action, Types.AUTHENTICATE.fail)) { 28 | state = ih.set(state, "authenticationError", action.apiError); 29 | state = ih.set(state, "authenticating", false); 30 | } 31 | 32 | return state; 33 | } 34 | -------------------------------------------------------------------------------- /app/assets/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | React Scaffolding 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | parser: "babel-eslint", 3 | "ecmaFeatures": { 4 | "jsx": true, 5 | "modules": true 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "es6": true, 11 | "mocha": true 12 | }, 13 | "rules": { 14 | "consistent-return": 0, 15 | "strict": 0, 16 | "no-underscore-dangle": 0, 17 | "no-multi-spaces": 0, 18 | "quotes": [ 2 ], 19 | "new-cap": 0, 20 | "comma-spacing": 0, 21 | "no-use-before-define": 0, 22 | "camelcase": 0, 23 | "curly": 0, 24 | "no-trailing-spaces": 0, 25 | "semi-spacing": 0, 26 | "semi-spacing": 0, 27 | "no-unused-expressions": 0, 28 | "eol-last": 0, 29 | "dot-notation": [2, {"allowPattern": "^NODE_ENV$"}], 30 | "no-extend-native": 0, 31 | "comma-dangle": 0, 32 | "no-redeclare": 1, 33 | "no-shadow": 1 34 | }, 35 | "plugins": [ 36 | "react" 37 | ], 38 | "globals": { 39 | // testing globals 40 | "_": true, 41 | "sinon": true, 42 | "rewire" : true, 43 | "testHelpers": true, 44 | "expect" : true, 45 | "sinon" : true, 46 | "should" : true, 47 | "makeSrcPath": true, 48 | "assert" : true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/views/login/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import _ from "lodash"; 3 | import {connect} from "react-redux"; 4 | 5 | import Login from "app/views/login/login"; 6 | import {authenticate} from "app/auth/actions"; 7 | import {isTokenSet} from "app/auth/auth_token"; 8 | 9 | 10 | const select = (state) => ({ 11 | authenticationError: state.auth.authenticationError 12 | }); 13 | 14 | /** 15 | * This is the entry point for any page that requires a logged in user 16 | */ 17 | @connect(select) 18 | export default class LoginContainer extends Component { 19 | 20 | componentWillMount() { 21 | if (isTokenSet()) { 22 | this.props.history.pushState(null, "/home"); 23 | } 24 | } 25 | 26 | render () { 27 | return ( 28 | 31 | ); 32 | } 33 | 34 | _handleSubmit ({email, password}) { 35 | const {dispatch} = this.props; 36 | 37 | dispatch(authenticate(email, password)).then((result) => { 38 | if (result.apiError) return; 39 | 40 | this.props.history.pushState(null, "/home"); 41 | }); 42 | } 43 | 44 | } 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React scaffolding 2 | 3 | An opinionated scaffolding with the following technical stack and configuration: 4 | 5 | * React (0.14.x) 6 | * React Router (1.x) 7 | * Flux by using Redux (3.x) 8 | * Webpack 9 | * CSS Modules 10 | * Stylus 11 | * Seamless Immutable 12 | * Hot module replacement 13 | * Babel 14 | * Testing mocha + shallow rendering with React 15 | 16 | The idea is to provide a base structure that enable consumers to start building freatures and deliver a production ready package of a Single Page App. 17 | 18 | ## TO DO 19 | 20 | - [ ] Better examples of styling and way of shaing styles between css modules and js 21 | - [ ] Test examples for unit test with shallow rendering. 22 | 23 | ## Getting started 24 | 25 | To start hacking simply do: 26 | 27 | ``` 28 | $ make install-githooks 29 | $ npm install 30 | $ make run 31 | ``` 32 | Point browser to http://localhost:9898 33 | 34 | For more detailed reference continue reading. 35 | 36 | ## Rational behind the stack 37 | 38 | Check the resources [section](https://github.com/rafaelchiti/react_scaffolding/wiki/resources) for talks and explanations on WHY the stack I present here. 39 | 40 | ### Bundle process 41 | 42 | The scaffolding provides a feature-rich configuration for delivering the application as well as a rich development experience. 43 | Go [here](./WEBPACK.md) to read more about the details of our bundle process. 44 | 45 | ### Architecture (React + Flux) 46 | 47 | ### React patterns 48 | 49 | ### Other patterns 50 | -------------------------------------------------------------------------------- /app/views/login/login.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import classNames from "./styles"; 3 | 4 | export default class Login extends Component { 5 | 6 | constructor (props) { 7 | super(props); 8 | 9 | this.state = { 10 | email: "", 11 | password: "" 12 | }; 13 | } 14 | 15 | render () { 16 | return ( 17 |
18 |

Enter your credentials

19 |
20 | 21 | {this._renderAuthenticationErrors()} 22 | 23 | 25 | 26 | 28 | 29 |
30 | 31 |
32 | 33 |
34 |
35 | ); 36 | } 37 | 38 | _renderAuthenticationErrors () { 39 | if (this.props.authenticationError) { 40 | return
{this.props.authenticationError.errorMessage}
41 | } 42 | } 43 | 44 | _handleSubmit (event) { 45 | event.preventDefault(); 46 | this.props.onSubmit({email: this.state.email, password: this.state.password}); 47 | } 48 | 49 | _handleEmailChange (event) { 50 | this.setState({email: event.target.value}); 51 | } 52 | 53 | _handlePasswordChange (event) { 54 | this.setState({password: event.target.value}); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react_scaffolding", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/rafaelchiti/react_scaffolding.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/rafaelchiti/react_scaffolding/issues" 17 | }, 18 | "homepage": "https://github.com/rafaelchiti/react_scaffolding#readme", 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "babel-core": "6.3.21", 22 | "babel-eslint": "5.0.0-beta6", 23 | "babel-loader": "6.2.0", 24 | "babel-plugin-transform-decorators-legacy": "1.3.4", 25 | "babel-preset-es2015": "6.3.13", 26 | "babel-preset-react": "6.3.13", 27 | "babel-preset-stage-0": "6.3.13", 28 | "css-loader": "0.23.0", 29 | "eslint": "1.10.3", 30 | "eslint-plugin-react": "3.12.0", 31 | "extract-text-webpack-plugin": "0.9.1", 32 | "file-loader": "0.8.5", 33 | "history": "1.17.0", 34 | "html-webpack-plugin": "1.7.0", 35 | "json-loader": "0.5.4", 36 | "lodash": "3.10.1", 37 | "null-loader": "0.1.1", 38 | "platform": "1.3.0", 39 | "react": "0.14.3", 40 | "react-dom": "0.14.3", 41 | "react-hot-loader": "1.3.0", 42 | "react-redux": "4.0.2", 43 | "react-router": "1.0.1", 44 | "redux": "3.0.5", 45 | "redux-logger": "2.3.1", 46 | "script-loader": "0.6.1", 47 | "seamless-immutable": "4.1.1", 48 | "style-loader": "0.13.0", 49 | "stylus-loader": "1.4.2", 50 | "superagent": "1.6.1", 51 | "url-loader": "0.5.7", 52 | "webpack": "1.12.9", 53 | "webpack-dev-server": "1.14.0", 54 | "webpack-notifier": "1.2.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var WebpackNotifierPlugin = require("webpack-notifier"); 3 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 5 | var path = require("path"); 6 | 7 | var devServer; 8 | if (process.env.WEBPACK_DEV_SERVER) { 9 | devServer = { 10 | contentBase: "./build/prod_build" 11 | }; 12 | } 13 | 14 | var webpackConfig = { 15 | entry: { 16 | app: [ 17 | "./app/index.jsx" 18 | ], 19 | vendor: "./app/vendors/index.js" 20 | }, 21 | output: { 22 | path: "./build/prod_build", 23 | filename: "app.bundle-[chunkhash].js", 24 | }, 25 | devServer: devServer, 26 | module: { 27 | loaders: [ 28 | // IMPORTANT: we don"t want to process EVERY single JS file with babel 29 | // loader. We only want to process the files inside our app structure 30 | // otherwise this could get very slow or even fail. 31 | {test: /\.jsx?$/, exclude: /node_modules/, loaders: ["react-hot-loader", "babel-loader?optional=runtime&stage=0"]}, 32 | 33 | {test: /\.json$/, loader: "json-loader"}, 34 | {test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader?modules")}, 35 | {test: /\.styl$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader?modules!stylus-loader")}, 36 | { 37 | test: /\.(jpe?g|png|gif|svg)$/i, 38 | loaders: [ 39 | "file", 40 | "image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=true&progressive=true" 41 | ] 42 | }, 43 | {test: /\.mp3/, loader: "file"}, 44 | {test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader?mimetype=application/font-woff"}, 45 | {test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader"} 46 | ] 47 | }, 48 | resolve: { 49 | // Needed so you can require("a") instead of require("a.jsx") 50 | extensions: ["", ".js", ".jsx", ".json", ".styl"], 51 | // Let us do things like require("app/reducers/application") 52 | root: __dirname, 53 | alias: { 54 | react: path.join(__dirname, "node_modules/react") 55 | } 56 | }, 57 | plugins: [ 58 | new ExtractTextPlugin("app.bundle-[chunkhash].css", {allChunks: true}), 59 | new WebpackNotifierPlugin(), 60 | new webpack.NoErrorsPlugin(), 61 | new webpack.optimize.CommonsChunkPlugin({name: "vendor", filename: "vendor.bundle-[chunkhash].js", minChunks: Infinity}), 62 | new HtmlWebpackPlugin({ 63 | template: "./app/assets/index.template.html" 64 | }), 65 | new webpack.DefinePlugin({ 66 | "process.env": { 67 | NODE_ENV: JSON.stringify("production") 68 | } 69 | }) 70 | ] 71 | }; 72 | 73 | 74 | module.exports = webpackConfig; 75 | -------------------------------------------------------------------------------- /app/utils/custom_promise.js: -------------------------------------------------------------------------------- 1 | import env from "app/utils/env"; 2 | 3 | let CustomPromise = Promise; 4 | 5 | 6 | // On dev use chrome native promises, opposite to Babel sandboxed version. 7 | // Chrome native promise impl is the same as babel one spec-wise BUT 8 | // gets proper stack traces on uncaught exceptions. 9 | if (env.isDev) { 10 | CustomPromise = window.Promise; 11 | } 12 | 13 | export { 14 | CustomPromise 15 | }; 16 | 17 | /** 18 | * Wrap our implentation choise of promise library so we can easily modify or fix 19 | * errors and also switch to a new one in case we need to. HOWEVER please be 20 | * careful about changes here. 21 | * Whenever we change promises details we need to make sure a few conditions 22 | * are met: 23 | * 24 | * - Exceptions NEVER go silent. This could happen for instance if we reject a 25 | * promise intentionally and there is a consumer of that promise that adds 26 | * a .catch or .then(null, errorHanlder) that DOES NOT re-throws the uncaught 27 | * exception, in which case we are never gonna see an error in the console for 28 | * things like typo. 29 | * Example: 30 | * 31 | * apiCall().then( 32 | * () => Promise.resolve('success'), 33 | * () => Promise.reject('expected negative flow') 34 | * ).catch(() => 'expected negative flow handling') 35 | * 36 | * This case looks all good BUT what happens if we have a TYPO or an uncaught 37 | * error inside the apiCall, in that case since we added to the outter promise 38 | * a .catch the promise library is gonna think that we KNOW about the error 39 | * and delegate to us the 'handling', but we are not doing anything with the 40 | * exception that is gonna come in the arguments. Neither we want to do something 41 | * would be massive boilerplate to do so. 42 | * A solution to this is NEVER use rejected promises to state 'expected flows'. 43 | * 44 | * - Uncaught errors: An uncaught error is basically any runtime error (typo f.i), 45 | * they usually happen inside a resolveHandler or errorHandler, the problem is that 46 | * some libraries do not show an error on uncaught exceptions (mostly those that have 47 | * .done as part of their API). Therefore if you have something like 48 | * 49 | * apiCall().then(() => 'do something', null); 50 | * 51 | * and there is a typo inside the api call or even inside the 'success handler' 52 | * we could get an exception that depending on the library goes unseen or gets 53 | * rethrown as 'uncaught exception'. 54 | * 55 | * - Stack traces - Uncaught exceptions. 56 | * Most of the libraries that re-throw an uncaught exception will NOT provide 57 | * a stacktrace that chrome would understand correctly therefore we don't get 58 | * a nice stack trace. We found out that to get nice stacktraces you need to 59 | * rethrow the ex in the next tick (setTimeout(() => throw error))) or use 60 | * .done in the libraries that provide this, which internally does the setTimeout 61 | * solution. 62 | * However! the chrome native impl actually DOES throws the exception properly 63 | * and you get proper stack traces, so this is always an option at least for 64 | * development. 65 | */ 66 | 67 | 68 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var WebpackNotifierPlugin = require("webpack-notifier"); 3 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 5 | var path = require("path"); 6 | 7 | babelOptions = { 8 | cacheDirectory: true, 9 | presets: ["es2015", "stage-0", "react"], 10 | plugins: ["transform-decorators-legacy"] 11 | } 12 | 13 | var webpackConfig = { 14 | entry: { 15 | app: [ 16 | "webpack-dev-server/client?http://localhost:9898", // WebpackDevServer host and port 17 | "webpack/hot/only-dev-server", 18 | "./app/index.jsx" 19 | ], 20 | vendor: "./app/vendors/index.js" 21 | }, 22 | devServer: { 23 | 24 | // Configuration in case you need to proxy calls to an api 25 | proxy: { 26 | "/api/*": "http://localhost:5000" 27 | }, 28 | 29 | contentBase: "./build/dev_build" 30 | }, 31 | output: { 32 | path: "./build/dev_build", 33 | filename: "app.bundle-[hash].js" 34 | }, 35 | devtool: "cheap-module-eval-source-map", 36 | module: { 37 | loaders: [ 38 | 39 | // IMPORTANT: we don"t want to process EVERY single JS file with babel 40 | // loader. We only want to process the files inside our app structure 41 | // otherwise this could get very slow or even fail. 42 | { 43 | test: /\.jsx?$/, 44 | exclude: /node_modules/, 45 | loaders: [ 46 | "react-hot-loader", 47 | "babel-loader?" + JSON.stringify(babelOptions) 48 | ], 49 | }, 50 | 51 | {test: /\.json$/, loader: "json-loader"}, 52 | {test: /\.css$/, loader: "style-loader!css-loader?modules"}, 53 | {test: /\.styl$/, loader: "style-loader!css-loader?modules!stylus-loader"}, 54 | {test: /\.png/, loader: "file-loader?mimetype=image/png"}, 55 | {test: /\.jpg/, loader: "file"}, 56 | {test: /\.gif/, loader: "file"}, 57 | {test: /\.mp3/, loader: "file"}, 58 | {test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader?mimetype=application/font-woff"}, 59 | {test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader"} 60 | ] 61 | }, 62 | resolve: { 63 | 64 | // Needed so you can require("a") instead of require("a.jsx") 65 | extensions: ["", ".js", ".jsx", ".json", ".css", ".styl"], 66 | 67 | // Let us do things like require("app/reducers/application") 68 | root: __dirname, 69 | 70 | // Whenever someone does import "react", resolve the one in the node_modules 71 | // at the top level, just in case a dependency also has react in its node_modules, 72 | // we don't want to be running to versions of react!!! 73 | alias: { 74 | react: path.join(__dirname, "node_modules/react") 75 | } 76 | }, 77 | plugins: [ 78 | new WebpackNotifierPlugin(), 79 | new webpack.NoErrorsPlugin(), 80 | new webpack.optimize.CommonsChunkPlugin({name: "vendor", filename: "vendor.bundle-[hash].js", minChunks: Infinity}), 81 | new HtmlWebpackPlugin({ 82 | template: "./app/assets/index.template.html" 83 | }), 84 | new webpack.DefinePlugin({ 85 | "process.env": { 86 | NODE_ENV: JSON.stringify("development") 87 | } 88 | }) 89 | ] 90 | }; 91 | 92 | 93 | module.exports = webpackConfig; 94 | -------------------------------------------------------------------------------- /app/middleware/async_actions_middleware.js: -------------------------------------------------------------------------------- 1 | import {CustomPromise} from "app/utils/custom_promise"; 2 | import {isString, isObject} from "lodash"; 3 | 4 | /** 5 | * Middleware to accept actions like: 6 | * To accept async actions. 7 | * An async action is an action that has a type with the shape: 8 | * type: {request: requestType, done: doneType, fail: failType} 9 | * If the action type is provided has the shape shown above it will be 10 | * treated as an async action and dispatch the request version and then 11 | * execute the api call provided on the action. 12 | * 13 | * If the action type is a string then is treated as a sync action. 14 | */ 15 | export default function callAPIMiddleware ({ dispatch, getState }) { 16 | return function (next) { 17 | return function (action) { 18 | const { 19 | type, 20 | callAPI, 21 | shouldCallAPI = () => true, 22 | // Default to an empty object but let user provide 23 | // here the parameters used during dispatch in case they are required 24 | // by the DONE or FAIL action. 25 | payload = {} 26 | } = action; 27 | 28 | if (!type) { 29 | throw new Error("Not type provided for the action"); 30 | } 31 | 32 | // Validate the type provided 33 | if (isString(type)) { 34 | if (callAPI) { 35 | throw new Error(`The action: [${type}] was dispatched as a sync action but provided an Api Call, which is not coherent`); 36 | } 37 | // This is a sync action, just dispatch and return. 38 | return next(action); 39 | 40 | } 41 | 42 | if (isObject(type)) { 43 | if (!isString(type.request)) throw new Error(`Action type.request is expected to be a String. The value provided was: [${type.request}]`); 44 | if (!isString(type.done)) throw new Error(`Action type.request is expected to be a String. The value provided was: [${type.request}]`); 45 | if (!isString(type.fail)) throw new Error(`Action type.request is expected to be a String. The value provided was: [${type.request}]`); 46 | } else { 47 | throw new Error("Action type was expected to be an Object or a String."); 48 | } 49 | 50 | // We are in presence of a async action, therefore validate it has a 51 | // api call. 52 | if (typeof callAPI !== "function") { 53 | throw new Error("Expected fetch to be a function."); 54 | } 55 | 56 | if (!shouldCallAPI(getState())) { 57 | return; 58 | } 59 | 60 | 61 | dispatch({payload: payload, type: type.request}); 62 | 63 | 64 | // Always return a 'resolved' promise. This means that we don't need 65 | // to use .catch or .then(null, errorHandler) when consuming the result 66 | // of the dispatch. This also means that now everytime you attach to 67 | // .then when calling .dispatch() you need to check if the result was 68 | // an expected negative flow or a positive flow, you can eaily do that 69 | // by checking on the params .then((result) => result.error) f.i. 70 | return callAPI().then( 71 | (result) => CustomPromise.resolve(dispatch({payload: payload, apiResponse: result.apiResponse, type: type.done})), 72 | (result) => CustomPromise.resolve(dispatch({payload: payload, apiError: result.apiError, type: type.fail})) 73 | ); 74 | }; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /app/utils/immutable_helpers.js: -------------------------------------------------------------------------------- 1 | import Immutable from "seamless-immutable"; 2 | import { 3 | isPlainObject, 4 | isArray, 5 | isUndefined, 6 | isObject 7 | } from "lodash"; 8 | 9 | /** 10 | * Collection of helpers to perform operations on objects/arrays. 11 | * The current implementation works under the assumption of seamless-immutable 12 | * collections wrapping all our objects/arrays. 13 | * The beauty of this is that we can easily migrate this impl to use other library 14 | * or just use plain objects if we want and all the reducers should work as usual. 15 | */ 16 | 17 | /** 18 | * Returns a new object also containing the new key, value pair. 19 | * If an equivalent key already exists in this Map, it will be replaced. 20 | * You can use as a shortcut nexted paths (delimited by dots). 21 | */ 22 | export function set (sourceObject, keyPath, value) { 23 | if (isPlainObject(keyPath)) { 24 | return sourceObject.merge(keyPath); 25 | } 26 | 27 | const keys = isArray(keyPath) ? keyPath : keyPath.split("."); 28 | 29 | //TODO: This will only short circuit at the very first execution, 30 | // when doing recursion, we don't need this anymore and is time spent. 31 | // seamless-immutable is handling this in SOME of the cases, but not others 32 | // (remove this line and run test to see it fails). How we can improve this? 33 | if(getIn(sourceObject, keys) === value) return sourceObject; 34 | 35 | 36 | let merged = {}; 37 | if (keys.length === 1) { 38 | if (isPlainObject(sourceObject)) { 39 | merged[keys[0]] = value; 40 | return sourceObject.merge(merged); 41 | } else if (isArray(sourceObject)) { 42 | let newObject = sourceObject.asMutable(); 43 | newObject[keys[0]] = value; 44 | return Immutable(newObject); 45 | } else if (isUndefined(sourceObject)) { 46 | let newObject = {}; 47 | newObject[keys[0]] = value; 48 | return Immutable(newObject); 49 | } 50 | } else { 51 | if (isPlainObject(sourceObject)) { 52 | merged[keys[0]] = set(sourceObject[keys[0]], keys.slice(1), value); 53 | return sourceObject.merge(merged); 54 | } else if (isArray(sourceObject)) { 55 | let newObject = sourceObject.asMutable(); 56 | newObject[keys[0]] = set(sourceObject[keys[0]], keys.slice(1), value); 57 | return Immutable(newObject); 58 | } else if (isUndefined(sourceObject)) { 59 | merged[keys[0]] = set(undefined, keys.slice(1), value); 60 | return Immutable(merged); 61 | } 62 | } 63 | } 64 | 65 | 66 | 67 | /** 68 | * Returns a new object resulting from merging the source object the new one. 69 | * The keyPath allows you to specify at which level to perform the merge, or if you 70 | * send the object to merge instead of a keyPath then it will be used to be merged 71 | * on the root level of the source object. This will perform a deep merge but won't 72 | * affect those siblings or keys that already existed in the source object, will 73 | * only override existing keys with the values from the new object. 74 | * 75 | */ 76 | export function merge (sourceObject, keyPath, object) { 77 | if (isObject(keyPath)) { 78 | return sourceObject.merge(keyPath, {deep: true}); 79 | } else { 80 | return sourceObject.merge(buildNestedObject({}, keyPath, object), {deep: true}); 81 | } 82 | } 83 | 84 | /** 85 | * Returns a new object containing all the keys / values from the source object 86 | * but the one specified in the `key` parameter. 87 | */ 88 | export function without (sourceObject, key) { 89 | return sourceObject.without(key); 90 | } 91 | 92 | /** 93 | * Wrap the object as a seamless immutable object. 94 | */ 95 | export function immutable (object) { 96 | return Immutable(object); 97 | } 98 | 99 | 100 | 101 | 102 | /*** 103 | * Utilitary functions 104 | */ 105 | 106 | /** 107 | * Private: Take a key patch such as "student.teacher.name" and a value to build 108 | * the nested structure with that value assigned. Also expects an initial object 109 | * to use for building the structure. 110 | * f.i: 111 | * buildNestedObject({}, "student.teacher.name", "john") => {student: {teacher: {name: "john"}}} 112 | */ 113 | function buildNestedObject (obj = {}, keyPath, value) { 114 | const keys = isArray(keyPath) ? keyPath : keyPath.split("."); 115 | 116 | if (keys.length === 1) { 117 | obj[keys[0]] = value; 118 | } else { 119 | var key = keys.shift(); 120 | obj[key] = buildNestedObject(typeof obj[key] === "undefined" ? {} : obj[key], keys, value); 121 | } 122 | 123 | return obj; 124 | } 125 | 126 | 127 | 128 | /** 129 | * Private: Return the value at the given key path. 130 | * keyPath can be either an array of keys or a string delimited by dots. 131 | * Useful for getting values in a nested object. 132 | * f.i: getIn(object, "key1.key2.name") 133 | */ 134 | function getIn (object, keyPath) { 135 | const keys = isArray(keyPath) ? keyPath : keyPath.split("."); 136 | 137 | if (isUndefined(object)) { return undefined; } 138 | 139 | if (keys.length === 1) { 140 | return object[keys[0]]; 141 | } else { 142 | return getIn(object[keys[0]], keys.slice(1)); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/utils/api.js: -------------------------------------------------------------------------------- 1 | import Request from "superagent"; 2 | import _ from "lodash"; 3 | import {CustomPromise} from "app/utils/custom_promise"; 4 | 5 | var TIMEOUT = 15000; 6 | 7 | function makeUrl(url) { 8 | if (_.isArray(url)) { 9 | url = "/" + url.join("/"); 10 | } 11 | url = "/api" + url; 12 | return url; 13 | } 14 | 15 | function removeValue (arr, v) { 16 | _.remove(arr, (item) => item === v); 17 | } 18 | 19 | var _pendingRequests = []; 20 | 21 | // Abor the requests for the Api Call with the specified name. 22 | // Be careful since won"t make any difference if the same api call gets 23 | // called with diffrent query strings or body, this feature stops any 24 | // pending call for the specified Api Call 25 | function abortPendingRequestsForApiCall(apiCallName) { 26 | var pendingRequest = _.find(_pendingRequests, (pending) => { 27 | return pending._apiCallName === apiCallName; 28 | }); 29 | 30 | if (pendingRequest) { 31 | pendingRequest._callback = () => {}; 32 | pendingRequest.abort(); 33 | removeValue(_pendingRequests, pendingRequest); 34 | } 35 | } 36 | 37 | function digestResponse(resolve, reject, error, request, response, options) { 38 | 39 | 40 | var result = {}; 41 | 42 | 43 | // Autofail with standard api error on timeout. 44 | if (error && error.timeout >= 0) { 45 | result.apiError = "TIMEOUT"; 46 | reject(result); 47 | 48 | // Auto-fail with special auth error on 401. 49 | } else if (response.status === 401 && !options.ignoreAuthFailure) { 50 | result.apiError = "AUTH_ERROR"; 51 | reject(result); 52 | 53 | // Auto-fail with special api down error on 502 - 504. 54 | // IE can do weird things with 5xx errors like call them 12031s. 55 | } else if ((response.status >= 502 && response.status <= 504) || response.status > 12000) { 56 | result.apiError = "500_ERROR"; 57 | reject(result); 58 | 59 | } else if (response.status === 429) { 60 | result.apiError = "RATE_LIMIT_ERROR"; 61 | reject(result); 62 | 63 | } else { 64 | 65 | response.body = response.body || {}; // patch response.body if nonexistant 66 | 67 | let gotExpectedResponse; 68 | let parser = options.parse || defaultParser; 69 | 70 | let done = function (data) { 71 | gotExpectedResponse = true; 72 | result.apiResponse = data || {}; 73 | resolve(result); 74 | }; 75 | 76 | let fail = function (err) { 77 | gotExpectedResponse = true; 78 | result.apiError = err; 79 | reject(result); 80 | }; 81 | 82 | let pass = function() { 83 | // do nothing. don"t resolve or reject the promise. 84 | gotExpectedResponse = true; 85 | }; 86 | 87 | parser.call({done, fail, pass}, response); 88 | 89 | // Our parser did not get a response it understands 90 | if (!gotExpectedResponse) { 91 | result.apiError = "UNKOWN ERROR"; 92 | reject(result); 93 | } 94 | } 95 | } 96 | 97 | function defaultParser(res) { 98 | if (isSuccessStatus(res.status)) { 99 | return this.done(res.body); 100 | } 101 | } 102 | 103 | function isSuccessStatus(status) { 104 | return status >= 200 && status <= 299; 105 | } 106 | 107 | function executeRequestFlow(options) { 108 | return new CustomPromise(function (resolve, reject) { 109 | 110 | options.method = options.method || "GET"; 111 | 112 | let url = options.absolutePath || makeUrl(options.path); 113 | 114 | var request = Request(options.method, url); 115 | 116 | var query = {}; 117 | 118 | if (_(["GET", "POST", "PUT"]).contains(options.method)) { 119 | request.accept("json"); 120 | request.type("json"); 121 | } 122 | 123 | if (_(["POST", "PUT"]).contains(options.method)) { 124 | options.body = options.body || {}; 125 | } 126 | 127 | // If you need to set a cookie do as follow: 128 | // request.set("Cookie", sessionCookie); 129 | 130 | if (options.body) { 131 | request.send(options.body); 132 | Object.keys(options.body).forEach(function (key) { 133 | if (options.body[key] === undefined) { 134 | console.warn("Key was undefined in request body:", key); 135 | } 136 | }); 137 | } 138 | 139 | 140 | if (options.query) { 141 | _.extend(query, options.query); 142 | } 143 | 144 | if (Object.keys(query).length) { 145 | request.query(query); 146 | } 147 | 148 | request.timeout(TIMEOUT); 149 | 150 | // Prevent concurrent calls for the same Api Call type. 151 | if (options.cancelPendingRequests) { 152 | 153 | if (request._apiCallName) console.log("WARNING: Prop clashing with request object"); 154 | if (!options.apiCallName) console.log("WARNING: To cancel previous calls the Api Call needs a name defined."); 155 | 156 | request._apiCallName = options.apiCallName; 157 | 158 | abortPendingRequestsForApiCall(options.apiCallName); 159 | } 160 | 161 | // Request Callback logic 162 | request.end(function (error, response) { 163 | digestResponse(resolve, reject, error, request, response, options); 164 | removeValue(_pendingRequests, request); 165 | }); 166 | 167 | _pendingRequests.push(request); 168 | }); 169 | } 170 | 171 | // API Interface 172 | var Api = { 173 | execute: executeRequestFlow, 174 | 175 | get: function (options) { 176 | options.method = "GET"; 177 | return executeRequestFlow(options); 178 | }, 179 | 180 | put: function (options) { 181 | options.method = "PUT"; 182 | return executeRequestFlow(options); 183 | }, 184 | 185 | post: function (options) { 186 | options.method = "POST"; 187 | return executeRequestFlow(options); 188 | }, 189 | 190 | delete: function (options) { 191 | options.method = "DELETE"; 192 | return executeRequestFlow(options); 193 | }, 194 | 195 | head: function (options) { 196 | options.method = "HEAD"; 197 | return executeRequestFlow(options); 198 | }, 199 | 200 | isSuccessStatus: function (status) { 201 | return isSuccessStatus(status); 202 | } 203 | 204 | }; 205 | 206 | module.exports = Api; 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /app/assets/stylesheets/normalize.styl: -------------------------------------------------------------------------------- 1 | :global { 2 | 3 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 4 | 5 | /** 6 | * 1. Set default font family to sans-serif. 7 | * 2. Prevent iOS text size adjust after orientation change, without disabling 8 | * user zoom. 9 | */ 10 | 11 | html { 12 | font-family: sans-serif; /* 1 */ 13 | -ms-text-size-adjust: 100%; /* 2 */ 14 | -webkit-text-size-adjust: 100%; /* 2 */ 15 | } 16 | 17 | /** 18 | * Remove default margin. 19 | */ 20 | 21 | body { 22 | margin: 0; 23 | } 24 | 25 | /* HTML5 display definitions 26 | ========================================================================== */ 27 | 28 | /** 29 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 30 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 31 | * and Firefox. 32 | * Correct `block` display not defined for `main` in IE 11. 33 | */ 34 | 35 | article, 36 | aside, 37 | details, 38 | figcaption, 39 | figure, 40 | footer, 41 | header, 42 | hgroup, 43 | main, 44 | menu, 45 | nav, 46 | section, 47 | summary { 48 | display: block; 49 | } 50 | 51 | /** 52 | * 1. Correct `inline-block` display not defined in IE 8/9. 53 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 54 | */ 55 | 56 | audio, 57 | canvas, 58 | progress, 59 | video { 60 | display: inline-block; /* 1 */ 61 | vertical-align: baseline; /* 2 */ 62 | } 63 | 64 | /** 65 | * Prevent modern browsers from displaying `audio` without controls. 66 | * Remove excess height in iOS 5 devices. 67 | */ 68 | 69 | audio:not([controls]) { 70 | display: none; 71 | height: 0; 72 | } 73 | 74 | /** 75 | * Address `[hidden]` styling not present in IE 8/9/10. 76 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 77 | */ 78 | 79 | [hidden], 80 | template { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * Remove the gray background color from active links in IE 10. 89 | */ 90 | 91 | a { 92 | background-color: transparent; 93 | } 94 | 95 | /** 96 | * Improve readability when focused and also mouse hovered in all browsers. 97 | */ 98 | 99 | a:active, 100 | a:hover { 101 | outline: 0; 102 | } 103 | 104 | /* Text-level semantics 105 | ========================================================================== */ 106 | 107 | /** 108 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 109 | */ 110 | 111 | abbr[title] { 112 | border-bottom: 1px dotted; 113 | } 114 | 115 | /** 116 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 117 | */ 118 | 119 | b, 120 | strong { 121 | font-weight: bold; 122 | } 123 | 124 | /** 125 | * Address styling not present in Safari and Chrome. 126 | */ 127 | 128 | dfn { 129 | font-style: italic; 130 | } 131 | 132 | /** 133 | * Address variable `h1` font-size and margin within `section` and `article` 134 | * contexts in Firefox 4+, Safari, and Chrome. 135 | */ 136 | 137 | h1 { 138 | font-size: 2em; 139 | margin: 0.67em 0; 140 | } 141 | 142 | /** 143 | * Address styling not present in IE 8/9. 144 | */ 145 | 146 | mark { 147 | background: #ff0; 148 | color: #000; 149 | } 150 | 151 | /** 152 | * Address inconsistent and variable font size in all browsers. 153 | */ 154 | 155 | small { 156 | font-size: 80%; 157 | } 158 | 159 | /** 160 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 161 | */ 162 | 163 | sub, 164 | sup { 165 | font-size: 75%; 166 | line-height: 0; 167 | position: relative; 168 | vertical-align: baseline; 169 | } 170 | 171 | sup { 172 | top: -0.5em; 173 | } 174 | 175 | sub { 176 | bottom: -0.25em; 177 | } 178 | 179 | /* Embedded content 180 | ========================================================================== */ 181 | 182 | /** 183 | * Remove border when inside `a` element in IE 8/9/10. 184 | */ 185 | 186 | img { 187 | border: 0; 188 | } 189 | 190 | /** 191 | * Correct overflow not hidden in IE 9/10/11. 192 | */ 193 | 194 | svg:not(:root) { 195 | overflow: hidden; 196 | } 197 | 198 | /* Grouping content 199 | ========================================================================== */ 200 | 201 | /** 202 | * Address margin not present in IE 8/9 and Safari. 203 | */ 204 | 205 | figure { 206 | margin: 1em 40px; 207 | } 208 | 209 | /** 210 | * Address differences between Firefox and other browsers. 211 | */ 212 | 213 | hr { 214 | -moz-box-sizing: content-box; 215 | box-sizing: content-box; 216 | height: 0; 217 | } 218 | 219 | /** 220 | * Contain overflow in all browsers. 221 | */ 222 | 223 | pre { 224 | overflow: auto; 225 | } 226 | 227 | /** 228 | * Address odd `em`-unit font size rendering in all browsers. 229 | */ 230 | 231 | code, 232 | kbd, 233 | pre, 234 | samp { 235 | font-family: monospace, monospace; 236 | font-size: 1em; 237 | } 238 | 239 | /* Forms 240 | ========================================================================== */ 241 | 242 | /** 243 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 244 | * styling of `select`, unless a `border` property is set. 245 | */ 246 | 247 | /** 248 | * 1. Correct color not being inherited. 249 | * Known issue: affects color of disabled elements. 250 | * 2. Correct font properties not being inherited. 251 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 252 | */ 253 | 254 | button, 255 | input, 256 | optgroup, 257 | select, 258 | textarea { 259 | color: inherit; /* 1 */ 260 | font: inherit; /* 2 */ 261 | margin: 0; /* 3 */ 262 | } 263 | 264 | /** 265 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 266 | */ 267 | 268 | button { 269 | overflow: visible; 270 | } 271 | 272 | /** 273 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 274 | * All other form control elements do not inherit `text-transform` values. 275 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 276 | * Correct `select` style inheritance in Firefox. 277 | */ 278 | 279 | button, 280 | select { 281 | text-transform: none; 282 | } 283 | 284 | /** 285 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 286 | * and `video` controls. 287 | * 2. Correct inability to style clickable `input` types in iOS. 288 | * 3. Improve usability and consistency of cursor style between image-type 289 | * `input` and others. 290 | */ 291 | 292 | button, 293 | html input[type="button"], /* 1 */ 294 | input[type="reset"], 295 | input[type="submit"] { 296 | -webkit-appearance: button; /* 2 */ 297 | cursor: pointer; /* 3 */ 298 | } 299 | 300 | /** 301 | * Re-set default cursor for disabled elements. 302 | */ 303 | 304 | button[disabled], 305 | html input[disabled] { 306 | cursor: default; 307 | } 308 | 309 | /** 310 | * Remove inner padding and border in Firefox 4+. 311 | */ 312 | 313 | button::-moz-focus-inner, 314 | input::-moz-focus-inner { 315 | border: 0; 316 | padding: 0; 317 | } 318 | 319 | /** 320 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 321 | * the UA stylesheet. 322 | */ 323 | 324 | input { 325 | line-height: normal; 326 | } 327 | 328 | /** 329 | * It's recommended that you don't attempt to style these elements. 330 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 331 | * 332 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 333 | * 2. Remove excess padding in IE 8/9/10. 334 | */ 335 | 336 | input[type="checkbox"], 337 | input[type="radio"] { 338 | box-sizing: border-box; /* 1 */ 339 | padding: 0; /* 2 */ 340 | } 341 | 342 | /** 343 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 344 | * `font-size` values of the `input`, it causes the cursor style of the 345 | * decrement button to change from `default` to `text`. 346 | */ 347 | 348 | input[type="number"]::-webkit-inner-spin-button, 349 | input[type="number"]::-webkit-outer-spin-button { 350 | height: auto; 351 | } 352 | 353 | /** 354 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 355 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 356 | * (include `-moz` to future-proof). 357 | */ 358 | 359 | input[type="search"] { 360 | -webkit-appearance: textfield; /* 1 */ 361 | -moz-box-sizing: content-box; 362 | -webkit-box-sizing: content-box; /* 2 */ 363 | box-sizing: content-box; 364 | } 365 | 366 | /** 367 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 368 | * Safari (but not Chrome) clips the cancel button when the search input has 369 | * padding (and `textfield` appearance). 370 | */ 371 | 372 | input[type="search"]::-webkit-search-cancel-button, 373 | input[type="search"]::-webkit-search-decoration { 374 | -webkit-appearance: none; 375 | } 376 | 377 | /** 378 | * Define consistent border, margin, and padding. 379 | */ 380 | 381 | fieldset { 382 | border: 1px solid #c0c0c0; 383 | margin: 0 2px; 384 | padding: 0.35em 0.625em 0.75em; 385 | } 386 | 387 | /** 388 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 389 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 390 | */ 391 | 392 | legend { 393 | border: 0; /* 1 */ 394 | padding: 0; /* 2 */ 395 | } 396 | 397 | /** 398 | * Remove default vertical scrollbar in IE 8/9/10/11. 399 | */ 400 | 401 | textarea { 402 | overflow: auto; 403 | } 404 | 405 | /** 406 | * Don't inherit the `font-weight` (applied by a rule above). 407 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 408 | */ 409 | 410 | optgroup { 411 | font-weight: bold; 412 | } 413 | 414 | /* Tables 415 | ========================================================================== */ 416 | 417 | /** 418 | * Remove most spacing between table cells. 419 | */ 420 | 421 | table { 422 | border-collapse: collapse; 423 | border-spacing: 0; 424 | } 425 | 426 | td, 427 | th { 428 | padding: 0; 429 | } 430 | 431 | } 432 | -------------------------------------------------------------------------------- /app/assets/stylesheets/glyphicons.styl: -------------------------------------------------------------------------------- 1 | :global { 2 | @font-face { 3 | font-family: 'Glyphicons Halflings'; 4 | 5 | src: url('app/assets/fonts/glyphicons/glyphicons-halflings-regular.eot'); 6 | src: url('app/assets/fonts/glyphicons/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('app/assets/fonts/glyphicons/glyphicons-halflings-regular.woff2') format('woff2'), url('app/assets/fonts/glyphicons/glyphicons-halflings-regular.woff') format('woff'), url('app/assets/fonts/glyphicons/glyphicons-halflings-regular.ttf') format('truetype'), url('app/assets/fonts/glyphicons/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); 7 | } 8 | .glyphicon { 9 | position: relative; 10 | top: 1px; 11 | display: inline-block; 12 | font-family: 'Glyphicons Halflings'; 13 | font-style: normal; 14 | font-weight: normal; 15 | line-height: 1; 16 | 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | } 20 | .glyphicon-asterisk:before { 21 | content: "\2a"; 22 | } 23 | .glyphicon-plus:before { 24 | content: "\2b"; 25 | } 26 | .glyphicon-euro:before, 27 | .glyphicon-eur:before { 28 | content: "\20ac"; 29 | } 30 | .glyphicon-minus:before { 31 | content: "\2212"; 32 | } 33 | .glyphicon-cloud:before { 34 | content: "\2601"; 35 | } 36 | .glyphicon-envelope:before { 37 | content: "\2709"; 38 | } 39 | .glyphicon-pencil:before { 40 | content: "\270f"; 41 | } 42 | .glyphicon-glass:before { 43 | content: "\e001"; 44 | } 45 | .glyphicon-music:before { 46 | content: "\e002"; 47 | } 48 | .glyphicon-search:before { 49 | content: "\e003"; 50 | } 51 | .glyphicon-heart:before { 52 | content: "\e005"; 53 | } 54 | .glyphicon-star:before { 55 | content: "\e006"; 56 | } 57 | .glyphicon-star-empty:before { 58 | content: "\e007"; 59 | } 60 | .glyphicon-user:before { 61 | content: "\e008"; 62 | } 63 | .glyphicon-film:before { 64 | content: "\e009"; 65 | } 66 | .glyphicon-th-large:before { 67 | content: "\e010"; 68 | } 69 | .glyphicon-th:before { 70 | content: "\e011"; 71 | } 72 | .glyphicon-th-list:before { 73 | content: "\e012"; 74 | } 75 | .glyphicon-ok:before { 76 | content: "\e013"; 77 | } 78 | .glyphicon-remove:before { 79 | content: "\e014"; 80 | } 81 | .glyphicon-zoom-in:before { 82 | content: "\e015"; 83 | } 84 | .glyphicon-zoom-out:before { 85 | content: "\e016"; 86 | } 87 | .glyphicon-off:before { 88 | content: "\e017"; 89 | } 90 | .glyphicon-signal:before { 91 | content: "\e018"; 92 | } 93 | .glyphicon-cog:before { 94 | content: "\e019"; 95 | } 96 | .glyphicon-trash:before { 97 | content: "\e020"; 98 | } 99 | .glyphicon-home:before { 100 | content: "\e021"; 101 | } 102 | .glyphicon-file:before { 103 | content: "\e022"; 104 | } 105 | .glyphicon-time:before { 106 | content: "\e023"; 107 | } 108 | .glyphicon-road:before { 109 | content: "\e024"; 110 | } 111 | .glyphicon-download-alt:before { 112 | content: "\e025"; 113 | } 114 | .glyphicon-download:before { 115 | content: "\e026"; 116 | } 117 | .glyphicon-upload:before { 118 | content: "\e027"; 119 | } 120 | .glyphicon-inbox:before { 121 | content: "\e028"; 122 | } 123 | .glyphicon-play-circle:before { 124 | content: "\e029"; 125 | } 126 | .glyphicon-repeat:before { 127 | content: "\e030"; 128 | } 129 | .glyphicon-refresh:before { 130 | content: "\e031"; 131 | } 132 | .glyphicon-list-alt:before { 133 | content: "\e032"; 134 | } 135 | .glyphicon-lock:before { 136 | content: "\e033"; 137 | } 138 | .glyphicon-flag:before { 139 | content: "\e034"; 140 | } 141 | .glyphicon-headphones:before { 142 | content: "\e035"; 143 | } 144 | .glyphicon-volume-off:before { 145 | content: "\e036"; 146 | } 147 | .glyphicon-volume-down:before { 148 | content: "\e037"; 149 | } 150 | .glyphicon-volume-up:before { 151 | content: "\e038"; 152 | } 153 | .glyphicon-qrcode:before { 154 | content: "\e039"; 155 | } 156 | .glyphicon-barcode:before { 157 | content: "\e040"; 158 | } 159 | .glyphicon-tag:before { 160 | content: "\e041"; 161 | } 162 | .glyphicon-tags:before { 163 | content: "\e042"; 164 | } 165 | .glyphicon-book:before { 166 | content: "\e043"; 167 | } 168 | .glyphicon-bookmark:before { 169 | content: "\e044"; 170 | } 171 | .glyphicon-print:before { 172 | content: "\e045"; 173 | } 174 | .glyphicon-camera:before { 175 | content: "\e046"; 176 | } 177 | .glyphicon-font:before { 178 | content: "\e047"; 179 | } 180 | .glyphicon-bold:before { 181 | content: "\e048"; 182 | } 183 | .glyphicon-italic:before { 184 | content: "\e049"; 185 | } 186 | .glyphicon-text-height:before { 187 | content: "\e050"; 188 | } 189 | .glyphicon-text-width:before { 190 | content: "\e051"; 191 | } 192 | .glyphicon-align-left:before { 193 | content: "\e052"; 194 | } 195 | .glyphicon-align-center:before { 196 | content: "\e053"; 197 | } 198 | .glyphicon-align-right:before { 199 | content: "\e054"; 200 | } 201 | .glyphicon-align-justify:before { 202 | content: "\e055"; 203 | } 204 | .glyphicon-list:before { 205 | content: "\e056"; 206 | } 207 | .glyphicon-indent-left:before { 208 | content: "\e057"; 209 | } 210 | .glyphicon-indent-right:before { 211 | content: "\e058"; 212 | } 213 | .glyphicon-facetime-video:before { 214 | content: "\e059"; 215 | } 216 | .glyphicon-picture:before { 217 | content: "\e060"; 218 | } 219 | .glyphicon-map-marker:before { 220 | content: "\e062"; 221 | } 222 | .glyphicon-adjust:before { 223 | content: "\e063"; 224 | } 225 | .glyphicon-tint:before { 226 | content: "\e064"; 227 | } 228 | .glyphicon-edit:before { 229 | content: "\e065"; 230 | } 231 | .glyphicon-share:before { 232 | content: "\e066"; 233 | } 234 | .glyphicon-check:before { 235 | content: "\e067"; 236 | } 237 | .glyphicon-move:before { 238 | content: "\e068"; 239 | } 240 | .glyphicon-step-backward:before { 241 | content: "\e069"; 242 | } 243 | .glyphicon-fast-backward:before { 244 | content: "\e070"; 245 | } 246 | .glyphicon-backward:before { 247 | content: "\e071"; 248 | } 249 | .glyphicon-play:before { 250 | content: "\e072"; 251 | } 252 | .glyphicon-pause:before { 253 | content: "\e073"; 254 | } 255 | .glyphicon-stop:before { 256 | content: "\e074"; 257 | } 258 | .glyphicon-forward:before { 259 | content: "\e075"; 260 | } 261 | .glyphicon-fast-forward:before { 262 | content: "\e076"; 263 | } 264 | .glyphicon-step-forward:before { 265 | content: "\e077"; 266 | } 267 | .glyphicon-eject:before { 268 | content: "\e078"; 269 | } 270 | .glyphicon-chevron-left:before { 271 | content: "\e079"; 272 | } 273 | .glyphicon-chevron-right:before { 274 | content: "\e080"; 275 | } 276 | .glyphicon-plus-sign:before { 277 | content: "\e081"; 278 | } 279 | .glyphicon-minus-sign:before { 280 | content: "\e082"; 281 | } 282 | .glyphicon-remove-sign:before { 283 | content: "\e083"; 284 | } 285 | .glyphicon-ok-sign:before { 286 | content: "\e084"; 287 | } 288 | .glyphicon-question-sign:before { 289 | content: "\e085"; 290 | } 291 | .glyphicon-info-sign:before { 292 | content: "\e086"; 293 | } 294 | .glyphicon-screenshot:before { 295 | content: "\e087"; 296 | } 297 | .glyphicon-remove-circle:before { 298 | content: "\e088"; 299 | } 300 | .glyphicon-ok-circle:before { 301 | content: "\e089"; 302 | } 303 | .glyphicon-ban-circle:before { 304 | content: "\e090"; 305 | } 306 | .glyphicon-arrow-left:before { 307 | content: "\e091"; 308 | } 309 | .glyphicon-arrow-right:before { 310 | content: "\e092"; 311 | } 312 | .glyphicon-arrow-up:before { 313 | content: "\e093"; 314 | } 315 | .glyphicon-arrow-down:before { 316 | content: "\e094"; 317 | } 318 | .glyphicon-share-alt:before { 319 | content: "\e095"; 320 | } 321 | .glyphicon-resize-full:before { 322 | content: "\e096"; 323 | } 324 | .glyphicon-resize-small:before { 325 | content: "\e097"; 326 | } 327 | .glyphicon-exclamation-sign:before { 328 | content: "\e101"; 329 | } 330 | .glyphicon-gift:before { 331 | content: "\e102"; 332 | } 333 | .glyphicon-leaf:before { 334 | content: "\e103"; 335 | } 336 | .glyphicon-fire:before { 337 | content: "\e104"; 338 | } 339 | .glyphicon-eye-open:before { 340 | content: "\e105"; 341 | } 342 | .glyphicon-eye-close:before { 343 | content: "\e106"; 344 | } 345 | .glyphicon-warning-sign:before { 346 | content: "\e107"; 347 | } 348 | .glyphicon-plane:before { 349 | content: "\e108"; 350 | } 351 | .glyphicon-calendar:before { 352 | content: "\e109"; 353 | } 354 | .glyphicon-random:before { 355 | content: "\e110"; 356 | } 357 | .glyphicon-comment:before { 358 | content: "\e111"; 359 | } 360 | .glyphicon-magnet:before { 361 | content: "\e112"; 362 | } 363 | .glyphicon-chevron-up:before { 364 | content: "\e113"; 365 | } 366 | .glyphicon-chevron-down:before { 367 | content: "\e114"; 368 | } 369 | .glyphicon-retweet:before { 370 | content: "\e115"; 371 | } 372 | .glyphicon-shopping-cart:before { 373 | content: "\e116"; 374 | } 375 | .glyphicon-folder-close:before { 376 | content: "\e117"; 377 | } 378 | .glyphicon-folder-open:before { 379 | content: "\e118"; 380 | } 381 | .glyphicon-resize-vertical:before { 382 | content: "\e119"; 383 | } 384 | .glyphicon-resize-horizontal:before { 385 | content: "\e120"; 386 | } 387 | .glyphicon-hdd:before { 388 | content: "\e121"; 389 | } 390 | .glyphicon-bullhorn:before { 391 | content: "\e122"; 392 | } 393 | .glyphicon-bell:before { 394 | content: "\e123"; 395 | } 396 | .glyphicon-certificate:before { 397 | content: "\e124"; 398 | } 399 | .glyphicon-thumbs-up:before { 400 | content: "\e125"; 401 | } 402 | .glyphicon-thumbs-down:before { 403 | content: "\e126"; 404 | } 405 | .glyphicon-hand-right:before { 406 | content: "\e127"; 407 | } 408 | .glyphicon-hand-left:before { 409 | content: "\e128"; 410 | } 411 | .glyphicon-hand-up:before { 412 | content: "\e129"; 413 | } 414 | .glyphicon-hand-down:before { 415 | content: "\e130"; 416 | } 417 | .glyphicon-circle-arrow-right:before { 418 | content: "\e131"; 419 | } 420 | .glyphicon-circle-arrow-left:before { 421 | content: "\e132"; 422 | } 423 | .glyphicon-circle-arrow-up:before { 424 | content: "\e133"; 425 | } 426 | .glyphicon-circle-arrow-down:before { 427 | content: "\e134"; 428 | } 429 | .glyphicon-globe:before { 430 | content: "\e135"; 431 | } 432 | .glyphicon-wrench:before { 433 | content: "\e136"; 434 | } 435 | .glyphicon-tasks:before { 436 | content: "\e137"; 437 | } 438 | .glyphicon-filter:before { 439 | content: "\e138"; 440 | } 441 | .glyphicon-briefcase:before { 442 | content: "\e139"; 443 | } 444 | .glyphicon-fullscreen:before { 445 | content: "\e140"; 446 | } 447 | .glyphicon-dashboard:before { 448 | content: "\e141"; 449 | } 450 | .glyphicon-paperclip:before { 451 | content: "\e142"; 452 | } 453 | .glyphicon-heart-empty:before { 454 | content: "\e143"; 455 | } 456 | .glyphicon-link:before { 457 | content: "\e144"; 458 | } 459 | .glyphicon-phone:before { 460 | content: "\e145"; 461 | } 462 | .glyphicon-pushpin:before { 463 | content: "\e146"; 464 | } 465 | .glyphicon-usd:before { 466 | content: "\e148"; 467 | } 468 | .glyphicon-gbp:before { 469 | content: "\e149"; 470 | } 471 | .glyphicon-sort:before { 472 | content: "\e150"; 473 | } 474 | .glyphicon-sort-by-alphabet:before { 475 | content: "\e151"; 476 | } 477 | .glyphicon-sort-by-alphabet-alt:before { 478 | content: "\e152"; 479 | } 480 | .glyphicon-sort-by-order:before { 481 | content: "\e153"; 482 | } 483 | .glyphicon-sort-by-order-alt:before { 484 | content: "\e154"; 485 | } 486 | .glyphicon-sort-by-attributes:before { 487 | content: "\e155"; 488 | } 489 | .glyphicon-sort-by-attributes-alt:before { 490 | content: "\e156"; 491 | } 492 | .glyphicon-unchecked:before { 493 | content: "\e157"; 494 | } 495 | .glyphicon-expand:before { 496 | content: "\e158"; 497 | } 498 | .glyphicon-collapse-down:before { 499 | content: "\e159"; 500 | } 501 | .glyphicon-collapse-up:before { 502 | content: "\e160"; 503 | } 504 | .glyphicon-log-in:before { 505 | content: "\e161"; 506 | } 507 | .glyphicon-flash:before { 508 | content: "\e162"; 509 | } 510 | .glyphicon-log-out:before { 511 | content: "\e163"; 512 | } 513 | .glyphicon-new-window:before { 514 | content: "\e164"; 515 | } 516 | .glyphicon-record:before { 517 | content: "\e165"; 518 | } 519 | .glyphicon-save:before { 520 | content: "\e166"; 521 | } 522 | .glyphicon-open:before { 523 | content: "\e167"; 524 | } 525 | .glyphicon-saved:before { 526 | content: "\e168"; 527 | } 528 | .glyphicon-import:before { 529 | content: "\e169"; 530 | } 531 | .glyphicon-export:before { 532 | content: "\e170"; 533 | } 534 | .glyphicon-send:before { 535 | content: "\e171"; 536 | } 537 | .glyphicon-floppy-disk:before { 538 | content: "\e172"; 539 | } 540 | .glyphicon-floppy-saved:before { 541 | content: "\e173"; 542 | } 543 | .glyphicon-floppy-remove:before { 544 | content: "\e174"; 545 | } 546 | .glyphicon-floppy-save:before { 547 | content: "\e175"; 548 | } 549 | .glyphicon-floppy-open:before { 550 | content: "\e176"; 551 | } 552 | .glyphicon-credit-card:before { 553 | content: "\e177"; 554 | } 555 | .glyphicon-transfer:before { 556 | content: "\e178"; 557 | } 558 | .glyphicon-cutlery:before { 559 | content: "\e179"; 560 | } 561 | .glyphicon-header:before { 562 | content: "\e180"; 563 | } 564 | .glyphicon-compressed:before { 565 | content: "\e181"; 566 | } 567 | .glyphicon-earphone:before { 568 | content: "\e182"; 569 | } 570 | .glyphicon-phone-alt:before { 571 | content: "\e183"; 572 | } 573 | .glyphicon-tower:before { 574 | content: "\e184"; 575 | } 576 | .glyphicon-stats:before { 577 | content: "\e185"; 578 | } 579 | .glyphicon-sd-video:before { 580 | content: "\e186"; 581 | } 582 | .glyphicon-hd-video:before { 583 | content: "\e187"; 584 | } 585 | .glyphicon-subtitles:before { 586 | content: "\e188"; 587 | } 588 | .glyphicon-sound-stereo:before { 589 | content: "\e189"; 590 | } 591 | .glyphicon-sound-dolby:before { 592 | content: "\e190"; 593 | } 594 | .glyphicon-sound-5-1:before { 595 | content: "\e191"; 596 | } 597 | .glyphicon-sound-6-1:before { 598 | content: "\e192"; 599 | } 600 | .glyphicon-sound-7-1:before { 601 | content: "\e193"; 602 | } 603 | .glyphicon-copyright-mark:before { 604 | content: "\e194"; 605 | } 606 | .glyphicon-registration-mark:before { 607 | content: "\e195"; 608 | } 609 | .glyphicon-cloud-download:before { 610 | content: "\e197"; 611 | } 612 | .glyphicon-cloud-upload:before { 613 | content: "\e198"; 614 | } 615 | .glyphicon-tree-conifer:before { 616 | content: "\e199"; 617 | } 618 | .glyphicon-tree-deciduous:before { 619 | content: "\e200"; 620 | } 621 | .glyphicon-cd:before { 622 | content: "\e201"; 623 | } 624 | .glyphicon-save-file:before { 625 | content: "\e202"; 626 | } 627 | .glyphicon-open-file:before { 628 | content: "\e203"; 629 | } 630 | .glyphicon-level-up:before { 631 | content: "\e204"; 632 | } 633 | .glyphicon-copy:before { 634 | content: "\e205"; 635 | } 636 | .glyphicon-paste:before { 637 | content: "\e206"; 638 | } 639 | .glyphicon-alert:before { 640 | content: "\e209"; 641 | } 642 | .glyphicon-equalizer:before { 643 | content: "\e210"; 644 | } 645 | .glyphicon-king:before { 646 | content: "\e211"; 647 | } 648 | .glyphicon-queen:before { 649 | content: "\e212"; 650 | } 651 | .glyphicon-pawn:before { 652 | content: "\e213"; 653 | } 654 | .glyphicon-bishop:before { 655 | content: "\e214"; 656 | } 657 | .glyphicon-knight:before { 658 | content: "\e215"; 659 | } 660 | .glyphicon-baby-formula:before { 661 | content: "\e216"; 662 | } 663 | .glyphicon-tent:before { 664 | content: "\26fa"; 665 | } 666 | .glyphicon-blackboard:before { 667 | content: "\e218"; 668 | } 669 | .glyphicon-bed:before { 670 | content: "\e219"; 671 | } 672 | .glyphicon-apple:before { 673 | content: "\f8ff"; 674 | } 675 | .glyphicon-erase:before { 676 | content: "\e221"; 677 | } 678 | .glyphicon-hourglass:before { 679 | content: "\231b"; 680 | } 681 | .glyphicon-lamp:before { 682 | content: "\e223"; 683 | } 684 | .glyphicon-duplicate:before { 685 | content: "\e224"; 686 | } 687 | .glyphicon-piggy-bank:before { 688 | content: "\e225"; 689 | } 690 | .glyphicon-scissors:before { 691 | content: "\e226"; 692 | } 693 | .glyphicon-bitcoin:before { 694 | content: "\e227"; 695 | } 696 | .glyphicon-btc:before { 697 | content: "\e227"; 698 | } 699 | .glyphicon-xbt:before { 700 | content: "\e227"; 701 | } 702 | .glyphicon-yen:before { 703 | content: "\00a5"; 704 | } 705 | .glyphicon-jpy:before { 706 | content: "\00a5"; 707 | } 708 | .glyphicon-ruble:before { 709 | content: "\20bd"; 710 | } 711 | .glyphicon-rub:before { 712 | content: "\20bd"; 713 | } 714 | .glyphicon-scale:before { 715 | content: "\e230"; 716 | } 717 | .glyphicon-ice-lolly:before { 718 | content: "\e231"; 719 | } 720 | .glyphicon-ice-lolly-tasted:before { 721 | content: "\e232"; 722 | } 723 | .glyphicon-education:before { 724 | content: "\e233"; 725 | } 726 | .glyphicon-option-horizontal:before { 727 | content: "\e234"; 728 | } 729 | .glyphicon-option-vertical:before { 730 | content: "\e235"; 731 | } 732 | .glyphicon-menu-hamburger:before { 733 | content: "\e236"; 734 | } 735 | .glyphicon-modal-window:before { 736 | content: "\e237"; 737 | } 738 | .glyphicon-oil:before { 739 | content: "\e238"; 740 | } 741 | .glyphicon-grain:before { 742 | content: "\e239"; 743 | } 744 | .glyphicon-sunglasses:before { 745 | content: "\e240"; 746 | } 747 | .glyphicon-text-size:before { 748 | content: "\e241"; 749 | } 750 | .glyphicon-text-color:before { 751 | content: "\e242"; 752 | } 753 | .glyphicon-text-background:before { 754 | content: "\e243"; 755 | } 756 | .glyphicon-object-align-top:before { 757 | content: "\e244"; 758 | } 759 | .glyphicon-object-align-bottom:before { 760 | content: "\e245"; 761 | } 762 | .glyphicon-object-align-horizontal:before { 763 | content: "\e246"; 764 | } 765 | .glyphicon-object-align-left:before { 766 | content: "\e247"; 767 | } 768 | .glyphicon-object-align-vertical:before { 769 | content: "\e248"; 770 | } 771 | .glyphicon-object-align-right:before { 772 | content: "\e249"; 773 | } 774 | .glyphicon-triangle-right:before { 775 | content: "\e250"; 776 | } 777 | .glyphicon-triangle-left:before { 778 | content: "\e251"; 779 | } 780 | .glyphicon-triangle-bottom:before { 781 | content: "\e252"; 782 | } 783 | .glyphicon-triangle-top:before { 784 | content: "\e253"; 785 | } 786 | .glyphicon-console:before { 787 | content: "\e254"; 788 | } 789 | .glyphicon-superscript:before { 790 | content: "\e255"; 791 | } 792 | .glyphicon-subscript:before { 793 | content: "\e256"; 794 | } 795 | .glyphicon-menu-left:before { 796 | content: "\e257"; 797 | } 798 | .glyphicon-menu-right:before { 799 | content: "\e258"; 800 | } 801 | .glyphicon-menu-down:before { 802 | content: "\e259"; 803 | } 804 | .glyphicon-menu-up:before { 805 | content: "\e260"; 806 | } 807 | 808 | } 809 | -------------------------------------------------------------------------------- /app/assets/fonts/glyphicons/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------