├── .babelrc ├── .gitignore ├── README.md ├── app.js ├── config └── config.json ├── db_models.js ├── package.json ├── public ├── bootstrap │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── css │ └── app.css ├── index.html └── src │ ├── .babelrc │ ├── actions │ ├── index.js │ └── types.js │ ├── components │ ├── add_invoice │ │ ├── add_invoice.js │ │ └── add_invoice.test.js │ ├── add_invoice_item │ │ ├── add_invoice_item.js │ │ └── add_invoice_item.test.js │ ├── app │ │ ├── app.js │ │ └── app.test.js │ ├── create_customer │ │ ├── create_customer.js │ │ └── create_customer.test.js │ ├── customer_list │ │ ├── customer_list.js │ │ └── customer_list.test.js │ ├── form_field │ │ ├── form_field.js │ │ └── form_field.test.js │ ├── header │ │ ├── header.js │ │ └── header.test.js │ ├── invoice_list │ │ ├── invoice_list.js │ │ └── invoice_list.test.js │ ├── nav │ │ ├── nav.js │ │ └── nav.test.js │ ├── product_list │ │ ├── product_list.js │ │ └── product_list.test.js │ ├── select_customer │ │ ├── select_customer.js │ │ └── select_customer.test.js │ └── test_helpers.js │ ├── index.js │ ├── pages │ ├── add_invoice.js │ ├── create_invoice_customer.js │ ├── customers.js │ ├── invoices.js │ ├── products.js │ └── set_invoice_customer.js │ └── reducers │ ├── current_invoice.js │ ├── customers.js │ ├── index.js │ ├── invoice_items.js │ ├── invoices.js │ ├── product.js │ ├── products.js │ └── selected_customer.js ├── seed_database.js ├── testConfig ├── fileMock.js ├── jest.config.json └── styleMock.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | invoices.sqlite 3 | public/bundle.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | This project began as part of a coding interview. The challenge was to create an invoice application given a set of framework-agnostic starter files. I decided to go with React. I thought the challenge was interesting enough that I continued working on it after the interview. 4 | 5 | # Dependencies 6 | 7 | - sqlite3 8 | - node 9 | - npm 10 | 11 | # Getting Started 12 | 13 | ###### Seed database with data 14 | `node seed_database` 15 | 16 | ###### Install npm dependencies 17 | `npm install` 18 | 19 | ###### Run the node server 20 | `npm start` 21 | 22 | ###### Viewing the application in your browser 23 | `http://localhost:8000` 24 | 25 | # SQLite Schema 26 | 27 | ## Customers 28 | 29 | - id (integer) 30 | - name (string) 31 | - address (string) 32 | - phone (string) 33 | 34 | 35 | ## Products 36 | 37 | - id (integer) 38 | - name (string) 39 | - price (decimal) 40 | 41 | ## Invoices 42 | 43 | - id (integer) 44 | - customer_id (integer) 45 | - discount (decimal) 46 | - total (decimal) 47 | 48 | ## InvoiceItems 49 | 50 | - id (integer) 51 | - invoice_id (integer) 52 | - product_id (integer) 53 | - quantity (decimal) 54 | 55 | 56 | # API Endpoints 57 | 58 | ## Customers 59 | ``` 60 | GET|POST /api/customers 61 | GET|PUT|DELETE /api/customers/{id} 62 | ``` 63 | 64 | ## Products 65 | ``` 66 | GET|POST /api/products 67 | GET|PUT|DELETE /api/products/{id} 68 | ``` 69 | ## Invoices 70 | ``` 71 | GET|POST /api/invoices 72 | GET|PUT|DELETE /api/invoices/{id} 73 | ``` 74 | 75 | ## InvoiceItems 76 | ``` 77 | GET|POST /api/invoices/{id}/items 78 | GET|PUT|DELETE /api/invoices/{invoice_id}/items/{id} 79 | ``` 80 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | bodyParser = require('body-parser'), 3 | http = require('http'), 4 | path = require('path'), 5 | cors = require('cors'), 6 | Sequelize = require('sequelize'), 7 | _ = require('lodash'), 8 | db_models = require('./db_models'); 9 | 10 | var app = module.exports = express(); 11 | app.set('port', process.env.PORT || 8000); 12 | app.use(bodyParser.json()); 13 | app.use(bodyParser.urlencoded({ extended: true })); 14 | app.use(express.static(path.join(__dirname, 'public'))); 15 | app.use(cors()); 16 | 17 | // CUSTOMERS API 18 | 19 | app.route('/api/customers') 20 | .get(function(req, res) { 21 | Customer.findAll().then(function(customers) { 22 | res.json(customers); 23 | }) 24 | }) 25 | .post(function(req, res) { 26 | var customer = Customer.build(_.pick(req.body, ['name', 'address', 'phone'])); 27 | customer.save().then(function(customer){ 28 | res.json(customer); 29 | }); 30 | }); 31 | 32 | app.route('/api/customers/:customer_id') 33 | .get(function(req, res) { 34 | Customer.findById(req.params.customer_id).then(function(customer) { 35 | res.json(customer); 36 | }); 37 | }) 38 | .put(function(req, res) { 39 | Customer.findById(req.params.customer_id).then(function(customer) { 40 | customer.update(_.pick(req.body, ['name', 'address', 'phone'])).then(function(customer) { 41 | res.json(customer); 42 | }); 43 | }); 44 | }) 45 | .delete(function(req, res) { 46 | Customer.findById(req.params.customer_id).then(function(customer) { 47 | customer.destroy().then(function(customer) { 48 | res.json(customer); 49 | }); 50 | }); 51 | }); 52 | 53 | // PRODUCTS API 54 | 55 | app.route('/api/products') 56 | .get(function(req, res) { 57 | Product.findAll().then(function(products) { 58 | res.json(products); 59 | }) 60 | }) 61 | .post(function(req, res) { 62 | var product = Product.build(_.pick(req.body, ['name', 'price'])); 63 | product.save().then(function(product){ 64 | res.json(product); 65 | }); 66 | }); 67 | 68 | app.route('/api/products/:product_id') 69 | .get(function(req, res) { 70 | Product.findById(req.params.product_id).then(function(product) { 71 | res.json(product); 72 | }); 73 | }) 74 | .put(function(req, res) { 75 | Product.findById(req.params.product_id).then(function(product) { 76 | product.update(_.pick(req.body, ['name', 'price'])).then(function(product) { 77 | res.json(product); 78 | }); 79 | }); 80 | }) 81 | .delete(function(req, res) { 82 | Product.findById(req.params.product_id).then(function(product) { 83 | product.destroy().then(function(product) { 84 | res.json(product); 85 | }); 86 | }); 87 | }); 88 | 89 | 90 | // INVOICES API 91 | 92 | app.route('/api/invoices') 93 | .get(function(req, res) { 94 | Invoice.findAll().then(function(invoices) { 95 | res.json(invoices); 96 | }) 97 | }) 98 | .post(function(req, res) { 99 | var invoice = Invoice.build(_.pick(req.body, ['customer_id', 'discount', 'total'])); 100 | invoice.save().then(function(invoice){ 101 | res.json(invoice); 102 | }); 103 | }); 104 | 105 | app.route('/api/invoices/:invoice_id') 106 | .get(function(req, res) { 107 | Invoice.findById(req.params.invoice_id).then(function(invoice) { 108 | res.json(invoice); 109 | }); 110 | }) 111 | .put(function(req, res) { 112 | Invoice.findById(req.params.invoice_id).then(function(invoice) { 113 | invoice.update(_.pick(req.body, ['customer_id', 'discount', 'total'])).then(function(invoice) { 114 | console.log('------TEST--------'); 115 | console.log(req.body); 116 | console.log(invoice.total, invoice.discount); 117 | console.log('------TEST--------'); 118 | res.json(invoice); 119 | }); 120 | }); 121 | }) 122 | .delete(function(req, res) { 123 | Invoice.findById(req.params.invoice_id).then(function(invoice) { 124 | invoice.destroy().then(function(invoice) { 125 | res.json(invoice); 126 | }); 127 | }); 128 | }); 129 | 130 | 131 | // INVOICE ITEMS API 132 | 133 | app.route('/api/invoices/:invoice_id/items') 134 | .get(function(req, res) { 135 | InvoiceItem.findAll({where: { invoice_id: req.params.invoice_id }}).then(function(invoice_items) { 136 | res.json(invoice_items); 137 | }) 138 | }) 139 | .post(function(req, res) { 140 | var invoice_item = InvoiceItem.build(_.pick(req.body, ['product_id', 'quantity'])); 141 | invoice_item.set('invoice_id', req.params.invoice_id); 142 | invoice_item.save().then(function(invoice_item){ 143 | res.json(invoice_item); 144 | }); 145 | }); 146 | 147 | app.route('/api/invoices/:invoice_id/items/:id') 148 | .get(function(req, res) { 149 | InvoiceItem.findById(req.params.id).then(function(invoice_item) { 150 | res.json(invoice_item); 151 | }); 152 | }) 153 | .put(function(req, res) { 154 | InvoiceItem.findById(req.params.id).then(function(invoice_item) { 155 | invoice_item.update(_.pick(req.body, ['product_id', 'quantity'])).then(function(invoice_item) { 156 | res.json(invoice_item); 157 | }); 158 | }); 159 | }) 160 | .delete(function(req, res) { 161 | InvoiceItem.findById(req.params.id).then(function(invoice_item) { 162 | invoice_item.destroy().then(function(invoice_item) { 163 | res.json(invoice_item); 164 | }); 165 | }); 166 | }); 167 | 168 | 169 | // Redirect all non api requests to the index 170 | app.get('*', function(req, res) { 171 | res.sendFile(path.join(__dirname, 'public', 'index.html')); 172 | }); 173 | 174 | // Starting express server 175 | http.createServer(app).listen(app.get('port'), function () { 176 | console.log('Express server listening on port ' + app.get('port')); 177 | }); 178 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "dialect": "sqlite", 4 | "storage": "./db.development.sqlite" 5 | } 6 | } -------------------------------------------------------------------------------- /db_models.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | Sequelize = require('sequelize'); 3 | 4 | sequelize = new Sequelize('sqlite://' + path.join(__dirname, 'invoices.sqlite'), { 5 | dialect: 'sqlite', 6 | storage: path.join(__dirname, 'invoices.sqlite') 7 | }); 8 | 9 | Customer = sequelize.define('customers', { 10 | id: { 11 | type: Sequelize.INTEGER, 12 | primaryKey: true, 13 | autoIncrement: true 14 | }, 15 | name: { 16 | type: Sequelize.STRING 17 | }, 18 | address: { 19 | type: Sequelize.STRING 20 | }, 21 | phone: { 22 | type: Sequelize.STRING 23 | } 24 | }); 25 | 26 | Product = sequelize.define('products', { 27 | id: { 28 | type: Sequelize.INTEGER, 29 | primaryKey: true, 30 | autoIncrement: true 31 | }, 32 | name: { 33 | type: Sequelize.STRING 34 | }, 35 | price: { 36 | type: Sequelize.DECIMAL 37 | } 38 | }); 39 | 40 | Invoice = sequelize.define('invoices', { 41 | id: { 42 | type: Sequelize.INTEGER, 43 | primaryKey: true, 44 | autoIncrement: true 45 | }, 46 | customer_id: { 47 | type: Sequelize.INTEGER 48 | }, 49 | discount: { 50 | type: Sequelize.TEXT 51 | }, 52 | total: { 53 | type: Sequelize.TEXT 54 | } 55 | }); 56 | 57 | InvoiceItem = sequelize.define('invoice_items', { 58 | id: { 59 | type: Sequelize.INTEGER, 60 | primaryKey: true, 61 | autoIncrement: true 62 | }, 63 | invoice_id: { 64 | type: Sequelize.INTEGER 65 | }, 66 | product_id: { 67 | type: Sequelize.INTEGER 68 | }, 69 | quantity: { 70 | type: Sequelize.DECIMAL 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interviewed-angular", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app & webpack --progress --colors --watch", 7 | "seed": "node seed_database", 8 | "test": "jest --config=testConfig/jest.config.json", 9 | "test:watch": "jest --config=testConfig/jest.config.json --watch" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.15.3", 13 | "babel-preset-stage-1": "^6.1.18", 14 | "body-parser": "1.15.2", 15 | "cors": "2.8.1", 16 | "enzyme": "^2.8.2", 17 | "express": "~4.13.4", 18 | "jest": "^19.0.2", 19 | "lodash": "^3.10.1", 20 | "react": "^0.14.3", 21 | "react-dom": "^0.14.3", 22 | "react-redux": "^4.0.0", 23 | "react-router": "^2.0.1", 24 | "react-router-dom": "^4.0.0", 25 | "redux": "^3.0.4", 26 | "redux-form": "^6.6.1", 27 | "redux-promise": "^0.5.3", 28 | "sequelize": "^3.20.0", 29 | "sqlite3": "latest" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.2.1", 33 | "babel-loader": "^6.2.0", 34 | "babel-preset-es2015": "^6.1.18", 35 | "babel-preset-react": "^6.1.18", 36 | "chai": "^3.5.0", 37 | "chai-jquery": "^2.0.0", 38 | "jquery": "^2.2.1", 39 | "jsdom": "^8.1.0", 40 | "mocha": "^2.4.5", 41 | "react-addons-test-utils": "^0.14.7", 42 | "webpack": "^1.12.9", 43 | "webpack-dev-server": "^1.14.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/bootstrap/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 16 | } 17 | 18 | .btn-default:active, 19 | .btn-primary:active, 20 | .btn-success:active, 21 | .btn-info:active, 22 | .btn-warning:active, 23 | .btn-danger:active, 24 | .btn-default.active, 25 | .btn-primary.active, 26 | .btn-success.active, 27 | .btn-info.active, 28 | .btn-warning.active, 29 | .btn-danger.active { 30 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 31 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 32 | } 33 | 34 | .btn:active, 35 | .btn.active { 36 | background-image: none; 37 | } 38 | 39 | .btn-default { 40 | text-shadow: 0 1px 0 #fff; 41 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); 42 | background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%); 43 | background-repeat: repeat-x; 44 | border-color: #dbdbdb; 45 | border-color: #ccc; 46 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 47 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 48 | } 49 | 50 | .btn-default:hover, 51 | .btn-default:focus { 52 | background-color: #e0e0e0; 53 | background-position: 0 -15px; 54 | } 55 | 56 | .btn-default:active, 57 | .btn-default.active { 58 | background-color: #e0e0e0; 59 | border-color: #dbdbdb; 60 | } 61 | 62 | .btn-primary { 63 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 64 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 65 | background-repeat: repeat-x; 66 | border-color: #2b669a; 67 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 68 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 69 | } 70 | 71 | .btn-primary:hover, 72 | .btn-primary:focus { 73 | background-color: #2d6ca2; 74 | background-position: 0 -15px; 75 | } 76 | 77 | .btn-primary:active, 78 | .btn-primary.active { 79 | background-color: #2d6ca2; 80 | border-color: #2b669a; 81 | } 82 | 83 | .btn-success { 84 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 85 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 86 | background-repeat: repeat-x; 87 | border-color: #3e8f3e; 88 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 89 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 90 | } 91 | 92 | .btn-success:hover, 93 | .btn-success:focus { 94 | background-color: #419641; 95 | background-position: 0 -15px; 96 | } 97 | 98 | .btn-success:active, 99 | .btn-success.active { 100 | background-color: #419641; 101 | border-color: #3e8f3e; 102 | } 103 | 104 | .btn-warning { 105 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 106 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 107 | background-repeat: repeat-x; 108 | border-color: #e38d13; 109 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 110 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 111 | } 112 | 113 | .btn-warning:hover, 114 | .btn-warning:focus { 115 | background-color: #eb9316; 116 | background-position: 0 -15px; 117 | } 118 | 119 | .btn-warning:active, 120 | .btn-warning.active { 121 | background-color: #eb9316; 122 | border-color: #e38d13; 123 | } 124 | 125 | .btn-danger { 126 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 127 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 128 | background-repeat: repeat-x; 129 | border-color: #b92c28; 130 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 131 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 132 | } 133 | 134 | .btn-danger:hover, 135 | .btn-danger:focus { 136 | background-color: #c12e2a; 137 | background-position: 0 -15px; 138 | } 139 | 140 | .btn-danger:active, 141 | .btn-danger.active { 142 | background-color: #c12e2a; 143 | border-color: #b92c28; 144 | } 145 | 146 | .btn-info { 147 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 148 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 149 | background-repeat: repeat-x; 150 | border-color: #28a4c9; 151 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 152 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 153 | } 154 | 155 | .btn-info:hover, 156 | .btn-info:focus { 157 | background-color: #2aabd2; 158 | background-position: 0 -15px; 159 | } 160 | 161 | .btn-info:active, 162 | .btn-info.active { 163 | background-color: #2aabd2; 164 | border-color: #28a4c9; 165 | } 166 | 167 | .thumbnail, 168 | .img-thumbnail { 169 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 170 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 171 | } 172 | 173 | .dropdown-menu > li > a:hover, 174 | .dropdown-menu > li > a:focus { 175 | background-color: #e8e8e8; 176 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 177 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 178 | background-repeat: repeat-x; 179 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 180 | } 181 | 182 | .dropdown-menu > .active > a, 183 | .dropdown-menu > .active > a:hover, 184 | .dropdown-menu > .active > a:focus { 185 | background-color: #357ebd; 186 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 187 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 188 | background-repeat: repeat-x; 189 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 190 | } 191 | 192 | .navbar-default { 193 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); 194 | background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); 195 | background-repeat: repeat-x; 196 | border-radius: 4px; 197 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 198 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 199 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 200 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 201 | } 202 | 203 | .navbar-default .navbar-nav > .active > a { 204 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 205 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); 206 | background-repeat: repeat-x; 207 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); 208 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); 209 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); 210 | } 211 | 212 | .navbar-brand, 213 | .navbar-nav > li > a { 214 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); 215 | } 216 | 217 | .navbar-inverse { 218 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%); 219 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%); 220 | background-repeat: repeat-x; 221 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 222 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); 223 | } 224 | 225 | .navbar-inverse .navbar-nav > .active > a { 226 | background-image: -webkit-linear-gradient(top, #222222 0%, #282828 100%); 227 | background-image: linear-gradient(to bottom, #222222 0%, #282828 100%); 228 | background-repeat: repeat-x; 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); 230 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); 231 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); 232 | } 233 | 234 | .navbar-inverse .navbar-brand, 235 | .navbar-inverse .navbar-nav > li > a { 236 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 237 | } 238 | 239 | .navbar-static-top, 240 | .navbar-fixed-top, 241 | .navbar-fixed-bottom { 242 | border-radius: 0; 243 | } 244 | 245 | .alert { 246 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); 247 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 248 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 249 | } 250 | 251 | .alert-success { 252 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 253 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 254 | background-repeat: repeat-x; 255 | border-color: #b2dba1; 256 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 257 | } 258 | 259 | .alert-info { 260 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 261 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 262 | background-repeat: repeat-x; 263 | border-color: #9acfea; 264 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 265 | } 266 | 267 | .alert-warning { 268 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 269 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 270 | background-repeat: repeat-x; 271 | border-color: #f5e79e; 272 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 273 | } 274 | 275 | .alert-danger { 276 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 277 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 278 | background-repeat: repeat-x; 279 | border-color: #dca7a7; 280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 281 | } 282 | 283 | .progress { 284 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 285 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 286 | background-repeat: repeat-x; 287 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 288 | } 289 | 290 | .progress-bar { 291 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 292 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 293 | background-repeat: repeat-x; 294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 295 | } 296 | 297 | .progress-bar-success { 298 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 299 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 300 | background-repeat: repeat-x; 301 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 302 | } 303 | 304 | .progress-bar-info { 305 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 306 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 307 | background-repeat: repeat-x; 308 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 309 | } 310 | 311 | .progress-bar-warning { 312 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 313 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 314 | background-repeat: repeat-x; 315 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 316 | } 317 | 318 | .progress-bar-danger { 319 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 320 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 321 | background-repeat: repeat-x; 322 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 323 | } 324 | 325 | .list-group { 326 | border-radius: 4px; 327 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 328 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 329 | } 330 | 331 | .list-group-item.active, 332 | .list-group-item.active:hover, 333 | .list-group-item.active:focus { 334 | text-shadow: 0 -1px 0 #3071a9; 335 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 336 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 337 | background-repeat: repeat-x; 338 | border-color: #3278b3; 339 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 340 | } 341 | 342 | .panel { 343 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 344 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 345 | } 346 | 347 | .panel-default > .panel-heading { 348 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 349 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 350 | background-repeat: repeat-x; 351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 352 | } 353 | 354 | .panel-primary > .panel-heading { 355 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 356 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 357 | background-repeat: repeat-x; 358 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 359 | } 360 | 361 | .panel-success > .panel-heading { 362 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 363 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 364 | background-repeat: repeat-x; 365 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 366 | } 367 | 368 | .panel-info > .panel-heading { 369 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 370 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 371 | background-repeat: repeat-x; 372 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 373 | } 374 | 375 | .panel-warning > .panel-heading { 376 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 377 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 378 | background-repeat: repeat-x; 379 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 380 | } 381 | 382 | .panel-danger > .panel-heading { 383 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 384 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 385 | background-repeat: repeat-x; 386 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 387 | } 388 | 389 | .well { 390 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 391 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 392 | background-repeat: repeat-x; 393 | border-color: #dcdcdc; 394 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 395 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 396 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 397 | } -------------------------------------------------------------------------------- /public/bootstrap/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /public/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdotson/react-invoice-app/2960900e7696df2598481d8191fcd69e5fe2c525/public/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/bootstrap/fonts/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 | -------------------------------------------------------------------------------- /public/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdotson/react-invoice-app/2960900e7696df2598481d8191fcd69e5fe2c525/public/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdotson/react-invoice-app/2960900e7696df2598481d8191fcd69e5fe2c525/public/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | .page-header h1 { 2 | text-transform: capitalize; 3 | } 4 | 5 | .error { 6 | color: #a94442; 7 | } 8 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /public/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router'; 3 | 4 | import { 5 | FETCH_INVOICES, 6 | ADD_INVOICE, 7 | UPDATE_INVOICE, 8 | ADD_INVOICE_ITEM, 9 | FETCH_CUSTOMERS, 10 | SELECT_CUSTOMER, 11 | CREATE_CUSTOMER, 12 | SELECT_INVOICE_CUSTOMER, 13 | FETCH_PRODUCTS, 14 | SET_PRODUCT 15 | } from './types'; 16 | 17 | const ROOT_URL = 'http://localhost:8000/api/'; 18 | 19 | export function fetchInvoices() { 20 | const request = axios({ 21 | method: 'get', 22 | url: `${ROOT_URL}invoices` 23 | }); 24 | 25 | return { 26 | type: FETCH_INVOICES, 27 | payload: request 28 | }; 29 | } 30 | 31 | export function addInvoice(customerID) { 32 | const request = axios({ 33 | method: 'post', 34 | url: `${ROOT_URL}invoices`, 35 | data: { 36 | customer_id: customerID 37 | } 38 | }); 39 | 40 | browserHistory.push('/add-invoice/items'); 41 | 42 | return { 43 | type: ADD_INVOICE, 44 | payload: request 45 | }; 46 | } 47 | 48 | export function updateInvoice({ id, customer_id, discount, total }) { 49 | const request = axios({ 50 | method: 'put', 51 | url: `${ROOT_URL}invoices/${id}`, 52 | data: { 53 | id, 54 | customer_id, 55 | discount, 56 | total 57 | } 58 | }); 59 | 60 | return { 61 | type: UPDATE_INVOICE, 62 | payload: request 63 | }; 64 | } 65 | 66 | export function fetchCustomers() { 67 | const request = axios({ 68 | method: 'get', 69 | url: `${ROOT_URL}customers` 70 | }); 71 | 72 | return { 73 | type: FETCH_CUSTOMERS, 74 | payload: request 75 | }; 76 | } 77 | 78 | export function selectCustomer(id) { 79 | const request = axios({ 80 | method: 'get', 81 | url: `${ROOT_URL}customers/${id}` 82 | }); 83 | 84 | browserHistory.push('/add-invoice/items'); 85 | 86 | return { 87 | type: SELECT_CUSTOMER, 88 | payload: request 89 | }; 90 | } 91 | 92 | export function createCustomer(data) { 93 | console.log('createCustomer', data); 94 | const request = axios({ 95 | method: 'post', 96 | url: `${ROOT_URL}customers`, 97 | data: data 98 | }); 99 | 100 | browserHistory.push('/add-invoice/items'); 101 | 102 | return { 103 | type: CREATE_CUSTOMER, 104 | payload: request 105 | }; 106 | } 107 | 108 | export function fetchProducts() { 109 | const request = axios({ 110 | method: 'get', 111 | url: `${ROOT_URL}products` 112 | }); 113 | 114 | return { 115 | type: FETCH_PRODUCTS, 116 | payload: request 117 | }; 118 | } 119 | 120 | export function setProduct(productID) { 121 | const request = axios({ 122 | method: 'get', 123 | url: `${ROOT_URL}products/${productID}` 124 | }); 125 | 126 | return { 127 | type: SET_PRODUCT, 128 | payload: request 129 | }; 130 | } 131 | 132 | export function addInvoiceItem(id, item, customer) { 133 | console.log('add invoice item form', item, customer); 134 | const request = axios.all([axios({ 135 | method: 'post', 136 | url: `${ROOT_URL}invoices/${id}/items`, 137 | data: { 138 | invoice_id: id, 139 | product_id: item.product_id, 140 | quantity: item.quantity 141 | } 142 | }), axios({ 143 | method: 'put', 144 | url: `${ROOT_URL}invoices/${id}`, 145 | data: { 146 | id: id, 147 | customer_id: customer.id, 148 | discount: customer.discount, 149 | total: customer.total 150 | } 151 | }) 152 | ]); 153 | 154 | return { 155 | type: ADD_INVOICE_ITEM, 156 | payload: request 157 | }; 158 | } 159 | -------------------------------------------------------------------------------- /public/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const FETCH_INVOICES = 'fetch_invoices'; 2 | 3 | export const ADD_INVOICE = 'add_invoice'; 4 | export const UPDATE_INVOICE = 'update_invoice'; 5 | 6 | export const FETCH_CUSTOMERS = 'fetch_customers'; 7 | export const SELECT_CUSTOMER = 'select_customer'; 8 | export const CREATE_CUSTOMER = 'create_customer'; 9 | export const SELECT_INVOICE_CUSTOMER = 'select_invoice_customer'; 10 | 11 | export const FETCH_PRODUCTS = 'fetch_products'; 12 | export const SET_PRODUCT = 'set_product'; 13 | 14 | export const ADD_INVOICE_ITEM = 'add_invoice_item'; 15 | 16 | export const FORM_ERROR = 'form_error'; 17 | -------------------------------------------------------------------------------- /public/src/components/add_invoice/add_invoice.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Field, reduxForm } from 'redux-form'; 4 | 5 | import * as actions from '../../actions'; 6 | import AddInvoiceItemForm from '../add_invoice_item/add_invoice_item'; 7 | import FormField from '../form_field/form_field'; 8 | 9 | class AddInvoice extends Component { 10 | componentDidMount() { 11 | this.props.addInvoice(this.props.selectedCustomer.id); 12 | } 13 | 14 | handleDiscountChange(event) { 15 | this.props.updateInvoice({ 16 | discount: event.target.value, 17 | customer_id: this.props.selectedCustomer.id, 18 | id: this.props.currentInvoice.id, 19 | total: this.props.currentInvoice.total 20 | }); 21 | } 22 | 23 | render() { 24 | const { handleSubmit, products, selectedCustomer, currentInvoice, invoiceItems } = this.props; 25 | return ( 26 |
27 |

Invoice #{currentInvoice.id}
28 | {selectedCustomer.name}
29 | {selectedCustomer.address}

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {invoiceItems.map((item) => { 42 | let itemProduct = products.find(product => product.id == item.product_id); 43 | return ( 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 |
ProductQuantityPriceAction
{itemProduct.name}{item.quantity}${itemProduct.price * item.quantity}
Subtotal:${currentInvoice.total.toFixed(2)}
Discount:{currentInvoice.discount}%
Total:${(currentInvoice.total * (1 - currentInvoice.discount/100)).toFixed(2)}
76 |
77 | ); 78 | } 79 | } 80 | 81 | function validate(values) { 82 | const errors ={}; 83 | 84 | if (values.discount < 0 || values.discount > 100 || (values.discount && !values.discount.match(/[0-9]+/))) { 85 | errors.discount = "Discount must be a number between 0 and 100"; 86 | } 87 | 88 | return errors; 89 | } 90 | 91 | AddInvoice = reduxForm({ 92 | form: 'addinvoice', 93 | validate 94 | })(AddInvoice); 95 | 96 | function mapStateToProps(state) { 97 | return { 98 | products: state.products, 99 | selectedCustomer: state.selectedCustomer, 100 | currentInvoice: state.currentInvoice, 101 | invoiceItems: state.invoiceItems 102 | }; 103 | } 104 | 105 | export default connect(mapStateToProps, actions)(AddInvoice); 106 | -------------------------------------------------------------------------------- /public/src/components/add_invoice/add_invoice.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import mockFormStore from '../test_helpers' 6 | import { AddInvoice } from './add_invoice'; 7 | 8 | describe('Component: AddInvoice', () => { 9 | const props = {}; 10 | const store = mockFormStore(); 11 | props.location = {}; 12 | props.pathName = ''; 13 | 14 | it('renders without crashing', () => { 15 | const wrapper = shallow(); 16 | 17 | expect(wrapper).toHaveLength(1); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /public/src/components/add_invoice_item/add_invoice_item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Field, reduxForm } from 'redux-form'; 4 | 5 | import * as actions from '../../actions'; 6 | import FormField from '../form_field/form_field'; 7 | 8 | class AddInvoiceItem extends Component { 9 | componentWillMount() { 10 | this.props.fetchProducts(); 11 | this.props.setProduct(1); 12 | } 13 | 14 | handleFormSubmit(event) { 15 | this.props.setProduct(event.target.value); 16 | } 17 | 18 | addNewItem(values) { 19 | let currTotal = this.props.invoice.total || 0, 20 | addedExpense = this.props.currProduct.price * values.quantity, 21 | newTotal = currTotal + addedExpense; 22 | const invoiceID = this.props.invoice.id, 23 | newItem = { 24 | invoice_id: invoiceID, 25 | product_id: values.product || 1, 26 | quantity: values.quantity 27 | }, 28 | customer = { 29 | total: newTotal, 30 | discount: this.props.invoice.discount, 31 | id: this.props.customer.id 32 | }; 33 | 34 | this.props.addInvoiceItem(invoiceID, newItem, customer); 35 | } 36 | 37 | render() { 38 | const { currProduct, products, handleSubmit } = this.props; 39 | return ( 40 | 41 | 42 | 43 | {products.map(product => 44 | )} 45 | 46 | 47 | 48 | 49 | 50 | 51 | ${currProduct.price} 52 | 53 | 54 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | function validate(values) { 64 | console.log(values); 65 | const errors = {}; 66 | 67 | if (values.quantity && !values.quantity.match(/[1-9]+/)) { 68 | errors.quantity = "Quantity must be a number greater than 1"; 69 | } 70 | 71 | return errors; 72 | } 73 | 74 | AddInvoiceItem = reduxForm({ 75 | form: 'addinvoiceitem', 76 | validate 77 | })(AddInvoiceItem); 78 | 79 | function mapStateToProps(state) { 80 | return { 81 | currProduct: state.product, 82 | products: state.products, 83 | customer: state.selectedCustomer, 84 | invoice: state.currentInvoice 85 | }; 86 | } 87 | 88 | export default connect(mapStateToProps, actions)(AddInvoiceItem); 89 | -------------------------------------------------------------------------------- /public/src/components/add_invoice_item/add_invoice_item.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import mockFormStore from '../test_helpers' 6 | import AddInvoiceItem from './add_invoice_item'; 7 | 8 | describe('Component: AddInvoiceItem', () => { 9 | const props = {}; 10 | const store = mockFormStore(); 11 | props.location = {}; 12 | props.pathName = ''; 13 | 14 | it('renders without crashing', () => { 15 | const wrapper = shallow(); 16 | 17 | expect(wrapper).toHaveLength(1); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /public/src/components/app/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Nav from '../nav/nav'; 3 | 4 | export default class App extends Component { 5 | render() { 6 | return ( 7 |
8 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/src/components/app/app.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import App from './app'; 5 | 6 | describe('Component: ', () => { 7 | const props = {}; 8 | props.location = {}; 9 | props.location.pathname = ''; 10 | 11 | it('should render', () => { 12 | const wrapper = shallow(
); 13 | 14 | expect(wrapper).toHaveLength(1); 15 | }); 16 | 17 | it('should render its children', () => { 18 | const wrapper = shallow(
); 19 | const children = wrapper.find('.child'); 20 | 21 | expect(children).toHaveLength(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /public/src/components/create_customer/create_customer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Field, reduxForm } from 'redux-form'; 4 | import { Link } from 'react-router'; 5 | 6 | import * as actions from '../../actions'; 7 | import FormField from '../form_field/form_field'; 8 | 9 | class CreateCustomer extends Component { 10 | handleFormSubmit(values) { 11 | this.props.createCustomer(values); 12 | } 13 | 14 | render() { 15 | const { handleSubmit } = this.props; 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 | 30 | 31 | Or Select Existing Customer 32 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | function validate(values) { 39 | const errors = {}; 40 | 41 | if (!values.name) { 42 | errors.name = "Name is required" 43 | } 44 | 45 | if (!values.address) { 46 | errors.address = "Address is required" 47 | } 48 | 49 | if (!values.phone) { 50 | errors.phone = "Phone is required" 51 | } 52 | 53 | return errors; 54 | } 55 | 56 | CreateCustomer = reduxForm({ 57 | form: 'createcustomer', 58 | validate 59 | })(CreateCustomer); 60 | 61 | export default connect(null, actions)(CreateCustomer); 62 | -------------------------------------------------------------------------------- /public/src/components/create_customer/create_customer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import mockFormStore from '../test_helpers' 6 | import CreateCustomer from './create_customer'; 7 | 8 | describe('Component: CreateCustomer', () => { 9 | const props = {}; 10 | const store = mockFormStore(); 11 | props.location = {}; 12 | props.pathName = ''; 13 | 14 | it('renders without crashing', () => { 15 | const wrapper = shallow(); 16 | 17 | expect(wrapper).toHaveLength(1); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /public/src/components/customer_list/customer_list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import * as actions from '../../actions'; 6 | 7 | class CustomerList extends Component { 8 | componentWillMount() { 9 | this.props.fetchCustomers(); 10 | } 11 | 12 | renderCustomer(customer) { 13 | return ( 14 | 15 | {customer.id} 16 | {customer.name} 17 | {customer.address} 18 | {customer.phone} 19 | 20 | ); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {this.props.customers.map(this.renderCustomer)} 37 | 38 |
IDNameAddressPhone
39 |
40 | ); 41 | } 42 | } 43 | 44 | function mapStateToProps(state) { 45 | return { customers: state.customers } 46 | } 47 | 48 | export default connect(mapStateToProps, actions)(CustomerList); 49 | -------------------------------------------------------------------------------- /public/src/components/customer_list/customer_list.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import mockFormStore from '../test_helpers' 6 | import CustomerList from './customer_list'; 7 | 8 | describe('Component: CustomerList', () => { 9 | const props = {}; 10 | const store = mockFormStore(); 11 | props.location = {}; 12 | props.pathName = ''; 13 | 14 | it('renders without crashing', () => { 15 | const wrapper = shallow(); 16 | 17 | expect(wrapper).toHaveLength(1); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /public/src/components/form_field/form_field.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormField = ({ input, label, type, meta: { touched, error, warning } }) => { 4 | let classNames = ["form-group"]; 5 | if (error && touched) { 6 | classNames.push("has-error"); 7 | } 8 | 9 | return ( 10 |
11 | {label ? : ''} 12 | 13 | {touched && ((error && {error}) || (warning && {warning}))} 14 |
15 | ); 16 | }; 17 | 18 | export default FormField; 19 | -------------------------------------------------------------------------------- /public/src/components/form_field/form_field.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import FormField from './form_field'; 5 | 6 | describe('Component: FormField', () => { 7 | const props = {meta:{}}; 8 | props.location = {}; 9 | props.pathName = ''; 10 | 11 | it('renders without crashing', () => { 12 | const wrapper = shallow(); 13 | 14 | expect(wrapper).toHaveLength(1); 15 | }); 16 | 17 | it('renders input and no label if label prop absent', () => { 18 | const wrapper = shallow(); 19 | 20 | expect(wrapper.find('label')).toHaveLength(0); 21 | }); 22 | 23 | it('renders input and no label if label prop present', () => { 24 | props.label = "Test Label"; 25 | const wrapper = shallow(); 26 | 27 | expect(wrapper.find('label')).toHaveLength(1); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /public/src/components/header/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = ({title}) => { 4 | return ( 5 |
6 |

{title}

7 |
8 | ); 9 | }; 10 | 11 | export default Header; 12 | -------------------------------------------------------------------------------- /public/src/components/header/header.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Header from './header'; 5 | 6 | describe('Component: Header', () => { 7 | const props = {}; 8 | props.location = {}; 9 | props.pathName = ''; 10 | 11 | it('renders without crashing', () => { 12 | const wrapper = shallow(
); 13 | 14 | expect(wrapper).toHaveLength(1); 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /public/src/components/invoice_list/invoice_list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import * as actions from '../../actions'; 6 | 7 | class InvoiceList extends Component { 8 | componentWillMount() { 9 | this.props.fetchInvoices(); 10 | } 11 | 12 | renderInvoice(invoice) { 13 | return ( 14 | 15 | {invoice.id} 16 | {invoice.customer_id} 17 | {invoice.discount}% 18 | ${invoice.total} 19 | 20 | ); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | Add Invoice 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {this.props.invoices.filter(invoice => invoice.total).map(this.renderInvoice)} 38 | 39 |
Invoice IDCustomer IDDiscountTotal
40 |
41 | ); 42 | } 43 | } 44 | 45 | function mapStateToProps(state) { 46 | return { invoices: state.invoices } 47 | } 48 | 49 | export default connect(mapStateToProps, actions)(InvoiceList); 50 | -------------------------------------------------------------------------------- /public/src/components/invoice_list/invoice_list.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import mockFormStore from '../test_helpers' 6 | import InvoiceList from './invoice_list'; 7 | 8 | describe('Component: InvoiceList', () => { 9 | const props = {}; 10 | const store = mockFormStore(); 11 | props.location = {}; 12 | props.pathName = ''; 13 | 14 | it('renders without crashing', () => { 15 | const wrapper = shallow(); 16 | 17 | expect(wrapper).toHaveLength(1); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /public/src/components/nav/nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const Nav = (props) => { 5 | return ( 6 | 26 | ); 27 | }; 28 | 29 | export default Nav; 30 | -------------------------------------------------------------------------------- /public/src/components/nav/nav.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import Nav from './nav'; 5 | 6 | describe('Component: Nav', () => { 7 | const props = {}; 8 | props.location = {}; 9 | props.pathName = ''; 10 | 11 | it('renders without crashing', () => { 12 | const wrapper = shallow(