├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE.md ├── README.md ├── __mocks__ └── parse.js ├── config └── index.js ├── index.js ├── package.json ├── src ├── common │ ├── components │ │ ├── App │ │ │ └── App.jsx │ │ ├── ProtectedGateway │ │ │ └── ProtectedGateway.jsx │ │ ├── PublicGateway │ │ │ └── PublicGateway.jsx │ │ └── Routes │ │ │ └── Routes.jsx │ └── modules │ │ ├── action-creators │ │ └── index.js │ │ ├── action-repository │ │ └── index.js │ │ ├── combined-reducer │ │ └── index.js │ │ ├── create-action │ │ └── index.js │ │ ├── get-current-user │ │ └── index.js │ │ ├── parse-client │ │ ├── __tests__ │ │ │ └── index-test.js │ │ └── index.js │ │ ├── reducers │ │ └── index.js │ │ └── store │ │ └── index.js ├── dashboard │ └── components │ │ └── DashboardPage │ │ └── DashboardPage.jsx └── public │ ├── components │ ├── LoginPage │ │ └── LoginPage.jsx │ └── RegistrationPage │ │ └── RegistrationPage.jsx │ └── modules │ ├── action-creators │ └── index.js │ └── action-repository │ └── index.js ├── webpack.config.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-runtime"], 3 | "presets": ["es2015", "stage-0", "react"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "indent": [ 5 | 2, 6 | 4, 7 | { 8 | "SwitchCase": 1 9 | } 10 | ], 11 | "quotes": [ 12 | 2, 13 | "single" 14 | ], 15 | "jsx-quotes": [ 16 | 2, 17 | "prefer-double" 18 | ], 19 | "linebreak-style": [ 20 | 2, 21 | "unix" 22 | ], 23 | "semi": [ 24 | 2, 25 | "always" 26 | ], 27 | "object-curly-spacing": [ 28 | 2, 29 | "never" 30 | ], 31 | "no-console": 0, 32 | "dot-notation": 2, 33 | "eol-last": 2, 34 | "constructor-super": 2, 35 | "no-class-assign": 2, 36 | "no-const-assign": 2, 37 | "no-dupe-class-members": 2, 38 | "no-this-before-super": 2, 39 | "no-var": 2, 40 | "prefer-const": 2, 41 | "object-shorthand": 2, 42 | "prefer-arrow-callback": 2, 43 | "prefer-template": 2, 44 | "arrow-parens": 2, 45 | "react/jsx-uses-react": 1, 46 | "react/forbid-prop-types": 1, 47 | "react/jsx-boolean-value": 1, 48 | "react/jsx-closing-bracket-location": 1, 49 | "react/jsx-curly-spacing": 1, 50 | "react/jsx-indent-props": 1, 51 | "react/jsx-max-props-per-line": [ 52 | 1, 53 | { 54 | "maximum": 3 55 | } 56 | ], 57 | "react/jsx-no-duplicate-props": 1, 58 | "react/jsx-no-undef": 1, 59 | "react/jsx-uses-vars": 1, 60 | "react/no-danger": 1, 61 | "react/no-did-mount-set-state": 1, 62 | "react/no-did-update-set-state": 1, 63 | "react/no-direct-mutation-state": 1, 64 | "react/no-multi-comp": 1, 65 | "react/no-set-state": 1, 66 | "react/no-unknown-property": 1, 67 | "react/prefer-es6-class": 0, 68 | "react/react-in-jsx-scope": 1, 69 | "react/require-extension": 1, 70 | "react/self-closing-comp": 1, 71 | "react/sort-comp": 2, 72 | "react/wrap-multilines": 1 73 | }, 74 | "env": { 75 | "es6": true, 76 | "node": true, 77 | "browser": true, 78 | "jasmine": true, 79 | "jest": true 80 | }, 81 | "extends": "eslint:recommended", 82 | "ecmaFeatures": { 83 | "jsx": true, 84 | "experimentalObjectRestSpread": true, 85 | "modules": true 86 | }, 87 | "plugins": [ 88 | "react" 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | www 4 | !www/index.html 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maxim Potapov 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 all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | Since Parse is about to shutdown, this package is no longer maintained. 4 | 5 | # Kickstart your application with React, Redux and Parse. 6 | 7 | ```npm run watch``` to start a webpack dev server. 8 | 9 | ```npm run build``` to build a production version. 10 | 11 | work is in progress... 12 | 13 | ## What is included 14 | 15 | * parse 16 | * react 17 | * redux 18 | * react-router 19 | * webpack 20 | * babel (ES6 Stage 0) 21 | * jest 22 | * eslint 23 | 24 | 25 | ## Todo 26 | 27 | * ~~remove axios and material-ui from deps~~ 28 | * ~~add auth flow~~ 29 | * add redux dev tools 30 | * add more tests 31 | * split config to dev/prod 32 | * add react hot reloading (https://github.com/gaearon/react-transform-boilerplate when it works with babel 6) 33 | * ~~add .babelrc~~ 34 | * switch from Jest to something faster (several seconds to run a single test in [unacceptable](https://github.com/facebook/jest/issues/116)) 35 | -------------------------------------------------------------------------------- /__mocks__/parse.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const Parse = require.requireActual('parse'); 4 | spyOn(Parse, 'initialize'); 5 | module.exports = Parse; 6 | 7 | jest.autoMockOn(); 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PARSE_APP_ID: '', 3 | PARSE_JS_KEY: '' 4 | }; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import Routes from './src/common/components/Routes/Routes.jsx'; 3 | 4 | ReactDOM.render(Routes, document.getElementById('content')); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-parse-starter-kit", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV='production' webpack --progress --profile --colors --config webpack.config.production.js", 8 | "analyze": "webpack --json | analyze-bundle-size", 9 | "watch": "webpack-dev-server --content-base www/ --hot --inline --progress --colors", 10 | "test": "BABEL_JEST_STAGE=0 jest", 11 | "lint": "eslint --ext .js --ext .jsx src" 12 | }, 13 | "author": "Maxim Potapov", 14 | "license": "MIT", 15 | "dependencies": { 16 | "babel-polyfill": "^6.3.14", 17 | "babel-runtime": "^6.3.19", 18 | "history": "~1.13.0", 19 | "parse": "^1.6.8", 20 | "react": "^0.14.1", 21 | "react-addons-create-fragment": "^0.14.1", 22 | "react-addons-pure-render-mixin": "^0.14.1", 23 | "react-addons-transition-group": "^0.14.1", 24 | "react-addons-update": "^0.14.1", 25 | "react-dom": "^0.14.1", 26 | "react-redux": "^4.0.0", 27 | "react-router": "^1.0.0", 28 | "react-tap-event-plugin": "^0.2.1", 29 | "redux": "^3.0.4", 30 | "redux-thunk": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "babel-core": "^6.0.0", 34 | "babel-eslint": "^4.1.3", 35 | "babel-jest": "^6.0.0", 36 | "babel-loader": "^6.0.0", 37 | "babel-plugin-transform-runtime": "^6.3.13", 38 | "babel-preset-es2015": "^6.3.13", 39 | "babel-preset-react": "^6.3.13", 40 | "babel-preset-stage-0": "^6.3.13", 41 | "css-loader": "^0.21.0", 42 | "eslint": "^1.8.0", 43 | "eslint-loader": "^1.1.0", 44 | "eslint-plugin-react": "^3.6.3", 45 | "jest-cli": "^0.8.2", 46 | "react-addons-test-utils": "^0.14.1", 47 | "style-loader": "^0.13.0", 48 | "webpack": "^1.12.2", 49 | "webpack-bundle-size-analyzer": "^1.1.0", 50 | "webpack-dev-server": "^1.12.1" 51 | }, 52 | "jest": { 53 | "scriptPreprocessor": "node_modules/babel-jest", 54 | "unmockedModulePathPatterns": [ 55 | "node_modules/react", 56 | "node_modules/react-dom", 57 | "node_modules/react-addons-test-utils", 58 | "node_modules/fbjs" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/common/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class App extends React.Component { 5 | 6 | render() { 7 | return (
8 |

Welcome to App

9 | {this.props.children} 10 |
); 11 | } 12 | 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/common/components/ProtectedGateway/ProtectedGateway.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {History} from 'react-router'; 4 | import {signOut} from '../../modules/action-creators/index'; 5 | 6 | 7 | const Protected = React.createClass({ 8 | 9 | mixins: [History], 10 | 11 | componentWillMount() { 12 | if (!this.props.currentUser) { 13 | this.redirect(); 14 | } 15 | }, 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (!nextProps.currentUser) { 19 | this.redirect(); 20 | } 21 | }, 22 | 23 | redirect() { 24 | this.history.replaceState(null, '/signin'); 25 | }, 26 | 27 | handleSignOut() { 28 | this.props.dispatch(signOut()); 29 | }, 30 | 31 | render() { 32 | return ( 33 |
34 | 35 | {this.props.children} 36 |
37 | ); 38 | } 39 | 40 | }); 41 | 42 | export default connect((state) => ({currentUser: state.currentUser}))(Protected); 43 | -------------------------------------------------------------------------------- /src/common/components/PublicGateway/PublicGateway.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {History} from 'react-router'; 4 | 5 | 6 | const Public = React.createClass({ 7 | 8 | mixins: [History], 9 | 10 | componentWillMount() { 11 | if (this.props.currentUser) { 12 | this.redirect(); 13 | } 14 | }, 15 | 16 | componentWillReceiveProps(nextProps) { 17 | if (nextProps.currentUser) { 18 | this.redirect(); 19 | } 20 | }, 21 | 22 | redirect() { 23 | this.history.replaceState(null, '/dashboard'); 24 | }, 25 | 26 | render() { 27 | return this.props.children; 28 | } 29 | 30 | }); 31 | 32 | export default connect((state) => ({currentUser: state.currentUser}))(Public); 33 | -------------------------------------------------------------------------------- /src/common/components/Routes/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Router, Route, Redirect} from 'react-router'; 3 | import {Provider} from 'react-redux'; 4 | 5 | import store from '../../modules/store/index'; 6 | 7 | import App from '../App/App.jsx'; 8 | import PublicGateway from '../PublicGateway/PublicGateway.jsx'; 9 | import ProtectedGateway from '../ProtectedGateway/ProtectedGateway.jsx'; 10 | import RegistrationPage from '../../../public/components/RegistrationPage/RegistrationPage.jsx'; 11 | import LoginPage from '../../../public/components/LoginPage/LoginPage.jsx'; 12 | import DashboardPage from '../../../dashboard/components/DashboardPage/DashboardPage.jsx'; 13 | 14 | const Routes = ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | export default Routes; 34 | -------------------------------------------------------------------------------- /src/common/modules/action-creators/index.js: -------------------------------------------------------------------------------- 1 | import {signOut as actionSignOut} from '../action-repository/index'; 2 | import Parse from 'parse'; 3 | 4 | export const signOut = () => { 5 | Parse.User.logOut(); 6 | return actionSignOut(Parse.User.current()); 7 | }; 8 | -------------------------------------------------------------------------------- /src/common/modules/action-repository/index.js: -------------------------------------------------------------------------------- 1 | import createAction from '../create-action/index'; 2 | 3 | export const signOut = createAction('SIGN_OUT'); 4 | -------------------------------------------------------------------------------- /src/common/modules/combined-reducer/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import {currentUser} from '../reducers/index'; 3 | 4 | const reducer = combineReducers({ 5 | currentUser 6 | }); 7 | 8 | export default reducer; 9 | -------------------------------------------------------------------------------- /src/common/modules/create-action/index.js: -------------------------------------------------------------------------------- 1 | export default (type) => { 2 | return (payload) => { 3 | return { 4 | type, 5 | payload 6 | }; 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/common/modules/get-current-user/index.js: -------------------------------------------------------------------------------- 1 | import Parse from '../parse-client/index'; 2 | 3 | export default () => { 4 | return Parse.User.current(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/common/modules/parse-client/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | const CONFIG_PATH = '../../../../../config/index'; 2 | 3 | jest.dontMock('../index'); 4 | jest.dontMock(CONFIG_PATH); 5 | 6 | describe('parse-client', () => { 7 | 8 | it('should initialize itself with keys from config', () => { 9 | 10 | const config = require(CONFIG_PATH).default; 11 | const Parse = require('../index').default; 12 | 13 | expect(Parse.initialize).toHaveBeenCalledWith(config.PARSE_APP_ID, config.PARSE_JS_KEY); 14 | 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /src/common/modules/parse-client/index.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse'; 2 | import config from '../../../../config/index'; 3 | 4 | Parse.initialize(config.PARSE_APP_ID, config.PARSE_JS_KEY); 5 | 6 | export default Parse; 7 | -------------------------------------------------------------------------------- /src/common/modules/reducers/index.js: -------------------------------------------------------------------------------- 1 | import getCurrentUser from '../get-current-user/index'; 2 | 3 | 4 | export const currentUser = function(state = getCurrentUser(), action) { 5 | 6 | switch (action.type) { 7 | 8 | case 'REGISTER': 9 | case 'SIGN_IN': 10 | case 'SIGN_OUT': 11 | return action.payload; 12 | 13 | default: 14 | return state; 15 | 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/modules/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import app from '../combined-reducer/index'; 4 | 5 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 6 | 7 | const store = createStoreWithMiddleware(app); 8 | 9 | export default store; 10 | -------------------------------------------------------------------------------- /src/dashboard/components/DashboardPage/DashboardPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class DashboardPage extends React.Component { 5 | 6 | render() { 7 | return (
8 |

Dashboard

9 |
); 10 | } 11 | 12 | } 13 | 14 | export default DashboardPage; 15 | -------------------------------------------------------------------------------- /src/public/components/LoginPage/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import {signIn} from '../../modules/action-creators/index'; 4 | import {connect} from 'react-redux'; 5 | 6 | 7 | class LoginPage extends React.Component { 8 | 9 | handleSubmit(e) { 10 | e.preventDefault(); 11 | this.props.dispatch(signIn(this.form.elements.username.value, this.form.elements.password.value)); 12 | } 13 | 14 | render() { 15 | return (
16 |

Sign in

17 |
this.form = form}> 18 | 19 | 20 | 21 |
22 | Register 23 |
); 24 | } 25 | 26 | } 27 | 28 | export default connect()(LoginPage); 29 | -------------------------------------------------------------------------------- /src/public/components/RegistrationPage/RegistrationPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import {register} from '../../modules/action-creators/index'; 4 | import {connect} from 'react-redux'; 5 | 6 | 7 | class RegistrationPage extends React.Component { 8 | 9 | handleSubmit(e) { 10 | e.preventDefault(); 11 | this.props.dispatch(register(this.form.elements.username.value, this.form.elements.password.value)); 12 | } 13 | 14 | render() { 15 | return (
16 |

Register

17 |
this.form = form}> 18 | 19 | 20 | 21 |
22 | Sign in 23 |
); 24 | } 25 | 26 | } 27 | 28 | export default connect()(RegistrationPage); 29 | -------------------------------------------------------------------------------- /src/public/modules/action-creators/index.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse'; 2 | import {register as actionRegister, signIn as actionSignIn} from '../action-repository/index'; 3 | 4 | 5 | export const register = (username, password) => { 6 | 7 | return async function (dispatch) { 8 | 9 | const user = new Parse.User(); 10 | user.set('username', username); 11 | user.set('password', password); 12 | 13 | dispatch(actionRegister(user)); 14 | 15 | try { 16 | 17 | await user.signUp(); 18 | dispatch(actionRegister(Parse.User.current())); 19 | 20 | } catch (exception) { 21 | console.error('REGISTER', exception); 22 | } 23 | 24 | }; 25 | }; 26 | 27 | export const signIn = (username, password) => { 28 | 29 | return async function (dispatch) { 30 | 31 | try { 32 | 33 | await Parse.User.logIn(username, password); 34 | dispatch(actionSignIn(Parse.User.current())); 35 | 36 | } catch (exception) { 37 | console.error('SIGN_IN', exception); 38 | } 39 | 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/public/modules/action-repository/index.js: -------------------------------------------------------------------------------- 1 | import createAction from '../../../common/modules/create-action/index'; 2 | 3 | 4 | export const register = createAction('REGISTER'); 5 | export const signIn = createAction('SIGN_IN'); 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: [ 5 | // Set up an ES6-ish environment 6 | 'babel-polyfill', 7 | 8 | // Add your application's scripts below 9 | './index.js', 10 | ], 11 | devtool: 'source-map', 12 | output: { 13 | path: path.join(__dirname, 'www'), 14 | filename: 'bundle.js' 15 | }, 16 | module: { 17 | preLoaders: [ 18 | { 19 | loader: 'eslint-loader', 20 | exclude: /node_modules/, 21 | test: /\.jsx?$/, 22 | } 23 | ], 24 | loaders: [ 25 | { 26 | loader: 'babel', 27 | exclude: /node_modules/, 28 | test: /\.jsx?$/, 29 | }, 30 | { test: /\.css$/, loader: 'style!css' }, 31 | 32 | ] 33 | }, 34 | eslint: { 35 | configFile: '.eslintrc' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | 5 | module.exports = { 6 | entry: [ 7 | // Set up an ES6-ish environment 8 | 'babel-polyfill', 9 | 10 | // Add your application's scripts below 11 | './index.js', 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'www'), 15 | filename: 'bundle.js' 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | loader: 'babel', 21 | exclude: /node_modules/, 22 | test: /\.jsx?$/, 23 | }, 24 | { test: /\.css$/, loader: 'style!css' }, 25 | 26 | ] 27 | }, 28 | plugins: [ 29 | new webpack.optimize.DedupePlugin(), 30 | new webpack.optimize.UglifyJsPlugin() 31 | ] 32 | }; 33 | --------------------------------------------------------------------------------