├── .gitignore ├── src ├── main.css ├── actions │ ├── app.js │ └── devices.js ├── components │ ├── devices │ │ ├── validation.js │ │ ├── List.js │ │ ├── Devices.js │ │ └── Form.js │ ├── pages │ │ ├── About.js │ │ ├── Home.js │ │ └── Splash.js │ ├── Scenes.js │ └── App.js ├── reducers │ ├── app.js │ ├── index.js │ └── devices.js ├── containers │ ├── AppContainer.js │ └── DevicesContainer.js ├── index.html ├── index.js └── libs │ ├── localApi.js │ └── store.js ├── screens ├── form.png ├── list.png └── fetching.png ├── .babelrc ├── LICENSE.md ├── package.json ├── readme.md └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background: white; 4 | } 5 | -------------------------------------------------------------------------------- /screens/form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristianszwarc/react_crud_localStorage/HEAD/screens/form.png -------------------------------------------------------------------------------- /screens/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristianszwarc/react_crud_localStorage/HEAD/screens/list.png -------------------------------------------------------------------------------- /screens/fetching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cristianszwarc/react_crud_localStorage/HEAD/screens/fetching.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-1" 6 | ], 7 | "env": { 8 | "start": { 9 | "presets": [ 10 | "react-hmre" 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/app.js: -------------------------------------------------------------------------------- 1 | export const APP_NAVIGATE = 'APP_NAVIGATE'; 2 | 3 | export function appNavigate(value) { 4 | return { 5 | type: APP_NAVIGATE, 6 | payload: value 7 | }; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/components/devices/validation.js: -------------------------------------------------------------------------------- 1 | export default function DeviceValidation(data) { 2 | const errors = {}; 3 | if(!data.title) { 4 | errors.title = 'Required'; 5 | } 6 | if(data.title && data.title.length < 3) { 7 | errors.title = 'Must be longer than 3 characters'; 8 | } 9 | return errors; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/pages/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | export default class About extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 |

10 | About 11 |

12 |
13 | This is another page. 14 |
15 |
16 | ); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/components/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | export default class Home extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 |

10 | Home 11 |

12 |
13 | This is a linked page. 14 |
15 |
16 | ); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/components/pages/Splash.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | export default class Splash extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 |

10 | Splash! 11 |

12 |
13 | Welcome, this is an unlinked page that will be shown only once. 14 |
15 |
16 | ); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/reducers/app.js: -------------------------------------------------------------------------------- 1 | import { APP_NAVIGATE } from '../actions/app'; 2 | 3 | const INITIAL_STATE = { 4 | scene: 'splash', // the initial scene 5 | }; 6 | 7 | export default function(state = INITIAL_STATE, action) { 8 | switch (action.type) { 9 | // change the scene the main app is showing (home, about, devices) 10 | case APP_NAVIGATE: 11 | return { ...state, scene: action.payload}; 12 | 13 | // do nothing 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | 4 | // import each reducer 5 | import app from './app'; // this will then be available as state.app 6 | import devices from './devices'; // and this one as state.devices 7 | 8 | // combine all reducers, each one will then be available as state.someReducer 9 | const myReducers = combineReducers({ 10 | app, 11 | devices, 12 | form: formReducer, // this is required by redux-form 13 | }); 14 | 15 | export default myReducers; 16 | -------------------------------------------------------------------------------- /src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { appNavigate } from '../actions/app'; 3 | 4 | // import the component to map 5 | import App from '../components/App'; 6 | 7 | // this returns the pieces of the state 8 | const mapStateToProps = (state) => { 9 | return { 10 | scene: state.app.scene // selecting one element instead of the whole thing 11 | }; 12 | } 13 | 14 | // map actions to this.props.someFunction 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | navigate: (targetScene) => { 18 | dispatch(appNavigate(targetScene)); 19 | } 20 | } 21 | } 22 | 23 | // map the state 24 | const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App) 25 | 26 | export default AppContainer 27 | -------------------------------------------------------------------------------- /src/components/Scenes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | // show/hide a child component based on a given scene 5 | export class Scene extends Component { 6 | 7 | render() { 8 | if (this.props.scene === this.props.current ) { 9 | return ( 10 | this.props.children 11 | ); 12 | } else { 13 | return null; 14 | } 15 | 16 | } 17 | 18 | } 19 | 20 | export class SceneLink extends Component { 21 | render() { 22 | let styles = { 23 | cursor: 'pointer' 24 | } 25 | 26 | return ( 27 | 28 | {this.props.children} 29 | 30 | ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | React CRUD 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cristian Szwarc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crud_app", 3 | "version": "0.0.0", 4 | "description": "crud demo app", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config --progress --profile", 8 | "start": "webpack-dev-server --progress --inline" 9 | }, 10 | "keywords": [ 11 | "webpack", 12 | "demo" 13 | ], 14 | "author": "Cristian Szwarc", 15 | "license": "MIT", 16 | "dependencies": { 17 | "babel-core": "^6.4.5", 18 | "babel-loader": "^6.2.1", 19 | "babel-preset-es2015": "^6.3.13", 20 | "babel-preset-react": "^6.3.13", 21 | "babel-preset-stage-1": "^6.5.0", 22 | "expect": "^1.16.0", 23 | "file-loader": "^0.8.5", 24 | "html-loader": "^0.4.3", 25 | "lodash": "^4.6.1", 26 | "react": "^0.14.6", 27 | "react-dom": "^0.14.7", 28 | "react-redux": "^4.4.1", 29 | "redux": "^3.3.1", 30 | "redux-devtools": "^3.1.1", 31 | "redux-devtools-dock-monitor": "^1.1.0", 32 | "redux-devtools-log-monitor": "^1.0.5", 33 | "redux-form": "^4.2.2", 34 | "redux-localstorage": "^0.4.0", 35 | "redux-thunk": "^2.0.1", 36 | "webpack": "^1.12.12", 37 | "webpack-dev-server": "^1.14.1", 38 | "babel-preset-react-hmre": "^1.0.1", 39 | "css-loader": "^0.23.1", 40 | "elementtree": "^0.1.6", 41 | "replace": "^0.3.0", 42 | "style-loader": "^0.13.0", 43 | "webpack-merge": "^0.7.3" 44 | }, 45 | "devDependencies": { 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './main.css'; 2 | 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | import { compose, createStore, applyMiddleware } from 'redux'; 7 | import thunk from 'redux-thunk'; 8 | import persistState from 'redux-localstorage'; 9 | 10 | // dev tools 11 | import { createDevTools } from 'redux-devtools'; 12 | import LogMonitor from 'redux-devtools-log-monitor'; 13 | import DockMonitor from 'redux-devtools-dock-monitor'; 14 | 15 | // import a root component (the app!) 16 | import AppContainer from './containers/AppContainer'; 17 | 18 | // the reducers are combined in ./reducers/index.js 19 | // so they can be included as one thing 20 | import myReducers from './reducers'; 21 | 22 | // configure dev tools 23 | const DevTools = createDevTools( 24 | 25 | 26 | 27 | ); 28 | 29 | // persistant storage and middleware application 30 | const createPersistentStore = compose( 31 | persistState(), 32 | applyMiddleware(thunk), 33 | DevTools.instrument() 34 | )(createStore); 35 | 36 | // the store creation, using createPersistentStore instead createStore 37 | let store = createPersistentStore( 38 | myReducers 39 | ); 40 | 41 | // app render 42 | render( 43 | 44 |
45 | 46 | 47 |
48 |
, 49 | document.getElementById('app') 50 | ); 51 | -------------------------------------------------------------------------------- /src/components/devices/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | // scenes is a silly trick to avoid routes, check the file to see it how works 5 | import {SceneLink} from '../Scenes'; 6 | 7 | export default class List extends Component { 8 | componentWillMount() { 9 | this.props.fetch(); 10 | } 11 | 12 | renderList(items) { 13 | return items.map((item) => { 14 | return ( 15 | 16 | 17 | {item.title} 18 | 19 | 20 | {item.port} 21 | 22 | 23 | Edit 24 | 25 | 26 | Remove 27 | 28 | 29 | ); 30 | }); 31 | } 32 | 33 | render() { 34 | const { items, itemsFetching } = this.props; 35 | 36 | if (itemsFetching) { 37 | return ( 38 |
39 | Loading... 40 |
41 | ); 42 | } 43 | 44 | if (!items || items.length<1) { 45 | return ( 46 |
47 | No devices found, please add one. 48 |
49 | ); 50 | } 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {this.renderList(items)} 64 | 65 |
TitlePort
66 | ); 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/reducers/devices.js: -------------------------------------------------------------------------------- 1 | import { 2 | DEVICES_NAVIGATE, 3 | DEVICES_FETCHING, 4 | DEVICES_FETCHED, 5 | DEVICES_FETCHING_ONE, 6 | DEVICES_FETCHED_ONE, 7 | DEVICES_SET_SIMULATION 8 | } from '../actions/devices'; 9 | 10 | const INITIAL_STATE = { 11 | scene: 'list', // active scene displayed by the 'devices' component 12 | items: [], // fetched list of devices 13 | itemsFetching: false, // to display a 'loading..' when fetching 14 | item: null, // stores the loaded item to be used on the form 15 | itemFetching: false, // to display a 'loading..' when opening the form 16 | simulated: false, // if is simulating remote calls with a delay 17 | }; 18 | 19 | export default function(state = INITIAL_STATE, action) { 20 | switch (action.type) { 21 | // change the scene (form / list) 22 | case DEVICES_NAVIGATE: 23 | return { ...state, scene: action.payload }; 24 | 25 | // the list is being loaded, show the loading.. and reset the items 26 | case DEVICES_FETCHING: 27 | return { ...state, itemsFetching: true, items: [] }; 28 | 29 | // hide the loading and set the loaded data into items 30 | case DEVICES_FETCHED: 31 | return { ...state, itemsFetching: false, items: action.payload}; 32 | 33 | // one item is being loaded, show a loading.. inside the form and reset the current item 34 | case DEVICES_FETCHING_ONE: 35 | return { ...state, itemFetching: true, item: null}; 36 | 37 | // hide the loading.. inside the form and set the loaded data into our 'item' 38 | case DEVICES_FETCHED_ONE: 39 | return { ...state, itemFetching: false, item: action.payload}; 40 | 41 | // status change on the simulation checkbox 42 | case DEVICES_SET_SIMULATION: 43 | return { ...state, simulated: action.payload}; 44 | 45 | // do nothing 46 | default: 47 | return state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | React/Redux basic CRUD example 3 | ------------------------------- 4 | 5 | This little app was made to test a basic CRUD for a webpapp. This was done thinking on a mobile or chrome application so this is not using routes. 6 | 7 | A separation between the state and the application data is in place to test a more realistic situation where you do async calls to a remote API, something missing on many redux/react examples. 8 | 9 | This is being developed while I'm learning about react/redux so the code shared here may contain newbie errors and is not following any particular standard, but it contains a bunch of comments that may help some one that is struggling to understand how react/redux works. 10 | 11 | [Demo](http://cristianszwarc.github.io/react_crud_localStorage/) 12 | 13 | **Local Storage** 14 | This uses local storage for two tasks: 15 | 16 | - **store the state** ([redux-localstorage](https://github.com/elgerlambert/redux-localstorage)) 17 | this allows the webapp to be reloaded without losing the current state. (so it can be a hosted webapp that when added to IOS home screen does not lose the state each time the user goes away and comes back [stackoverflow](http://stackoverflow.com/questions/6930771/stop-reloading-of-web-app-launched-from-iphone-home-screen)) 18 | - **store the CRUD data** ([store.js](https://github.com/marcuswestin/store.js/)) 19 | when actions are dispatched, async calls are executed against a local api that takes care of the data, a delay time is in place so the "fetching" state can be shown. 20 | 21 | **Web app** 22 | Can be added to IOS/Andriod home screen and each time is loaded the state remains the same (because the persistent state plugin). 23 | 24 | **No routes** 25 | Routes are amazing and are a requirement in many cases but this is a trial to check an alternative way when there is no need to provide bookmarks to sections or server rendering. 26 | 27 | **To do** 28 | Learn more and improve this example. 29 | 30 | **Use** 31 | ``` 32 | npm install 33 | npm start 34 | ``` 35 | 36 | Open http://localhost:8080/ 37 | 38 | **Screens** 39 | 40 | ![List](screens/list.png) 41 | 42 | ![Form](screens/form.png) 43 | 44 | ![Fetch simulation](screens/fetching.png) 45 | 46 | **License** 47 | MIT 48 | -------------------------------------------------------------------------------- /src/components/devices/Devices.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | // components (scenes) that will be displayed by this component 5 | import List from './List'; 6 | import Form from './Form'; 7 | 8 | // scenes is a silly trick to avoid routes, check the file to see it how works 9 | import {Scene, SceneLink} from '../Scenes'; 10 | 11 | export default class Devices extends Component { 12 | 13 | componentWillMount() { 14 | this.props.setSimulation(this.props.simulated); 15 | } 16 | 17 | render() { 18 | // extract some fields from the props (to avoid use this.prop.bla after) 19 | const { scene } = this.props; 20 | 21 | // return the layout for the "devices", 22 | // check that List and Form are being feed with the current props 23 | // because I don't want to create more containers for them, 24 | // maybe a better way should be pass in only the functions each one require 25 | return ( 26 |
27 |

28 | Devices 29 |

30 | 31 | 49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | 60 |
61 | 62 | ); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/components/devices/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | import { reduxForm, initialize } from 'redux-form'; 4 | import validation from './validation'; 5 | 6 | // scenes is a silly trick to avoid routes, check the file to see it how works 7 | import {SceneLink} from '../Scenes'; 8 | 9 | class DeviceForm extends Component { 10 | 11 | render() { 12 | const { fields: {title, port}, item, itemFetching, handleSubmit, submitting, error } = this.props; 13 | 14 | if (itemFetching) { 15 | return ( 16 |
17 | Loading... 18 |
19 | ); 20 | } 21 | 22 | if (!item) { 23 | return ( 24 |
25 | Failed to load 26 |
27 | ); 28 | } 29 | 30 | return ( 31 |
32 | 33 |
34 | 35 | 36 | {title.touched && title.error &&
{title.error}
} 37 |
38 | 39 |
40 | 41 | 42 | {port.touched && port.error &&
{port.error}
} 43 |
44 | 45 | {error &&
{error}
} 46 | 47 | 50 | 51 |   {/* two white spaces (one because the nbsp and another beacuse this comment) */} 52 | 53 | 54 | Cancel 55 | 56 | 57 | 58 |
59 | 60 | ); 61 | } 62 | 63 | } 64 | 65 | DeviceForm = reduxForm({ 66 | form: 'newDeviceForm', 67 | fields: ['title', 'port', '_id'], 68 | validate: validation 69 | }, 70 | state => ({ // mapStateToProps 71 | initialValues: state.devices.item // will pull state into form's initialValues 72 | }) 73 | )(DeviceForm); 74 | 75 | export default DeviceForm; 76 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | // components (scenes) that will be displayed by this component 5 | import Splash from './pages/Splash'; 6 | import Home from './pages/Home'; 7 | import About from './pages/About'; 8 | import DevicesContainer from '../containers/DevicesContainer'; 9 | 10 | // scenes is a silly trick to avoid routes, check the file to see it how works 11 | import {Scene, SceneLink} from './Scenes'; 12 | 13 | export default class App extends Component { 14 | 15 | render() { 16 | // extract some fields from the props (to avoid use this.prop.bla after) 17 | const { scene } = this.props; 18 | 19 | return ( 20 |
21 | 22 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |

56 | Use CTRL + H to show/hide the redux dev tool.
57 | Note that the state and the application data are independent, 58 | so despite you can time travel on the state you can not do it on changes pushed to the API 59 | just like when you are working with a real remote database.
60 | GitHub 61 |

62 | 63 |
64 | ); 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/containers/DevicesContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import * as actions from '../actions/devices'; 3 | 4 | import Devices from '../components/devices/Devices'; 5 | 6 | // redux-form expects a promise to handle the submit 7 | // the save action could be used directly, 8 | // but we want to navigate away if saved correctly 9 | const handleSave = (values, dispatch) => { 10 | return new Promise((resolve, reject) => { 11 | 12 | // dispatch the save action 13 | dispatch(actions.save(values)).then( 14 | (data) => { 15 | // move away ad resolve the promise given to the form 16 | dispatch(actions.navigate('list')); 17 | resolve(); 18 | } 19 | ).catch( 20 | (error) => { 21 | // unable to save, let the form know 22 | reject({_error: 'Error saving...'}); 23 | } 24 | ); 25 | 26 | }); 27 | } 28 | 29 | // assign part of the state to the props (can return multiple items) 30 | const mapStateToProps = (state) => { 31 | return state.devices; 32 | } 33 | 34 | // map actions to this.props.someFunction 35 | const mapDispatchToProps = (dispatch) => { 36 | return { 37 | fetch: () => { 38 | dispatch(actions.fetch()); 39 | }, 40 | 41 | edit: (id) => { 42 | dispatch(actions.navigate('form')); // the form could be already filled 43 | dispatch(actions.fetchOne(id)); // but this fetch will clean it right away 44 | }, 45 | 46 | remove: (id) => { 47 | dispatch(actions.remove(id)); 48 | }, 49 | 50 | save: handleSave, // the promise for redux-form 51 | 52 | add: () => { 53 | dispatch(actions.navigate('form')); // the form could be already filled 54 | dispatch(actions.fetchOne()); // but this fetch will clean it right away 55 | }, 56 | 57 | navigate: (targetScene) => { 58 | dispatch(actions.navigate(targetScene)); 59 | }, 60 | 61 | switchSimulation: (currentStatus) => { // switchs the simulation on/off 62 | currentStatus = currentStatus ? false : true; // switchs the current status 63 | dispatch(actions.setSimulation(currentStatus)); 64 | }, 65 | 66 | setSimulation: (status) => { // when the app is loaded the simulation is set to off, we need to set the current status from the state when devices is shown 67 | dispatch(actions.setSimulation(status)); 68 | }, 69 | 70 | } 71 | } 72 | 73 | const DevicesContainer = connect(mapStateToProps, mapDispatchToProps)(Devices) 74 | 75 | export default DevicesContainer 76 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const webpack = require('webpack'); 4 | 5 | const TARGET = process.env.npm_lifecycle_event; 6 | const PATHS = { 7 | app: path.join(__dirname, 'src'), 8 | build: path.join(__dirname, 'build'), 9 | }; 10 | 11 | process.env.BABEL_ENV = TARGET; 12 | 13 | const common = { 14 | context: PATHS.app, 15 | entry: [ 16 | './index.js', 17 | './index.html', 18 | ], 19 | output: { 20 | path: PATHS.build, 21 | filename: 'bundle.js', 22 | }, 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.css$/, 27 | loaders: ['style', 'css'], 28 | include: PATHS.app, 29 | }, 30 | { 31 | test: /\.html$/, 32 | loader: 'file?name=[name].[ext]', 33 | include: PATHS.app, 34 | }, 35 | { 36 | test: /\.jsx?$/, 37 | loaders: ['babel?cacheDirectory'], 38 | include: PATHS.app, 39 | }, 40 | ], 41 | }, 42 | }; 43 | 44 | if (TARGET === 'start') { 45 | module.exports = merge(common, { 46 | devtool: 'eval-source-map', 47 | devServer: { 48 | contentBase: PATHS.build, 49 | 50 | historyApiFallback: true, 51 | hot: true, 52 | inline: true, 53 | progress: true, 54 | 55 | // display only errors to reduce the amount of output 56 | stats: 'errors-only', 57 | 58 | // parse host and port from env so this is easy 59 | // to customize 60 | host: process.env.HOST, 61 | port: process.env.PORT, 62 | }, 63 | plugins: [ 64 | new webpack.HotModuleReplacementPlugin(), 65 | ], 66 | }); 67 | } else { 68 | module.exports = merge(common, { 69 | plugins: [ 70 | new webpack.optimize.OccurenceOrderPlugin(true), 71 | 72 | new webpack.DefinePlugin({ 73 | 'process.env.NODE_ENV': '"production"' 74 | }), 75 | 76 | // Search for equal or similar files and deduplicate them in the output 77 | // https://webpack.github.io/docs/list-of-plugins.html#dedupeplugin 78 | new webpack.optimize.DedupePlugin(), 79 | 80 | // Minimize all JavaScript output of chunks 81 | // https://github.com/mishoo/UglifyJS2#compressor-options 82 | new webpack.optimize.UglifyJsPlugin({ 83 | compress: { 84 | screw_ie8: true, // jscs:ignore requireCamelCaseOrUpperCaseIdentifiers 85 | warnings: false, 86 | }, 87 | }), 88 | 89 | // A plugin for a more aggressive chunk merging strategy 90 | // https://webpack.github.io/docs/list-of-plugins.html#aggressivemergingplugin 91 | new webpack.optimize.AggressiveMergingPlugin(), 92 | ], 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/actions/devices.js: -------------------------------------------------------------------------------- 1 | export const DEVICES_NAVIGATE = 'DEVICES_NAVIGATE'; 2 | export const DEVICES_FETCHING = 'DEVICES_FETCHING'; 3 | export const DEVICES_FETCHED = 'DEVICES_FETCHED'; 4 | export const DEVICES_FETCHING_ONE = 'DEVICES_FETCHING_ONE'; 5 | export const DEVICES_FETCHED_ONE = 'DEVICES_FETCHED_ONE'; 6 | export const DEVICES_DELETING = 'DEVICES_DELETING'; 7 | export const DEVICES_SET_SIMULATION = 'DEVICES_SET_SIMULATION'; 8 | 9 | import localApi from '../libs/localApi'; 10 | 11 | // define a local db for devices (simulated async api) 12 | let myAPI = new localApi( 13 | { 14 | tableName: 'myDevices', // used as local storage key 15 | fields: { // row structure (pre loaded for new item) 16 | _id: null, // row key (required) 17 | title: 'New Device', 18 | port: '*', 19 | }, 20 | delay: 0, // simulated delay 21 | } 22 | ); 23 | 24 | export function navigate(value) { 25 | return { 26 | type: DEVICES_NAVIGATE, 27 | payload: value 28 | }; 29 | } 30 | 31 | export function fetch() { 32 | return function (dispatch) { 33 | 34 | // show a loading 35 | dispatch(fetching()) 36 | 37 | // async load 38 | myAPI.getAll().then( 39 | (data) => dispatch(fetched(data)) 40 | ); 41 | } 42 | 43 | } 44 | 45 | export function fetching() { 46 | return { 47 | type: DEVICES_FETCHING 48 | }; 49 | } 50 | 51 | export function fetched(data) { 52 | return { 53 | type: DEVICES_FETCHED, 54 | payload: data 55 | }; 56 | } 57 | 58 | export function fetchOne(id = null) { 59 | return function (dispatch) { 60 | 61 | // show a loading 62 | dispatch(fetchingOne()) 63 | 64 | // async load 65 | myAPI.get(id).then( 66 | (data) => dispatch(fetchedOne(data)) 67 | ); 68 | } 69 | } 70 | 71 | export function fetchingOne() { 72 | return { 73 | type: DEVICES_FETCHING_ONE 74 | }; 75 | } 76 | 77 | export function fetchedOne(data) { 78 | return { 79 | type: DEVICES_FETCHED_ONE, 80 | payload: data 81 | }; 82 | } 83 | 84 | export function save(values, callback) { 85 | return function (dispatch) { 86 | // return the save promise 87 | return myAPI.save(values); 88 | } 89 | 90 | } 91 | 92 | export function remove(id = null) { 93 | return function (dispatch) { 94 | 95 | // async delete 96 | myAPI.remove(id).then( 97 | (data) => dispatch(fetched(data)) 98 | ); 99 | } 100 | } 101 | 102 | export function setSimulation(status) { 103 | if (status) { 104 | myAPI.delay = 700; 105 | } else { 106 | myAPI.delay = 0; 107 | } 108 | 109 | return { 110 | type: DEVICES_SET_SIMULATION, 111 | payload: status 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /src/libs/localApi.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const storeJs = require('./store.js'); 3 | 4 | // this class allows to get/save objects just like if they were rows in a table 5 | // local storage is used and it gives back promises so async calls can be simulated 6 | // a delay can be provided so "fetching" states can be tested without using a server 7 | export default class localApi { 8 | constructor(options) { 9 | this.tableName = options.tableName ? options.tableName : 'localApi'; // local storage key 10 | this.delay = options.delay ? options.delay : 0; // increase delay to simulate remote calls 11 | this.empty = options.fields; // structure of each record 12 | this.empty._id = null; // enforce a null initial key 13 | } 14 | 15 | resolveWithDelay(resolve, data) { 16 | if (this.delay > 0) { 17 | window.setTimeout(function() { 18 | resolve(data); 19 | }, this.delay); 20 | } else { 21 | resolve(data); 22 | } 23 | } 24 | 25 | getDb() { 26 | let dbContent = storeJs.get(this.tableName); 27 | if (!dbContent) { 28 | // initialize 29 | this.commitDb(); 30 | }; 31 | 32 | return dbContent; 33 | } 34 | 35 | commitDb(dbContent = []) { 36 | storeJs.set(this.tableName, dbContent); 37 | } 38 | 39 | getAll() { 40 | return new Promise((resolve, reject) => { 41 | this.resolveWithDelay(resolve, this.getDb()); 42 | }); 43 | } 44 | 45 | get(id) { 46 | return new Promise((resolve, reject) => { 47 | if (!id) { 48 | // return a new empty one 49 | this.resolveWithDelay(resolve, this.empty); 50 | } else { 51 | let dbContent = this.getDb(); 52 | this.resolveWithDelay(resolve, _.find(dbContent, ['_id', id])); 53 | } 54 | }); 55 | } 56 | 57 | remove(id) { 58 | return new Promise((resolve, reject) => { 59 | let dbContent = this.getDb(); 60 | if (id) { 61 | let index = _.findIndex(dbContent, ['_id', id]); 62 | if (index > -1) { 63 | dbContent.splice(index, 1); 64 | } 65 | } 66 | 67 | this.commitDb(dbContent); 68 | 69 | // return the new db 70 | this.resolveWithDelay(resolve, dbContent); 71 | }); 72 | } 73 | 74 | save(values) { 75 | return new Promise((resolve, reject) => { 76 | let dbContent = this.getDb(); 77 | let item = null; 78 | 79 | // adding 80 | if (!values._id) { 81 | let newId = 1; 82 | if (dbContent.length > 0) { 83 | let maxIdDevice = dbContent.reduce((prev, current) => (prev.y > current.y) ? prev : current); 84 | newId = 1 + maxIdDevice._id; 85 | } 86 | 87 | item = { ...values, _id: newId}; 88 | dbContent.push(item); 89 | 90 | } else { // saving 91 | item = _.find(dbContent, ['_id', values._id]); 92 | if (item) { 93 | // update current values by new ones 94 | for (var prop in values) { 95 | if (values.hasOwnProperty(prop)) { 96 | item[prop] = values[prop]; 97 | } 98 | } 99 | } 100 | } 101 | 102 | this.commitDb(dbContent); 103 | this.resolveWithDelay(resolve, item); 104 | 105 | }); 106 | 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/libs/store.js: -------------------------------------------------------------------------------- 1 | // https://github.com/marcuswestin/store.js 2 | module.exports = (function() { 3 | // Store.js 4 | var store = {}, 5 | win = (typeof window != 'undefined' ? window : global), 6 | doc = win.document, 7 | localStorageName = 'localStorage', 8 | scriptTag = 'script', 9 | storage 10 | 11 | store.disabled = false 12 | store.version = '1.3.20' 13 | store.set = function(key, value) {} 14 | store.get = function(key, defaultVal) {} 15 | store.has = function(key) { return store.get(key) !== undefined } 16 | store.remove = function(key) {} 17 | store.clear = function() {} 18 | store.transact = function(key, defaultVal, transactionFn) { 19 | if (transactionFn == null) { 20 | transactionFn = defaultVal 21 | defaultVal = null 22 | } 23 | if (defaultVal == null) { 24 | defaultVal = {} 25 | } 26 | var val = store.get(key, defaultVal) 27 | transactionFn(val) 28 | store.set(key, val) 29 | } 30 | store.getAll = function() { 31 | var ret = {} 32 | store.forEach(function(key, val) { 33 | ret[key] = val 34 | }) 35 | return ret 36 | } 37 | store.forEach = function() {} 38 | store.serialize = function(value) { 39 | return JSON.stringify(value) 40 | } 41 | store.deserialize = function(value) { 42 | if (typeof value != 'string') { return undefined } 43 | try { return JSON.parse(value) } 44 | catch(e) { return value || undefined } 45 | } 46 | 47 | // Functions to encapsulate questionable FireFox 3.6.13 behavior 48 | // when about.config::dom.storage.enabled === false 49 | // See https://github.com/marcuswestin/store.js/issues#issue/13 50 | function isLocalStorageNameSupported() { 51 | try { return (localStorageName in win && win[localStorageName]) } 52 | catch(err) { return false } 53 | } 54 | 55 | if (isLocalStorageNameSupported()) { 56 | storage = win[localStorageName] 57 | store.set = function(key, val) { 58 | if (val === undefined) { return store.remove(key) } 59 | storage.setItem(key, store.serialize(val)) 60 | return val 61 | } 62 | store.get = function(key, defaultVal) { 63 | var val = store.deserialize(storage.getItem(key)) 64 | return (val === undefined ? defaultVal : val) 65 | } 66 | store.remove = function(key) { storage.removeItem(key) } 67 | store.clear = function() { storage.clear() } 68 | store.forEach = function(callback) { 69 | for (var i=0; idocument.w=window') 91 | storageContainer.close() 92 | storageOwner = storageContainer.w.frames[0].document 93 | storage = storageOwner.createElement('div') 94 | } catch(e) { 95 | // somehow ActiveXObject instantiation failed (perhaps some special 96 | // security settings or otherwse), fall back to per-path storage 97 | storage = doc.createElement('div') 98 | storageOwner = doc.body 99 | } 100 | var withIEStorage = function(storeFunction) { 101 | return function() { 102 | var args = Array.prototype.slice.call(arguments, 0) 103 | args.unshift(storage) 104 | // See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx 105 | // and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx 106 | storageOwner.appendChild(storage) 107 | storage.addBehavior('#default#userData') 108 | storage.load(localStorageName) 109 | var result = storeFunction.apply(store, args) 110 | storageOwner.removeChild(storage) 111 | return result 112 | } 113 | } 114 | 115 | // In IE7, keys cannot start with a digit or contain certain chars. 116 | // See https://github.com/marcuswestin/store.js/issues/40 117 | // See https://github.com/marcuswestin/store.js/issues/83 118 | var forbiddenCharsRegex = new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]", "g") 119 | var ieKeyFix = function(key) { 120 | return key.replace(/^d/, '___$&').replace(forbiddenCharsRegex, '___') 121 | } 122 | store.set = withIEStorage(function(storage, key, val) { 123 | key = ieKeyFix(key) 124 | if (val === undefined) { return store.remove(key) } 125 | storage.setAttribute(key, store.serialize(val)) 126 | storage.save(localStorageName) 127 | return val 128 | }) 129 | store.get = withIEStorage(function(storage, key, defaultVal) { 130 | key = ieKeyFix(key) 131 | var val = store.deserialize(storage.getAttribute(key)) 132 | return (val === undefined ? defaultVal : val) 133 | }) 134 | store.remove = withIEStorage(function(storage, key) { 135 | key = ieKeyFix(key) 136 | storage.removeAttribute(key) 137 | storage.save(localStorageName) 138 | }) 139 | store.clear = withIEStorage(function(storage) { 140 | var attributes = storage.XMLDocument.documentElement.attributes 141 | storage.load(localStorageName) 142 | for (var i=attributes.length-1; i>=0; i--) { 143 | storage.removeAttribute(attributes[i].name) 144 | } 145 | storage.save(localStorageName) 146 | }) 147 | store.forEach = withIEStorage(function(storage, callback) { 148 | var attributes = storage.XMLDocument.documentElement.attributes 149 | for (var i=0, attr; attr=attributes[i]; ++i) { 150 | callback(attr.name, store.deserialize(storage.getAttribute(attr.name))) 151 | } 152 | }) 153 | } 154 | 155 | try { 156 | var testKey = '__storejs__' 157 | store.set(testKey, testKey) 158 | if (store.get(testKey) != testKey) { store.disabled = true } 159 | store.remove(testKey) 160 | } catch(e) { 161 | store.disabled = true 162 | } 163 | store.enabled = !store.disabled 164 | 165 | return store 166 | }()) 167 | --------------------------------------------------------------------------------