├── .eslintignore
├── .eslintrc
├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE
├── README.md
├── app
├── Application
│ ├── index.jsx
│ ├── side-menu.css
│ ├── skylight.css
│ └── style.css
├── Contracts
│ ├── Layout.jsx
│ └── index.jsx
├── Dashboard
│ ├── index.jsx
│ └── layout.jsx
├── Home
│ └── index.jsx
├── Registers
│ ├── Clients.jsx
│ ├── Invoices.jsx
│ ├── Layout.jsx
│ ├── Products.jsx
│ └── index.jsx
├── api.js
├── index.js
├── lib
│ ├── Actions.js
│ ├── Create.jsx
│ ├── Crud.jsx
│ ├── FieldWrapper.jsx
│ ├── Form.jsx
│ ├── Modal.jsx
│ ├── SectionWrapper.jsx
│ ├── Store.js
│ ├── Table.jsx
│ ├── common.jsx
│ ├── generate_titles.js
│ ├── get_schema.js
│ └── get_visible.js
├── prerender.html
├── routes.jsx
└── simple.html
├── config
├── webpack.common.js
└── webpack.config.js
├── dev-server.js
├── index.html
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "jasmine": true
7 | },
8 | "plugins": [
9 | "react"
10 | ],
11 | "globals": {
12 | "jest": false
13 | },
14 | "ecmaFeatures": {
15 | "jsx": true,
16 | "globalReturn": false
17 | },
18 | "rules": {
19 | "no-shadow": 0,
20 | "no-underscore-dangle": 0,
21 | "no-use-before-define": 0,
22 | "quotes": [0, "single"],
23 | "comma-dangle": 0,
24 | "react/display-name": 1,
25 | "react/jsx-quotes": 1,
26 | "react/jsx-no-undef": 1,
27 | "react/jsx-uses-react": 1,
28 | "react/jsx-uses-vars": 1,
29 | "react/no-did-mount-set-state": 1,
30 | "react/no-did-update-set-state": 1,
31 | "react/no-multi-comp": 1,
32 | "react/prop-types": 1,
33 | "react/react-in-jsx-scope": 1,
34 | "react/self-closing-comp": 1,
35 | "react/wrap-multilines": 1
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | *.log
3 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "bitwise": true,
3 | "browser": true,
4 | "camelcase": false,
5 | "curly": true,
6 | "eqeqeq": true,
7 | "esnext": true,
8 | "immed": true,
9 | "indent": 4,
10 | "latedef": false,
11 | "newcap": true,
12 | "noarg": true,
13 | "node": true,
14 | "quotmark": "single",
15 | "strict": true,
16 | "trailing": true,
17 | "undef": true,
18 | "unused": true,
19 | "sub": true,
20 | "predef": ["-Promise"]
21 | }
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Juho Vepsalainen
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://travis-ci.org/bebraw/react-crm-frontend)
2 |
3 | # react-crm-frontend
4 |
5 | Frontend of React CRM.
6 |
7 | ## Development
8 |
9 | 1. Set up and run backend (defaults to port 3000)
10 | 2. `npm install`
11 | 3. `npm start`
12 | 4. Surf to `http://localhost:4000/`
13 |
14 | ## License
15 |
16 | `react-crm-frontend` is available under MIT. See LICENSE for more details.
17 |
--------------------------------------------------------------------------------
/app/Application/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var classNames = require('classnames');
3 | var React = require('react');
4 | var ReactRouter = require('react-router');
5 | var RouteHandler = ReactRouter.RouteHandler;
6 |
7 | var Menu = require('react-pure-menu');
8 | var MenuLink = require('lib/common.jsx').MenuLink;
9 |
10 | require('purecss/build/pure.css');
11 | require('reactabular/style.css');
12 | require('react-pagify/style.css');
13 | require('./skylight.css');
14 | require('./side-menu.css');
15 | require('./style.css');
16 |
17 |
18 | module.exports = React.createClass({
19 | displayName: 'Application',
20 |
21 | contextTypes: {
22 | router: React.PropTypes.func
23 | },
24 |
25 | getInitialState() {
26 | return {
27 | menuActive: false,
28 | };
29 | },
30 |
31 | render() {
32 | const menuActive = this.state.menuActive;
33 | const pathName = this.context.router.getCurrentPathname();
34 | const inRegisters = pathName.startsWith('/registers');
35 |
36 | return (
37 |
38 |
41 |
42 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | },
87 |
88 | menuLinkClicked(e) {
89 | e.preventDefault();
90 |
91 | this.setState({
92 | menuActive: !this.state.menuActive,
93 | });
94 | }
95 | });
96 |
--------------------------------------------------------------------------------
/app/Application/side-menu.css:
--------------------------------------------------------------------------------
1 | .pure-img-responsive {
2 | max-width: 100%;
3 | height: auto;
4 | }
5 |
6 | /*
7 | Add transition to containers so they can push in and out.
8 | */
9 | #layout,
10 | #menu,
11 | .menu-link {
12 | -webkit-transition: all 0.2s ease-out;
13 | -moz-transition: all 0.2s ease-out;
14 | -ms-transition: all 0.2s ease-out;
15 | -o-transition: all 0.2s ease-out;
16 | transition: all 0.2s ease-out;
17 | }
18 |
19 | /*
20 | This is the parent `` that contains the menu and the content area.
21 | */
22 | #layout {
23 | position: relative;
24 | padding-left: 0;
25 | }
26 | #layout.active #menu {
27 | left: 150px;
28 | width: 150px;
29 | }
30 |
31 | #layout.active .menu-link {
32 | left: 150px;
33 | }
34 | /*
35 | The content `
` is where all your content goes.
36 | */
37 | .content {
38 | margin: 0 auto;
39 | padding: 0 2em;
40 | max-width: 800px;
41 | margin-bottom: 50px;
42 | line-height: 1.6em;
43 | }
44 |
45 | .header {
46 | margin: 0;
47 | color: #333;
48 | text-align: center;
49 | padding: 2.5em 2em 0;
50 | border-bottom: 1px solid #eee;
51 | }
52 | .header h1 {
53 | margin: 0.2em 0;
54 | font-size: 3em;
55 | font-weight: 300;
56 | }
57 | .header h2 {
58 | font-weight: 300;
59 | color: #ccc;
60 | padding: 0;
61 | margin-top: 0;
62 | }
63 |
64 | .content-subhead {
65 | margin: 50px 0 20px 0;
66 | font-weight: 300;
67 | color: #888;
68 | }
69 |
70 |
71 |
72 | /*
73 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that
74 | appears on the left side of the page.
75 | */
76 |
77 | #menu {
78 | margin-left: -150px; /* "#menu" width */
79 | width: 150px;
80 | position: fixed;
81 | top: 0;
82 | left: 0;
83 | bottom: 0;
84 | z-index: 1000; /* so the menu or its navicon stays above all content */
85 | background: #191818;
86 | overflow-y: auto;
87 | -webkit-overflow-scrolling: touch;
88 | }
89 | /*
90 | All anchors inside the menu should be styled like this.
91 | */
92 | #menu a {
93 | color: #999;
94 | border: none;
95 | padding: 0.6em 0 0.6em 0.6em;
96 | }
97 |
98 | /*
99 | Remove all background/borders, since we are applying them to #menu.
100 | */
101 | #menu .pure-menu,
102 | #menu .pure-menu ul {
103 | border: none;
104 | background: transparent;
105 | }
106 |
107 | /*
108 | Add that light border to separate items into groups.
109 | */
110 | #menu .pure-menu ul,
111 | #menu .pure-menu .menu-item-divided {
112 | border-top: 1px solid #333;
113 | }
114 | /*
115 | Change color of the anchor links on hover/focus.
116 | */
117 | #menu .pure-menu li a:hover,
118 | #menu .pure-menu li a:focus {
119 | background: #333;
120 | }
121 |
122 | /*
123 | This styles the selected menu item `
`.
124 | */
125 | #menu .pure-menu-selected,
126 | #menu .pure-menu-heading {
127 | background: #1f8dd6;
128 | }
129 | /*
130 | This styles a link within a selected menu item ``.
131 | */
132 | #menu .pure-menu-selected a {
133 | color: #fff;
134 | }
135 |
136 | /*
137 | This styles the menu heading.
138 | */
139 | #menu .pure-menu-heading {
140 | font-size: 110%;
141 | color: #fff;
142 | margin: 0;
143 | }
144 |
145 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/
146 |
147 | /*
148 | The button to open/close the Menu is custom-made and not part of Pure. Here's
149 | how it works:
150 | */
151 |
152 | /*
153 | `.menu-link` represents the responsive menu toggle that shows/hides on
154 | small screens.
155 | */
156 | .menu-link {
157 | position: fixed;
158 | display: block; /* show this only on small screens */
159 | top: 0;
160 | left: 0; /* "#menu width" */
161 | background: #000;
162 | background: rgba(0,0,0,0.7);
163 | font-size: 10px; /* change this value to increase/decrease button size */
164 | z-index: 10;
165 | width: 2em;
166 | height: auto;
167 | padding: 2.1em 1.6em;
168 | }
169 |
170 | .menu-link:hover,
171 | .menu-link:focus {
172 | background: #000;
173 | }
174 |
175 | .menu-link span {
176 | position: relative;
177 | display: block;
178 | }
179 |
180 | .menu-link span,
181 | .menu-link span:before,
182 | .menu-link span:after {
183 | background-color: #fff;
184 | width: 100%;
185 | height: 0.2em;
186 | }
187 |
188 | .menu-link span:before,
189 | .menu-link span:after {
190 | position: absolute;
191 | margin-top: -0.6em;
192 | content: " ";
193 | }
194 |
195 | .menu-link span:after {
196 | margin-top: 0.6em;
197 | }
198 |
199 |
200 | /* -- Responsive Styles (Media Queries) ------------------------------------- */
201 |
202 | /*
203 | Hides the menu at `48em`, but modify this based on your app's needs.
204 | */
205 | @media (min-width: 48em) {
206 |
207 | .header,
208 | .content {
209 | padding-left: 2em;
210 | padding-right: 2em;
211 | }
212 |
213 | #layout {
214 | padding-left: 150px; /* left col width "#menu" */
215 | left: 0;
216 | }
217 | #menu {
218 | left: 150px;
219 | }
220 |
221 | .menu-link {
222 | position: fixed;
223 | left: 150px;
224 | display: none;
225 | }
226 |
227 | #layout.active .menu-link {
228 | left: 150px;
229 | }
230 | }
231 |
232 | @media (max-width: 48em) {
233 | /* Only apply this when the window is small. Otherwise, the following
234 | case results in extra padding on the left:
235 | * Make the window small.
236 | * Tap the menu to trigger the active state.
237 | * Make the window large again.
238 | */
239 | #layout.active {
240 | position: relative;
241 | left: 150px;
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/app/Application/skylight.css:
--------------------------------------------------------------------------------
1 | .skylight-dialog {
2 | width: 50%;
3 | height: 400px;
4 | position: fixed;
5 | top: 50%;
6 | left: 50%;
7 | margin-top: -200px;
8 | margin-left: -25%;
9 | background-color: #fff;
10 | border-radius: 2px;
11 | z-index: 100;
12 | padding: 10px;
13 | box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);
14 | overflow: auto;
15 | }
16 |
17 | .skylight-dialog--close {
18 | cursor: pointer;
19 | float: right;
20 | font-size: 1.6em;
21 | }
22 |
23 | .skylight-dialog__overlay {
24 | position: fixed;
25 | top: 0;
26 | left: 0;
27 | width: 100%;
28 | height: 100%;
29 | z-index: 99;
30 | background-color: rgba(0,0,0,0.3);
31 | }
--------------------------------------------------------------------------------
/app/Application/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Sans-Serif;
3 |
4 | line-height: 1.5;
5 | }
6 |
7 | .controls {
8 | margin-bottom: 1em;
9 | }
10 |
11 | /* XXX: fix menu item height, not sure why it scales to 100% by default */
12 | .pure-menu-item {
13 | height: inherit;
14 | }
15 |
16 | .form-element.error {
17 | background: rgb(250, 204, 202);
18 | }
19 |
20 | #menu .pure-menu-list.submenu {
21 | background: white;
22 | border: 1px solid #333;
23 | }
24 |
25 | .cancel-button {
26 | float: right;
27 | }
28 |
29 | .table-container table {
30 | width: 100%;
31 | }
32 |
33 | .table-container .table-controls {
34 | margin-bottom: 1em;
35 |
36 | overflow: auto;
37 | }
38 |
39 | .table-container .table-controls .table-per-page-container {
40 | float: left;
41 | }
42 |
43 | .table-container .table-controls .table-search-container {
44 | float: right;
45 | }
46 |
--------------------------------------------------------------------------------
/app/Contracts/Layout.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 |
4 |
5 | module.exports = React.createClass({
6 | displayName: 'Layout',
7 |
8 | render() {
9 | return (
10 |
11 |
12 |
Contracts
13 | This should show contracts
14 |
15 |
16 |
17 | TODO
18 |
19 |
20 | );
21 |
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/app/Contracts/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var Router = require('react-router');
4 | var Route = Router.Route;
5 |
6 | var Layout = require('./Layout');
7 |
8 |
9 | module.exports = function() {
10 | return (
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/app/Dashboard/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var Router = require('react-router');
4 | var Route = Router.Route;
5 |
6 | var Layout = require('./Layout');
7 |
8 |
9 | module.exports = function() {
10 | return (
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/app/Dashboard/layout.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 |
4 |
5 | module.exports = React.createClass({
6 | displayName: 'Layout',
7 |
8 | render() {
9 | return (
10 |
11 |
12 |
Dashboard
13 | Dashboard should go here
14 |
15 |
16 |
17 | TODO
18 |
19 |
20 | );
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/app/Home/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 |
5 |
6 | module.exports = React.createClass({
7 | displayName: 'Home',
8 |
9 | render() {
10 | return (
11 |
12 |
Login form should go here
13 |
14 | );
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/app/Registers/Clients.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 |
4 | var createCrud = require('../lib/Crud');
5 |
6 |
7 | module.exports = function(api) {
8 | var crud = createCrud(api);
9 | var ClientGroups = crud('clientgroup', 'client group');
10 | var Clients = crud('client');
11 |
12 | return React.createClass({
13 | displayName: 'Clients',
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/app/Registers/Invoices.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 |
4 | var createCrud = require('../lib/Crud');
5 |
6 |
7 | module.exports = function(api) {
8 | var crud = createCrud(api);
9 | var PendingInvoices = crud('pendinginvoice');
10 | var ApprovedInvoices = crud('approvedinvoice');
11 |
12 | return React.createClass({
13 | displayName: 'Invoices',
14 |
15 | render() {
16 | return (
17 |
21 | );
22 | }
23 | });
24 | };
25 |
26 |
--------------------------------------------------------------------------------
/app/Registers/Layout.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var ReactRouter = require('react-router');
4 | var RouteHandler = ReactRouter.RouteHandler;
5 |
6 |
7 | module.exports = React.createClass({
8 | displayName: 'Layout',
9 |
10 | render() {
11 | return (
12 |
13 |
14 |
Registers
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/app/Registers/Products.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 |
4 | var createCrud = require('../lib/Crud');
5 |
6 |
7 | module.exports = function(api) {
8 | var crud = createCrud(api);
9 | var ProductGroups = crud('productgroup', 'product group');
10 | var Products = crud('product');
11 |
12 | return React.createClass({
13 | displayName: 'Products',
14 |
15 | render() {
16 |
17 | return (
18 |
22 | );
23 | }
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/app/Registers/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var Router = require('react-router');
4 | var Route = Router.Route;
5 | var DefaultRoute = Router.DefaultRoute;
6 |
7 | var createCrud = require('../lib/Crud');
8 | var Layout = require('./Layout');
9 | var clients = require('./Clients');
10 | var invoices = require('./Invoices');
11 | var products = require('./Products');
12 |
13 |
14 | module.exports = function(api) {
15 | var crud = createCrud(api);
16 | var Users = crud('user');
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/app/api.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Promise = require('es6-promise').Promise;
3 | var axios = require('axios');
4 | var swaggerClient = require('swagger2client');
5 |
6 |
7 | module.exports = function(url) {
8 | return new Promise(function(resolve, reject) {
9 | axios.all([
10 | axios.get(url + '/v1/schema'),
11 | axios.post(url + '/authenticate'),
12 | ]).then(axios.spread(function(schema, token) {
13 | resolve(swaggerClient({
14 | url: url,
15 | schema: schema.data,
16 | headers: {
17 | 'Authorization': 'Bearer ' + token.data.token
18 | }
19 | }));
20 | })).catch(function(res) {
21 | reject(res);
22 | });
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var Router = require('react-router');
5 |
6 | var routes = require('./routes.jsx');
7 |
8 |
9 | main();
10 |
11 | function main() {
12 | routes().then(function(routeDefinition) {
13 | Router.run(routeDefinition, Router.HistoryLocation, function(Application) {
14 | React.render(, document.body);
15 | });
16 | }).catch(function(res) {
17 | console.error(res);
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/app/lib/Actions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Reflux = require('reflux');
3 |
4 |
5 | module.exports = function(api, resource) {
6 | var asyncChildren = {
7 | children: ['completed', 'failed'],
8 | };
9 | var Actions = Reflux.createActions({
10 | load: asyncChildren,
11 | create: asyncChildren,
12 | update: asyncChildren,
13 | sort: asyncChildren,
14 | });
15 |
16 | Actions.load.listen(function(o) {
17 | api[resource].get(o).then((res) => {
18 | this.completed({
19 | count: res.headers['total-count'],
20 | data: res.data
21 | });
22 | }).catch(this.failed);
23 | });
24 |
25 | Actions.create.listen(function(data) {
26 | api[resource].post(data).then((res) => {
27 | this.completed(res.data);
28 | }).catch(this.failed);
29 | });
30 |
31 | Actions.update.listen(function(data) {
32 | api[resource].put(data).then((res) => {
33 | this.completed(res.data);
34 | }).catch(this.failed);
35 | });
36 |
37 | Actions.sort.listen(function(o) {
38 | api[resource].get(o).then((res) => {
39 | this.completed({
40 | count: res.headers['total-count'],
41 | data: res.data
42 | });
43 | }).catch(this.failed);
44 | });
45 |
46 | return Actions;
47 | };
48 |
--------------------------------------------------------------------------------
/app/lib/Create.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var Button = require('react-pure-button');
4 |
5 | var Form = require('lib/Form');
6 | var Modal = require('./Modal');
7 | var getVisible = require('./get_visible');
8 |
9 |
10 | module.exports = React.createClass({
11 | displayName: 'Create',
12 |
13 | propTypes: {
14 | api: React.PropTypes.object,
15 | actions: React.PropTypes.object,
16 | schema: React.PropTypes.object,
17 | children: React.PropTypes.any,
18 | },
19 |
20 | getInitialState: function() {
21 | return {
22 | modal: {
23 | title: null,
24 | content: null,
25 | },
26 | };
27 | },
28 |
29 | render() {
30 | var modal = this.state.modal || {};
31 |
32 | return (
33 |
34 |
35 | {modal.content}
36 |
37 | );
38 | },
39 |
40 | createNew: function() {
41 | var that = this;
42 | var title = this.props.children;
43 | var schema = this.props.schema;
44 | var api = this.props.api;
45 |
46 | getVisible(api, schema.properties, (err, d) => {
47 | if(err) {
48 | return console.error(err);
49 | }
50 |
51 | schema.properties = d;
52 |
53 | this.setState({
54 | modal: {
55 | title: title,
56 | content:
61 | }
62 | });
63 |
64 | this.refs.modal.show();
65 | });
66 |
67 | function onSubmit(data, value, errors) {
68 | if(value === 'Cancel') {
69 | return that.refs.modal.hide();
70 | }
71 |
72 | if(!Object.keys(errors).length) {
73 | that.refs.modal.hide();
74 |
75 | delete data.id;
76 |
77 | that.props.actions.create(data);
78 | }
79 | else {
80 | console.info('errors', errors);
81 | }
82 | }
83 | },
84 | });
85 |
--------------------------------------------------------------------------------
/app/lib/Crud.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var titleCase = require('title-case');
4 |
5 | var Create = require('./Create.jsx');
6 | var Table = require('./Table.jsx');
7 | var getSchema = require('./get_schema');
8 |
9 |
10 | module.exports = function(api) {
11 | return function(resourceName, name) {
12 | name = name || resourceName;
13 |
14 | var multipleName = name + 's';
15 | var upperMultipleName = titleCase(multipleName);
16 | var actions = require('./Actions')(api, resourceName + 's');
17 | var store = require('./Store')(actions);
18 | var schema = getSchema(api[resourceName + 's']);
19 | var createNew = 'Create a new ' + name;
20 |
21 | return React.createClass({
22 | displayName: upperMultipleName,
23 |
24 | propTypes: {
25 | columns: React.PropTypes.array,
26 | },
27 |
28 | render: function() {
29 | const columns = this.props.columns;
30 |
31 | return (
32 |
33 |
34 |
{upperMultipleName}
35 |
36 |
37 |
38 |
39 | {createNew}
40 |
41 |
42 |
43 |
49 |
50 | );
51 | },
52 | });
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/app/lib/FieldWrapper.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 |
5 |
6 | module.exports = React.createClass({
7 | displayName: 'FieldWrapper',
8 |
9 | propTypes: {
10 | errors: React.PropTypes.array,
11 | classes: React.PropTypes.array,
12 | key: React.PropTypes.string,
13 | title: React.PropTypes.string,
14 | children: React.PropTypes.any,
15 | },
16 |
17 | render() {
18 | var errors = (this.props.errors || []).join('\n');
19 | var classes = [].concat(errors ? 'error' : [],
20 | 'form-element',
21 | this.props.classes || []);
22 |
23 | classes.push('pure-control-group');
24 |
25 | return (
26 |
27 |
28 | {this.props.children}
29 |
30 | );
31 | }
32 | });
33 |
--------------------------------------------------------------------------------
/app/lib/Form.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 |
4 | var Form = require('plexus-form');
5 | var validate = require('plexus-validate');
6 |
7 | var FieldWrapper = require('./FieldWrapper.jsx');
8 | var SectionWrapper = require('./SectionWrapper.jsx');
9 |
10 |
11 | module.exports = React.createClass({
12 | displayName: 'Form',
13 |
14 | propTypes: {
15 | schema: React.PropTypes.object,
16 | values: React.PropTypes.object,
17 | onSubmit: React.PropTypes.func
18 | },
19 |
20 | render() {
21 | return (
22 |
32 | );
33 | },
34 |
35 | buttons(submit) {
36 | return (
37 |
38 |
42 |
46 |
47 | );
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/app/lib/Modal.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var React = require('react');
3 | var SkyLight = require('babel!react-skylight/src/skylight.jsx');
4 |
5 |
6 | module.exports = React.createClass({
7 | displayName: 'Modal',
8 |
9 | propTypes: {
10 | children: React.PropTypes.any,
11 | },
12 |
13 | render: function() {
14 | var dialogStyles = {
15 | overflow: 'auto'
16 | };
17 |
18 | return (
19 |
20 | {this.props.children}
21 |
22 | );
23 | },
24 |
25 | show: function() {
26 | this.refs.modal.show();
27 | },
28 |
29 | hide: function() {
30 | this.refs.modal.hide();
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/app/lib/SectionWrapper.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 |
5 |
6 | module.exports = React.createClass({
7 | displayName: 'SectionWrapper',
8 |
9 | propTypes: {
10 | errors: React.PropTypes.array,
11 | path: React.PropTypes.array,
12 | classes: React.PropTypes.array,
13 | description: React.PropTypes.string,
14 | key: React.PropTypes.string,
15 | title: React.PropTypes.string,
16 | children: React.PropTypes.any,
17 | },
18 |
19 | render() {
20 | var errors = (this.props.errors || []).join('\n');
21 | var level = this.props.path.length;
22 | var classes = [].concat(errors ? 'error' : [],
23 | 'form-section',
24 | (level > 0 ? 'form-subsection' : []),
25 | this.props.classes || []);
26 | var helpClasses = 'form-help' + (this.props.description ? '' : ' hidden');
27 | var errorClasses = 'form-error' + (errors ? '' : ' hidden');
28 |
29 | return (
30 |
38 | );
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/app/lib/Store.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Reflux = require('reflux');
3 | var findIndex = require('lodash').findIndex;
4 |
5 |
6 | module.exports = function(actions) {
7 | return Reflux.createStore({
8 | init: function() {
9 | this.data = [];
10 | this.count = 0;
11 |
12 | this.listenTo(actions.load.completed, this.loadCompleted);
13 | this.listenTo(actions.load.failed, this.failed);
14 |
15 | this.listenTo(actions.create.completed, this.createCompleted);
16 | this.listenTo(actions.create.failed, this.failed);
17 |
18 | this.listenTo(actions.update.completed, this.updateCompleted);
19 | this.listenTo(actions.update.failed, this.failed);
20 |
21 | this.listenTo(actions.sort.completed, this.loadCompleted);
22 | this.listenTo(actions.sort.failed, this.failed);
23 | },
24 |
25 | loadCompleted: function(o) {
26 | this.data = o.data;
27 | this.count = o.count;
28 |
29 | this.refresh();
30 | },
31 |
32 | createCompleted: function(data) {
33 | // XXX: this might not be ok always (if paginated)
34 | this.data.push(data);
35 | this.count++;
36 |
37 | this.refresh();
38 | },
39 |
40 | updateCompleted: function(data) {
41 | var i = findIndex(this.data, {id: data.id});
42 |
43 | this.data[i] = data;
44 |
45 | this.refresh();
46 | },
47 |
48 | failed: function(err) {
49 | console.error(err);
50 | },
51 |
52 | refresh: function() {
53 | this.trigger({
54 | data: this.data,
55 | count: this.count,
56 | });
57 | }
58 | });
59 | };
60 |
--------------------------------------------------------------------------------
/app/lib/Table.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var _ = require('lodash'); // XXX: expand to exact import
3 | var React = require('react');
4 | var Reflux = require('reflux');
5 | var titleCase = require('title-case');
6 |
7 | var reactabular = require('reactabular');
8 | var Search = reactabular.Search;
9 | var Table = reactabular.Table;
10 |
11 | var Paginator = require('react-pagify');
12 |
13 | var Form = require('lib/Form');
14 | var Modal = require('./Modal');
15 | var getVisible = require('./get_visible');
16 |
17 |
18 | module.exports = React.createClass({
19 | displayName: 'Table',
20 |
21 | mixins: [Reflux.ListenerMixin],
22 |
23 | propTypes: {
24 | api: React.PropTypes.object,
25 | actions: React.PropTypes.object,
26 | columns: React.PropTypes.array,
27 | store: React.PropTypes.object,
28 | schema: React.PropTypes.object,
29 | onSort: React.PropTypes.func,
30 | },
31 |
32 | getInitialState() {
33 | const perPage = 10;
34 | const actions = this.props.actions;
35 | const store = this.props.store;
36 | const schema = this.props.schema || {};
37 | const visibleColumns = this.props.columns;
38 |
39 | if(store) {
40 | this.listenTo(this.props.store, this.onData);
41 | }
42 |
43 | if(actions) {
44 | actions.load({
45 | perPage: perPage,
46 | });
47 | }
48 |
49 | var columns = Object.keys(schema.properties).map(function(name) {
50 | return {
51 | property: name,
52 | header: titleCase(name),
53 | };
54 | });
55 |
56 | if(visibleColumns) {
57 | columns = columns.filter((o) => visibleColumns.indexOf(o.property) >= 0);
58 | }
59 |
60 | return {
61 | store: {
62 | data: [],
63 | count: 0,
64 | },
65 | modal: {
66 | title: null,
67 | content: null,
68 | },
69 | pagination: {
70 | page: 0,
71 | perPage: perPage,
72 | },
73 | search: {
74 | q: null,
75 | field: null,
76 | },
77 | sortBy: null,
78 | columns: columns,
79 | };
80 | },
81 |
82 | onData(store) {
83 | this.setState({
84 | store: store,
85 | });
86 | },
87 |
88 | render() {
89 | var columns = this.state.columns || [];
90 | var header = {
91 | onClick: (column) => {
92 | var actions = this.props.actions;
93 | var property = column.property;
94 | var pagination = this.state.pagination;
95 | var sortBy = this.state.sortBy;
96 |
97 | if(sortBy === property) {
98 | sortBy = '-' + property;
99 | }
100 | else {
101 | sortBy = property;
102 | }
103 |
104 | if(actions) {
105 | this.props.actions.sort(_.merge({
106 | sortBy: sortBy,
107 | }, pagination));
108 | }
109 |
110 | this.setState({
111 | sortBy: sortBy,
112 | });
113 | },
114 | };
115 | var store = this.state.store;
116 | var modal = this.state.modal;
117 | var pagination = this.state.pagination;
118 | var pageAmount = Math.ceil(store.count / pagination.perPage);
119 | var i18n = {
120 | noData: 'No data'
121 | };
122 |
123 | columns = columns.concat({
124 | cell: this.editCell,
125 | });
126 |
127 | return (
128 | store.data && store.data.length ?
129 |
130 |
131 |
132 | Per page
133 |
134 |
135 | Search
136 |
137 |
138 |
143 | {pageAmount > 1 ?
: null}
149 |
{modal.content}
150 |
151 | : {i18n.noData}
152 | );
153 | },
154 |
155 | onPerPage(e) {
156 | const actions = this.props.actions;
157 | const perPage = parseInt(e.target.value, 10);
158 | var pagination = this.state.pagination || {};
159 |
160 | pagination.perPage = perPage;
161 |
162 | this.setState({
163 | pagination: pagination
164 | });
165 |
166 | if(actions) {
167 | this.loadData();
168 | }
169 | },
170 |
171 | onSearch(d) {
172 | this.setState({
173 | search: {
174 | q: d.search.query,
175 | field: d.search.column,
176 | }
177 | }, this.loadData);
178 | },
179 |
180 | onSelectPage(page) {
181 | var pagination = this.state.pagination;
182 |
183 | pagination.page = page;
184 |
185 | this.props.actions.load(_.merge({
186 | sortBy: this.state.sortBy,
187 | }, pagination));
188 |
189 | this.setState({
190 | pagination: pagination,
191 | }, this.loadData);
192 | },
193 |
194 | loadData() {
195 | const actions = this.props.actions;
196 |
197 | if(!actions) {
198 | return;
199 | }
200 |
201 | const pagination = this.state.pagination || {};
202 | const search = this.state.search || {};
203 |
204 | this.props.actions.load(_.merge({
205 | sortBy: this.state.sortBy,
206 | }, pagination, search));
207 | },
208 |
209 | editCell(property, value, rowIndex) {
210 | var edit = () => {
211 | this.refs.modal.show();
212 |
213 | var onSubmit = (data, value, errors) => {
214 | this.refs.modal.hide();
215 |
216 | if(value === 'Cancel') {
217 | return;
218 | }
219 |
220 | if(!Object.keys(errors).length) {
221 | this.refs.modal.hide();
222 |
223 | this.props.actions.update(data);
224 | }
225 | };
226 |
227 | var schema = this.props.schema || {};
228 | var data = this.props.store.data;
229 | var api = this.props.api;
230 |
231 | getVisible(api, schema.properties, (err, d) => {
232 | if(err) {
233 | return console.error(err);
234 | }
235 |
236 | schema.properties = d;
237 |
238 | this.setState({
239 | modal: {
240 | title: 'Edit',
241 | content:
246 | }
247 | });
248 |
249 | this.refs.modal.show();
250 | });
251 | };
252 |
253 | return {
254 | value:
255 |
256 | ⇙
257 |
258 |
259 | };
260 | },
261 | });
262 |
--------------------------------------------------------------------------------
/app/lib/common.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var classNames = require('classnames');
3 | var React = require('react');
4 | var ReactRouter = require('react-router');
5 | var Link = ReactRouter.Link;
6 |
7 |
8 | exports.MenuLink = React.createClass({
9 | displayName: 'MenuLink',
10 |
11 | propTypes: {
12 | className: React.PropTypes.string,
13 | },
14 |
15 | render() {
16 | var {className, ...props} = this.props;
17 |
18 | return (
19 |
20 | {props.children}
21 |
22 | );
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/app/lib/generate_titles.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var titleCase = require('title-case');
3 | var zip = require('annozip');
4 |
5 |
6 | module.exports = function(o) {
7 | return zip.toObject(zip(o).map((pair) => {
8 | pair[1].title = titleCase(pair[0]);
9 |
10 | return pair;
11 | }));
12 | };
13 |
--------------------------------------------------------------------------------
/app/lib/get_schema.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var generateTitles = require('./generate_titles');
3 |
4 |
5 | module.exports = function(endpoint) {
6 | var schema = endpoint.get.responses['200'].schema;
7 |
8 | schema.type = 'object';
9 | schema.properties = generateTitles(schema.properties);
10 |
11 | return schema;
12 | };
13 |
--------------------------------------------------------------------------------
/app/lib/get_visible.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var async = require('async');
3 |
4 | var i18n = {
5 | 'en-en': 'English',
6 | 'fi-fi': 'Finnish',
7 | };
8 |
9 |
10 | module.exports = function(api, properties, cb) {
11 | var resources = Object.keys(api).map((s) => s.slice(0, -1));
12 | var ret = {};
13 |
14 | async.each(Object.keys(properties), (k, cb) => {
15 | var v = properties[k];
16 |
17 | if(!v.readOnly) {
18 | ret[k] = v;
19 | }
20 |
21 | if(v.enum) {
22 | ret[k].enumNames = v.enum.map((o) => i18n[o]);
23 |
24 | cb();
25 | }
26 | else if(resources.indexOf(k) >= 0) {
27 | api[k + 's'].get().then((d) => {
28 | var data = d.data;
29 |
30 | ret[k].enum = data.map((v) => v.id);
31 | ret[k].enumNames = data.map((v) => v.name);
32 |
33 | cb();
34 | }).catch(cb);
35 | }
36 | else {
37 | cb();
38 | }
39 | }, (err) => {
40 | if(err) {
41 | return cb(err);
42 | }
43 |
44 | cb(null, ret);
45 | });
46 | };
47 |
--------------------------------------------------------------------------------
/app/prerender.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | CONTENT
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/routes.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var Router = require('react-router');
5 | var Promise = require('es6-promise').Promise;
6 | var Route = Router.Route;
7 | var DefaultRoute = Router.DefaultRoute;
8 |
9 | var Application = require('./Application');
10 | var Home = require('./Home');
11 |
12 | var contracts = require('./Contracts');
13 | var dashboard = require('./Dashboard');
14 | var registers = require('./Registers');
15 |
16 | var url = 'http://localhost:3000'; // TODO: move this to configuration
17 | var createApi = require('./api');
18 |
19 |
20 | module.exports = function() {
21 | return new Promise(function(resolve, reject) {
22 | createApi(url).then(function(api) {
23 | resolve(
24 |
25 | {contracts(api)}
26 | {dashboard(api)}
27 | {registers(api)}
28 |
29 |
30 |
31 | );
32 | }).catch(function(res) {
33 | reject(res);
34 | });
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/app/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/config/webpack.common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | module.exports = {
5 | entry: [
6 | './app/index'
7 | ],
8 | resolve: {
9 | extensions: ['', '.js', '.jsx', '.css', '.png', '.jpg'],
10 | },
11 | };
12 |
13 | module.exports.loaders = [
14 | {
15 | test: /\.css$/,
16 | loaders: ['style', 'css'],
17 | },
18 | {
19 | test: /\.png$/,
20 | loader: 'url-loader?limit=100000&mimetype=image/png',
21 | },
22 | {
23 | test: /\.jpg$/,
24 | loader: 'file-loader',
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var path = require('path');
3 |
4 | var extend = require('xtend');
5 | var webpack = require('webpack');
6 |
7 | var common = require('./webpack.common');
8 |
9 |
10 | module.exports = extend(common, {
11 | devtool: 'eval',
12 | entry: [
13 | 'webpack-dev-server/client?http://0.0.0.0:4000',
14 | 'webpack/hot/only-dev-server',
15 | './app/index',
16 | ],
17 | resolve: {
18 | extensions: common.resolve.extensions,
19 | alias: {
20 | 'lib': path.join(__dirname, '../app/lib'),
21 | }
22 | },
23 | output: {
24 | path: __dirname,
25 | filename: 'bundle.js',
26 | publicPath: '/app/'
27 | },
28 | plugins: [
29 | new webpack.HotModuleReplacementPlugin(),
30 | new webpack.NoErrorsPlugin(),
31 | ],
32 | module: {
33 | loaders: common.loaders.concat([{
34 | test: /\.jsx?$/,
35 | loaders: ['react-hot', 'babel?stage=0'],
36 | exclude: /node_modules/,
37 | }])
38 | }
39 | });
40 |
--------------------------------------------------------------------------------
/dev-server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var webpack = require('webpack');
3 | var WebpackDevServer = require('webpack-dev-server');
4 |
5 | var config = require('./config/webpack.config');
6 |
7 |
8 | var port = 4000;
9 | var ip = '0.0.0.0';
10 | new WebpackDevServer(webpack(config), {
11 | publicPath: config.output.publicPath,
12 | hot: true,
13 | historyApiFallback: true,
14 | }).listen(port, ip, function (err) {
15 | if(err) {
16 | return console.log(err);
17 | }
18 |
19 | console.log('Listening at ' + ip + ':' + port);
20 | });
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React CRM
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-crm-frontend",
3 | "version": "0.2.0",
4 | "description": "",
5 | "main": "app/app.js",
6 | "private": true,
7 | "scripts": {
8 | "start": "node dev-server.js",
9 | "test": "npm run lint",
10 | "lint": "eslint . --ext .js --ext .jsx"
11 | },
12 | "keywords": [
13 | "webpack",
14 | "react",
15 | "crm"
16 | ],
17 | "author": "Juho Vepsalainen ",
18 | "dependencies": {
19 | "annozip": "^0.2.6",
20 | "async": "^1.3.0",
21 | "axios": "^0.5.4",
22 | "classnames": "^2.1.3",
23 | "es6-promise": "^2.3.0",
24 | "lodash": "^3.10.0",
25 | "plexus-form": "^0.1.3",
26 | "plexus-validate": "^0.0.4",
27 | "purecss": "^0.6.0",
28 | "react": "^0.13.3",
29 | "react-pagify": "^0.6.5",
30 | "react-pure-button": "^0.1.0",
31 | "react-pure-menu": "^0.2.1",
32 | "react-router": "^0.13.3",
33 | "react-skylight": "^0.2.0",
34 | "reactabular": "^0.6.4",
35 | "reflux": "^0.2.8",
36 | "swagger2client": "^0.1.4",
37 | "title-case": "^1.1.1"
38 | },
39 | "devDependencies": {
40 | "babel-core": "^5.6.15",
41 | "babel-eslint": "^3.1.23",
42 | "babel-loader": "^5.3.1",
43 | "css-loader": "^0.15.1",
44 | "eslint": "^0.24.0",
45 | "eslint-plugin-react": "^2.6.4",
46 | "express": "^4.13.1",
47 | "html-loader": "^0.3.0",
48 | "json-loader": "^0.5.2",
49 | "pre-commit": "^1.0.10",
50 | "react-hot-loader": "^1.2.7",
51 | "style-loader": "^0.12.3",
52 | "webpack": "^1.10.1",
53 | "webpack-dev-server": "^1.10.1",
54 | "xtend": "^4.0.0"
55 | },
56 | "repository": {
57 | "type": "git",
58 | "url": "https://github.com/bebraw/react-crm-frontend.git"
59 | },
60 | "homepage": "https://github.com/bebraw/react-crm-frontend",
61 | "bugs": {
62 | "url": "https://github.com/bebraw/react-crm-frontend/issues"
63 | },
64 | "licenses": [
65 | {
66 | "type": "MIT",
67 | "url": "https://github.com/bebraw/react-crm-frontend/blob/master/LICENSE"
68 | }
69 | ]
70 | }
--------------------------------------------------------------------------------