├── .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 | [![build status](https://secure.travis-ci.org/bebraw/react-crm-frontend.png)](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 |
    18 | 19 | 20 |
    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 |
    19 | 20 | 21 |
    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 |
    44 | 48 | 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 |
    31 | 32 | {this.props.title} 33 | ? 34 | ! 35 | 36 | {this.props.children} 37 |
    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 | } --------------------------------------------------------------------------------