├── .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 |
--------------------------------------------------------------------------------
/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('
').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h'}),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 | Product |
35 | Quantity |
36 | Price |
37 | Action |
38 |
39 |
40 |
41 | {invoiceItems.map((item) => {
42 | let itemProduct = products.find(product => product.id == item.product_id);
43 | return (
44 |
45 | {itemProduct.name} |
46 | {item.quantity} |
47 | ${itemProduct.price * item.quantity} |
48 | |
49 |
50 | );
51 | })
52 | }
53 |
54 |
55 |
56 |
57 | |
58 | |
59 | Subtotal: |
60 | ${currentInvoice.total.toFixed(2)} |
61 |
62 |
63 | |
64 | |
65 | Discount: |
66 | {currentInvoice.discount}% |
67 |
68 |
69 | |
70 | |
71 | Total: |
72 | ${(currentInvoice.total * (1 - currentInvoice.discount/100)).toFixed(2)} |
73 |
74 |
75 |
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 |
9 | {this.props.children}
10 |
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 |
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 | ID |
30 | Name |
31 | Address |
32 | Phone |
33 |
34 |
35 |
36 | {this.props.customers.map(this.renderCustomer)}
37 |
38 |
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 | Invoice ID |
31 | Customer ID |
32 | Discount |
33 | Total |
34 |
35 |
36 |
37 | {this.props.invoices.filter(invoice => invoice.total).map(this.renderInvoice)}
38 |
39 |
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();
13 |
14 | expect(wrapper).toHaveLength(1);
15 | });
16 |
17 | });
18 |
--------------------------------------------------------------------------------
/public/src/components/product_list/product_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 ProductList extends Component {
8 | componentWillMount() {
9 | this.props.fetchProducts();
10 | }
11 |
12 | renderProduct(product) {
13 | return (
14 |
15 | {product.id} |
16 | {product.name} |
17 | ${product.price} |
18 |
19 | );
20 | }
21 |
22 | render() {
23 | return (
24 |
25 |
26 |
27 |
28 | ID |
29 | Name |
30 | Price |
31 |
32 |
33 |
34 | {this.props.products.map(this.renderProduct)}
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | function mapStateToProps(state) {
43 | return { products: state.products }
44 | }
45 |
46 | export default connect(mapStateToProps, actions)(ProductList);
47 |
--------------------------------------------------------------------------------
/public/src/components/product_list/product_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 ProductList from './product_list';
7 |
8 | describe('Component: ProductList', () => {
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/select_customer/select_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 |
8 | class SelectCustomer extends Component {
9 | componentWillMount() {
10 | this.props.fetchCustomers();
11 | }
12 |
13 | handleFormSubmit({id}) {
14 | this.props.selectCustomer(id);
15 | }
16 |
17 | render() {
18 | const { handleSubmit, customers } = this.props;
19 | return (
20 |
35 | );
36 | }
37 | }
38 |
39 | SelectCustomer = reduxForm({
40 | form: 'setcustomer'
41 | })(SelectCustomer);
42 |
43 | function mapStateToProps(state) {
44 | return {
45 | customers: state.customers
46 | };
47 | }
48 |
49 | export default connect(mapStateToProps, actions)(SelectCustomer);
50 |
--------------------------------------------------------------------------------
/public/src/components/select_customer/select_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 SelectCustomer from './select_customer';
7 |
8 | describe('Component: SelectCustomer', () => {
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/test_helpers.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import { reducer as formReducer } from 'redux-form';
3 |
4 | /**
5 | * @param {object} initialState - Specify the initial state of the store.
6 | */
7 | export default function mockFormStore(initialState = {}) {
8 | const store = createStore((state = initialState, action) => {
9 | return Object.assign({}, state, {
10 | form: formReducer(state.form, action),
11 | });
12 | });
13 |
14 | return store;
15 | }
16 |
--------------------------------------------------------------------------------
/public/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { createStore, applyMiddleware } from 'redux';
5 | import promise from 'redux-promise';
6 | import { Router, Route, IndexRoute, browserHistory } from 'react-router';
7 |
8 | import App from './components/app/app';
9 | import Customers from './pages/customers';
10 | import Products from './pages/products';
11 | import Invoices from './pages/invoices';
12 | import SelectInvoiceCustomer from './pages/set_invoice_customer';
13 | import CreateInvoiceCustomer from './pages/create_invoice_customer';
14 | import AddInvoiceItems from './pages/add_invoice';
15 |
16 | import reducers from './reducers';
17 |
18 | const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
19 |
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | , document.querySelector('.container'));
35 |
--------------------------------------------------------------------------------
/public/src/pages/add_invoice.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import AddInvoiceForm from '../components/add_invoice/add_invoice';
4 | import Header from '../components/header/header';
5 |
6 | const AddInvoice = (props) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default AddInvoice;
16 |
--------------------------------------------------------------------------------
/public/src/pages/create_invoice_customer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Header from '../components/header/header';
4 | import CreateCustomer from '../components/create_customer/create_customer';
5 |
6 | const CreateInvoiceCustomer = (props) => {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default CreateInvoiceCustomer;
16 |
--------------------------------------------------------------------------------
/public/src/pages/customers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import CustomerList from '../components/customer_list/customer_list';
4 | import Header from '../components/header/header';
5 |
6 | const Customers = (props) => {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default Customers;
16 |
--------------------------------------------------------------------------------
/public/src/pages/invoices.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import InvoiceList from '../components/invoice_list/invoice_list';
4 | import Header from '../components/header/header';
5 |
6 | const Invoices = (props) => {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default Invoices;
16 |
--------------------------------------------------------------------------------
/public/src/pages/products.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ProductList from '../components/product_list/product_list';
4 | import Header from '../components/header/header';
5 |
6 | const Products = (props) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default Products;
16 |
--------------------------------------------------------------------------------
/public/src/pages/set_invoice_customer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import Header from '../components/header/header';
4 | import SelectCustomer from '../components/select_customer/select_customer';
5 |
6 | const SelectInvoiceCustomer = (props) => {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default SelectInvoiceCustomer;
16 |
--------------------------------------------------------------------------------
/public/src/reducers/current_invoice.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_INVOICE,
3 | ADD_INVOICE_ITEM,
4 | UPDATE_INVOICE
5 | } from '../actions/types';
6 |
7 | export default function(state = { total: 0, discount: 0 }, action) {
8 | switch(action.type) {
9 | case ADD_INVOICE:
10 | return { ...state, ...action.payload.data };
11 | case UPDATE_INVOICE:
12 | return { ...state, ...action.payload.data };
13 | case ADD_INVOICE_ITEM:
14 | console.log('currentINvoice reducer', action);
15 | return { ...state, total: action.payload[1].data.total }
16 | }
17 |
18 | return state;
19 | }
20 |
--------------------------------------------------------------------------------
/public/src/reducers/customers.js:
--------------------------------------------------------------------------------
1 | import {
2 | FETCH_CUSTOMERS
3 | } from '../actions/types';
4 |
5 | export default function(state = [], action) {
6 | switch(action.type) {
7 | case FETCH_CUSTOMERS:
8 | return action.payload.data;
9 | }
10 |
11 | return state;
12 | }
13 |
--------------------------------------------------------------------------------
/public/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as form } from 'redux-form';
3 | import invoicesReducer from './invoices';
4 | import currentInvoiceReducer from './current_invoice';
5 | import invoiceItemsReducer from './invoice_items';
6 | import customersReducer from './customers';
7 | import productsReducer from './products';
8 | import product from './product';
9 | import selectedCustomerReducer from './selected_customer';
10 |
11 | const rootReducer = combineReducers({
12 | invoices: invoicesReducer,
13 | customers: customersReducer,
14 | products: productsReducer,
15 | currentInvoice: currentInvoiceReducer,
16 | selectedCustomer: selectedCustomerReducer,
17 | product: product,
18 | invoiceItems: invoiceItemsReducer,
19 | form
20 | });
21 |
22 | export default rootReducer;
23 |
--------------------------------------------------------------------------------
/public/src/reducers/invoice_items.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_INVOICE_ITEM
3 | } from '../actions/types';
4 |
5 | export default function(state = [], action) {
6 | switch(action.type) {
7 | case ADD_INVOICE_ITEM:
8 | console.log('add_invoice_item', action);
9 | return [...state, action.payload[0].data];
10 | }
11 |
12 | return state;
13 | }
14 |
--------------------------------------------------------------------------------
/public/src/reducers/invoices.js:
--------------------------------------------------------------------------------
1 | import {
2 | FETCH_INVOICES
3 | } from '../actions/types';
4 |
5 | export default function(state = [], action) {
6 | switch(action.type) {
7 | case FETCH_INVOICES:
8 | return action.payload.data;
9 | }
10 |
11 | return state;
12 | }
13 |
--------------------------------------------------------------------------------
/public/src/reducers/product.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_PRODUCT
3 | } from '../actions/types';
4 |
5 | export default function(state = {}, action) {
6 | switch(action.type) {
7 | case SET_PRODUCT:
8 | console.log('set_product', action);
9 | return {...state, ...action.payload.data};
10 | }
11 |
12 | return state;
13 | }
14 |
--------------------------------------------------------------------------------
/public/src/reducers/products.js:
--------------------------------------------------------------------------------
1 | import {
2 | FETCH_PRODUCTS
3 | } from '../actions/types';
4 |
5 | export default function(state = [], action) {
6 | switch(action.type) {
7 | case FETCH_PRODUCTS:
8 | return action.payload.data;
9 | }
10 |
11 | return state;
12 | }
13 |
--------------------------------------------------------------------------------
/public/src/reducers/selected_customer.js:
--------------------------------------------------------------------------------
1 | import {
2 | CREATE_CUSTOMER,
3 | SELECT_CUSTOMER
4 | } from '../actions/types';
5 |
6 | export default function(state = {}, action) {
7 | switch(action.type) {
8 | case CREATE_CUSTOMER:
9 | console.log('action.payload createCustomer', action.payload.data);
10 | return { ...state, ...action.payload.data };
11 | case SELECT_CUSTOMER:
12 | console.log('action.payload selectCustomer', action.payload);
13 | return { ...state, ...action.payload.data };
14 | }
15 |
16 | return state;
17 | }
18 |
--------------------------------------------------------------------------------
/seed_database.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | db_models = require('./db_models'),
3 | Sequelize = require('sequelize');
4 |
5 | sequelize.sync().then(function() {
6 | Customer.create({
7 | name: "Mark Benson",
8 | address: "353 Rochester St, Rialto FL 43250",
9 | phone: "555-534-2342"
10 | });
11 |
12 | Customer.create({
13 | name: "Bob Smith",
14 | address: "215 Market St, Dansville CA 94325",
15 | phone: "555-534-2342"
16 | });
17 |
18 | Customer.create({
19 | name: "John Draper",
20 | address: "890 Main St, Fontana IL 31450",
21 | phone: "555-534-2342"
22 | });
23 |
24 | Product.create({
25 | name: "Parachute Pants",
26 | price: 29.99
27 | });
28 |
29 | Product.create({
30 | name: "Phone Holder",
31 | price: 9.99
32 | });
33 |
34 | Product.create({
35 | name: "Pet Rock",
36 | price: 5.99
37 | });
38 |
39 | Product.create({
40 | name: "Egg Timer",
41 | price: 15.99
42 | });
43 |
44 | Product.create({
45 | name: "Neon Green Hat",
46 | price: 21.99
47 | });
48 |
49 | }).catch(function(e) {
50 | console.log("ERROR SYNCING WITH DB", e);
51 | });
52 |
--------------------------------------------------------------------------------
/testConfig/fileMock.js:
--------------------------------------------------------------------------------
1 | export default {}
2 |
--------------------------------------------------------------------------------
/testConfig/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleNameMapper": {
3 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/testConfig/fileMock.js",
4 | "^.+\\.(css|scss|sass)$": "/testConfig/styleMock.js"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/testConfig/styleMock.js:
--------------------------------------------------------------------------------
1 | export default {}
2 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: [
3 | './public/src/index.js'
4 | ],
5 | output: {
6 | path: __dirname,
7 | publicPath: '/',
8 | filename: '/public/bundle.js'
9 | },
10 | module: {
11 | loaders: [{
12 | exclude: /node_modules/,
13 | loader: 'babel',
14 | query: {
15 | presets: ['react', 'es2015', 'stage-1']
16 | }
17 | }]
18 | },
19 | resolve: {
20 | extensions: ['', '.js', '.jsx']
21 | },
22 | devServer: {
23 | historyApiFallback: true,
24 | contentBase: './'
25 | }
26 | };
27 |
--------------------------------------------------------------------------------