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