From e02844814dc4e1a06575c5722d4cbcc566e5698b Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 10 Aug 2016 23:22:22 -0300 Subject: [PATCH] Ivan - Implement Redux architecture with stores and actions [skip ci] --- client/gulp/tasks/browserify.js | 2 +- client/package.json | 6 +- client/src/actions/common-actions.js | 9 - client/src/actions/config-actions.js | 8 + client/src/actions/session-actions.js | 63 ++++ client/src/actions/user-actions.js | 12 - client/src/app/App.js | 66 ++-- client/src/app/Routes.js | 7 +- client/src/app/index.js | 19 +- .../app/main/dashboard/dashboard-layout.js | 19 +- .../main-home/main-home-page-login-widget.js | 150 ++++----- .../src/app/main/main-home/main-home-page.js | 9 - client/src/app/main/main-layout-header.js | 36 ++- .../main-recover-password-page.js | 81 +++-- .../app/main/main-signup/main-signup-page.js | 55 ++-- client/src/app/store.js | 5 + client/src/data/fixtures/user-fixtures.js | 12 +- client/src/lib-app/api-call.js | 2 +- client/src/lib-app/fixtures-loader.js | 2 +- client/src/lib-app/i18n.js | 4 +- client/src/lib-app/session-store.js | 2 +- client/src/reducers/_reducers.js | 11 + client/src/reducers/config-reducer.js | 29 ++ client/src/reducers/reducer.js | 13 + client/src/reducers/session-reducer.js | 90 ++++++ .../src/stores/__mocks__/common-store-mock.js | 6 - .../src/stores/__mocks__/user-store-mock.js | 6 - .../src/stores/__tests__/user-store-test.js | 289 ------------------ client/src/stores/common-store.js | 29 -- client/src/stores/user-store.js | 131 -------- 30 files changed, 472 insertions(+), 701 deletions(-) delete mode 100644 client/src/actions/common-actions.js create mode 100644 client/src/actions/config-actions.js create mode 100644 client/src/actions/session-actions.js delete mode 100644 client/src/actions/user-actions.js create mode 100644 client/src/app/store.js create mode 100644 client/src/reducers/_reducers.js create mode 100644 client/src/reducers/config-reducer.js create mode 100644 client/src/reducers/reducer.js create mode 100644 client/src/reducers/session-reducer.js delete mode 100644 client/src/stores/__mocks__/common-store-mock.js delete mode 100644 client/src/stores/__mocks__/user-store-mock.js delete mode 100644 client/src/stores/__tests__/user-store-test.js delete mode 100644 client/src/stores/common-store.js delete mode 100644 client/src/stores/user-store.js diff --git a/client/gulp/tasks/browserify.js b/client/gulp/tasks/browserify.js index 949f0761..87726e2a 100644 --- a/client/gulp/tasks/browserify.js +++ b/client/gulp/tasks/browserify.js @@ -38,7 +38,7 @@ function buildScript(file, watch) { bundler.on('update', rebundle); } - bundler.transform(babelify); + bundler.transform(babelify, {'optional': ['es7.classProperties']}); bundler.transform(debowerify); function rebundle() { diff --git a/client/package.json b/client/package.json index be1c6fae..f25dea66 100644 --- a/client/package.json +++ b/client/package.json @@ -17,6 +17,7 @@ }, "devDependencies": { "babel-core": "^5.8.22", + "babel-plugin-transform-class-properties": "^6.11.5", "babel-register": "^6.7.2", "babelify": "^6.1.x", "browser-sync": "^2.7.13", @@ -63,7 +64,10 @@ "react-dom": "^15.0.1", "react-google-recaptcha": "^0.5.2", "react-motion": "^0.3.0", + "react-redux": "^4.4.5", "react-router": "^2.4.0", - "reflux": "^0.4.1" + "react-router-redux": "^4.0.5", + "redux": "^3.5.2", + "redux-promise-middleware": "^3.3.2" } } diff --git a/client/src/actions/common-actions.js b/client/src/actions/common-actions.js deleted file mode 100644 index 8112df05..00000000 --- a/client/src/actions/common-actions.js +++ /dev/null @@ -1,9 +0,0 @@ -import Reflux from 'reflux'; - -let CommonActions = Reflux.createActions([ - 'changeLanguage', - 'logged', - 'loggedOut' -]); - -export default CommonActions; \ No newline at end of file diff --git a/client/src/actions/config-actions.js b/client/src/actions/config-actions.js new file mode 100644 index 00000000..c79ad6ad --- /dev/null +++ b/client/src/actions/config-actions.js @@ -0,0 +1,8 @@ +export default { + changeLanguage(newLanguage) { + return { + type: 'CHANGE_LANGUAGE', + payload: newLanguage + }; + } +}; \ No newline at end of file diff --git a/client/src/actions/session-actions.js b/client/src/actions/session-actions.js new file mode 100644 index 00000000..64fe6ef1 --- /dev/null +++ b/client/src/actions/session-actions.js @@ -0,0 +1,63 @@ +import API from 'lib-app/api-call'; +import sessionStore from 'lib-app/session-store'; +import store from 'app/store'; + +export default { + login(loginData) { + return { + type: 'LOGIN', + payload: API.call({ + path: '/user/login', + data: loginData + }) + }; + }, + + autoLogin() { + const rememberData = sessionStore.getRememberData(); + + return { + type: 'LOGIN_AUTO', + payload: API.call({ + path: '/user/login', + data: { + userId: rememberData.userId, + rememberToken: rememberData.token, + isAutomatic: true + } + }) + }; + }, + + logout() { + return { + type: 'LOG_OUT', + payload: API.call({ + path: '/user/logout', + data: {} + }) + }; + }, + + initSession() { + return { + type: 'CHECK_SESSION', + payload: API.call({ + path: '/user/check-session', + data: {} + }).then((result) => { + if (!result.data.sessionActive) { + if (sessionStore.isRememberDataExpired()) { + store.dispatch(this.logout()); + } else { + store.dispatch(this.autoLogin()); + } + } else { + store.dispatch({ + type: 'SESSION_CHECKED' + }); + } + }) + } + } +}; \ No newline at end of file diff --git a/client/src/actions/user-actions.js b/client/src/actions/user-actions.js deleted file mode 100644 index 26caed17..00000000 --- a/client/src/actions/user-actions.js +++ /dev/null @@ -1,12 +0,0 @@ -import Reflux from 'reflux'; - -const UserActions = Reflux.createActions([ - 'checkLoginStatus', - 'login', - 'logout', - 'signup', - 'sendRecoverPassword', - 'recoverPassword' -]); - -export default UserActions; \ No newline at end of file diff --git a/client/src/app/App.js b/client/src/app/App.js index 2eee2843..a6924e90 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -1,16 +1,44 @@ import React from 'react'; -import Reflux from 'reflux'; +import _ from 'lodash'; +import { connect } from 'react-redux' -import CommonStore from 'stores/common-store'; - -const App = React.createClass({ - - contextTypes: { +class App extends React.Component { + static contextTypes = { router: React.PropTypes.object, location: React.PropTypes.object - }, + }; - mixins: [Reflux.listenTo(CommonStore, 'onCommonStoreChanged')], + constructor(props, context) { + super(props, context); + + if (_.includes(props.location.pathname, '/app/dashboard') && !props.config.logged) { + context.router.push('/app'); + } + + if (!_.includes(props.location.pathname, '/app/dashboard') && props.config.logged) { + context.router.push('/app/dashboard'); + } + } + + componentWillReceiveProps(nextProps) { + const validations = { + languageChanged: nextProps.config.language !== this.props.config.language, + loggedIn: nextProps.session.logged && !this.props.session.logged, + loggedOut: !nextProps.session.logged && this.props.session.logged + }; + + if (validations.languageChanged) { + this.context.router.push(this.props.location.pathname); + } + + if (validations.loggedIn) { + this.context.router.push('/app/dashboard'); + } + + if (validations.loggedOut) { + this.context.router.push('/app'); + } + } render() { return ( @@ -18,19 +46,13 @@ const App = React.createClass({ {React.cloneElement(this.props.children, {})} ); - }, - - onCommonStoreChanged(change) { - let handle = { - 'i18n': () => {this.context.router.push(this.context.location.pathname)}, - 'logged': () => {this.context.router.push('/app/dashboard')}, - 'loggedOut': () => {this.context.router.push('/app')} - }; - - if (handle[change]) { - handle[change](); - } } -}); +} -export default App; +export default connect((store) => { + return { + config: store.config, + session: store.session, + routing: store.routing + }; +})(App); \ No newline at end of file diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js index 71a755fa..ca77c038 100644 --- a/client/src/app/Routes.js +++ b/client/src/app/Routes.js @@ -1,5 +1,8 @@ const React = require('react'); const {Router, Route, IndexRoute, browserHistory} = require('react-router'); +import { syncHistoryWithStore } from 'react-router-redux'; + +import store from 'app/store'; const App = require('app/App'); const DemoPage = require('app/demo/components-demo-page'); @@ -20,8 +23,10 @@ const DashboardEditProfilePage = require('app/main/dashboard/dashboard-edit-prof const DashboardArticlePage = require('app/main/dashboard/dashboard-article/dashboard-article-page'); const DashboardTicketPage = require('app/main/dashboard/dashboard-ticket/dashboard-ticket-page'); +const history = syncHistoryWithStore(browserHistory, store); + export default ( - + diff --git a/client/src/app/index.js b/client/src/app/index.js index e4bec24b..440272bc 100644 --- a/client/src/app/index.js +++ b/client/src/app/index.js @@ -1,9 +1,10 @@ import React from 'react'; import {render} from 'react-dom' -import Router from 'react-router'; -import UserStore from 'stores/user-store'; +import { Provider } from 'react-redux'; +import SessionActions from 'actions/session-actions'; import routes from './Routes'; +import store from './store'; if ( process.env.NODE_ENV !== 'production' ) { // Enable React devtools @@ -14,9 +15,17 @@ if (noFixtures === 'disabled') { require('lib-app/fixtures-loader'); } -let onSessionInit = function () { - render(routes, document.getElementById('app')); +let renderApplication = function () { + render({routes}, document.getElementById('app')); }; -UserStore.initSession().then(onSessionInit, onSessionInit); +store.dispatch(SessionActions.initSession()); +let unsubscribe = store.subscribe(() => { + console.log(store.getState()); + + if (store.getState().session.initDone) { + unsubscribe(); + renderApplication(); + } +}); diff --git a/client/src/app/main/dashboard/dashboard-layout.js b/client/src/app/main/dashboard/dashboard-layout.js index f7e13d1b..80e6b5b6 100644 --- a/client/src/app/main/dashboard/dashboard-layout.js +++ b/client/src/app/main/dashboard/dashboard-layout.js @@ -1,20 +1,15 @@ import React from 'react'; +import {connect} from 'react-redux'; -import UserStore from 'stores/user-store'; -import CommonActions from 'actions/common-actions'; +//import UserStore from 'stores/user-store'; +//import CommonActions from 'actions/common-actions'; import DashboardMenu from 'app/main/dashboard/dashboard-menu'; const DashboardLayout = React.createClass({ - componentWillMount() { - if (!UserStore.isLoggedIn()) { - CommonActions.loggedOut(); - } - }, - render() { - return (UserStore.isLoggedIn()) ? ( + return (this.props.session.logged) ? (
{this.props.children}
@@ -23,4 +18,8 @@ const DashboardLayout = React.createClass({ } }); -export default DashboardLayout; +export default connect((store) => { + return { + session: store.session + }; +})(DashboardLayout); diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index 4ba19b47..a73582a7 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -1,10 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import Reflux from 'reflux'; +import {connect} from 'react-redux'; import classNames from 'classnames'; +import _ from 'lodash'; -import UserActions from 'actions/user-actions'; -import UserStore from 'stores/user-store'; +import SessionActions from 'actions/session-actions'; +import API from 'lib-app/api-call'; import focus from 'lib-core/focus'; import i18n from 'lib-app/i18n'; @@ -17,12 +18,12 @@ import Widget from 'core-components/widget'; import WidgetTransition from 'core-components/widget-transition'; import Message from 'core-components/message'; -let MainHomePageLoginWidget = React.createClass({ - - mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')], +class MainHomePageLoginWidget extends React.Component { - getInitialState() { - return { + constructor(props) { + super(props); + + this.state = { sideToShow: 'front', loginFormErrors: {}, recoverFormErrors: {}, @@ -30,7 +31,13 @@ let MainHomePageLoginWidget = React.createClass({ loadingLogin: false, loadingRecover: false }; - }, + } + + componentDidUpdate(prevProps) { + if (!prevProps.session.failed && this.props.session.failed) { + this.refs.loginForm.refs.password.focus(); + } + } render() { return ( @@ -39,7 +46,7 @@ let MainHomePageLoginWidget = React.createClass({ {this.renderPasswordRecovery()} ); - }, + } renderLogin() { return ( @@ -54,12 +61,12 @@ let MainHomePageLoginWidget = React.createClass({ LOG IN
- ); - }, + } renderPasswordRecovery() { return ( @@ -72,13 +79,13 @@ let MainHomePageLoginWidget = React.createClass({ {i18n('RECOVER_PASSWORD')} - {this.renderRecoverStatus()} ); - }, + } renderRecoverStatus() { let status = null; @@ -92,102 +99,94 @@ let MainHomePageLoginWidget = React.createClass({ } return status; - }, + } getLoginFormProps() { return { - loading: this.state.loadingLogin, + loading: this.props.session.pending, className: 'login-widget__form', ref: 'loginForm', - onSubmit:this.handleLoginFormSubmit, - errors: this.state.loginFormErrors, - onValidateErrors: this.handleLoginFormErrorsValidation + onSubmit: this.onLoginFormSubmit.bind(this), + errors: this.getLoginFormErrors(), + onValidateErrors: this.onLoginFormErrorsValidation.bind(this) }; - }, + } getRecoverFormProps() { return { loading: this.state.loadingRecover, className: 'login-widget__form', ref: 'recoverForm', - onSubmit:this.handleForgotPasswordSubmit, + onSubmit: this.onForgotPasswordSubmit.bind(this), errors: this.state.recoverFormErrors, - onValidateErrors: this.handleRecoverFormErrorsValidation + onValidateErrors: this.onRecoverFormErrorsValidation.bind(this) }; - }, + } - handleLoginFormSubmit(formState) { - UserActions.login(formState); - - this.setState({ - loadingLogin: true + getLoginFormErrors() { + return _.extend({}, this.state.loginFormErrors, { + password: (this.props.session.failed) ? i18n('ERROR_PASSWORD') : null }); - }, + } - handleForgotPasswordSubmit(formState) { - UserActions.sendRecoverPassword(formState); + onLoginFormSubmit(formState) { + this.props.dispatch(SessionActions.login(formState)); + } + onForgotPasswordSubmit(formState) { this.setState({ - loadingRecover: true + loadingRecover: true, + recoverSent: false }); - }, - handleLoginFormErrorsValidation(errors) { + API.call({ + path: '/user/send-recover-password', + data: formState + }).then(this.onRecoverPasswordSent.bind(this)).catch(this.onRecoverPasswordFail.bind(this)); + } + + onLoginFormErrorsValidation(errors) { this.setState({ loginFormErrors: errors }); - }, + } - handleRecoverFormErrorsValidation(errors) { + onRecoverFormErrorsValidation(errors) { this.setState({ recoverFormErrors: errors }); - }, + } - handleForgotPasswordClick() { + onForgotPasswordClick() { this.setState({ sideToShow: 'back' }, this.moveFocusToCurrentSide); - }, + } - handleBackToLoginClick() { + onBackToLoginClick() { this.setState({ sideToShow: 'front', recoverSent: false }, this.moveFocusToCurrentSide); - }, - - onUserStoreChanged(event) { - if (event === 'LOGIN_FAIL') { - this.setState({ - loadingLogin: false, - loginFormErrors: { - password: i18n('ERROR_PASSWORD') - } - }, function () { - this.refs.loginForm.refs.password.focus(); - }.bind(this)); - } + } - if (event === 'SEND_RECOVER_FAIL') { - this.setState({ - loadingRecover: false, - recoverFormErrors: { - email: i18n('EMAIL_NOT_EXIST') - } - }, function () { - this.refs.recoverForm.refs.email.focus(); - }.bind(this)); + onRecoverPasswordSent() { + this.setState({ + loadingRecover: false, + recoverSent: true + }); + } - } - - if (event === 'SEND_RECOVER_SUCCESS') { - this.setState({ - loadingRecover: false, - recoverSent: true - }); - } - }, + onRecoverPasswordFail() { + this.setState({ + loadingRecover: false, + recoverFormErrors: { + email: i18n('EMAIL_NOT_EXIST') + } + }, function () { + this.refs.recoverForm.refs.email.focus(); + }.bind(this)); + } moveFocusToCurrentSide() { let currentWidget; @@ -205,6 +204,11 @@ let MainHomePageLoginWidget = React.createClass({ focus.focusFirstInput(currentWidget); } } -}); +} -export default MainHomePageLoginWidget; + +export default connect((store) => { + return { + session: store.session + }; +})(MainHomePageLoginWidget); diff --git a/client/src/app/main/main-home/main-home-page.js b/client/src/app/main/main-home/main-home-page.js index 57c4bc7b..c8d3f7f7 100644 --- a/client/src/app/main/main-home/main-home-page.js +++ b/client/src/app/main/main-home/main-home-page.js @@ -3,16 +3,7 @@ import React from 'react'; import MainHomePageLoginWidget from 'app/main/main-home/main-home-page-login-widget'; import MainHomePagePortal from 'app/main/main-home/main-home-page-portal'; -import CommonActions from 'actions/common-actions'; -import UserStore from 'stores/user-store'; - const MainHomePage = React.createClass({ - - componentWillMount() { - if (UserStore.isLoggedIn()) { - CommonActions.logged(); - } - }, render() { return ( diff --git a/client/src/app/main/main-layout-header.js b/client/src/app/main/main-layout-header.js index d8a52883..1ab6c5f8 100644 --- a/client/src/app/main/main-layout-header.js +++ b/client/src/app/main/main-layout-header.js @@ -1,9 +1,9 @@ import React from 'react'; +import { connect } from 'react-redux' import i18n from 'lib-app/i18n'; -import CommonActions from 'actions/common-actions'; -import UserActions from 'actions/user-actions'; -import UserStore from 'stores/user-store'; +import SessionActions from 'actions/user-actions'; +import ConfigActions from 'actions/config-actions'; import Button from 'core-components/button'; import DropDown from 'core-components/drop-down'; @@ -18,23 +18,24 @@ let codeLanguages = { 'Indian': 'in' }; -let MainLayoutHeader = React.createClass({ +class MainLayoutHeader extends React.Component { render() { return (
{this.renderAccessLinks()} - +
); - }, + } renderAccessLinks() { let result; - if (UserStore.isLoggedIn()) { + + if (this.props.session.logged) { result = (
- Welcome, pepito + Welcome, John
); @@ -48,7 +49,7 @@ let MainLayoutHeader = React.createClass({ } return result; - }, + } getLanguageList() { return Object.keys(codeLanguages).map((language) => { @@ -57,17 +58,22 @@ let MainLayoutHeader = React.createClass({ icon: codeLanguages[language] }; }); - }, + } changeLanguage(event) { let language = Object.keys(codeLanguages)[event.index]; - CommonActions.changeLanguage(codeLanguages[language]); - }, + this.props.dispatch(ConfigActions.changeLanguage(codeLanguages[language])); + } logout() { - UserActions.logout(); + this.props.dispatch(SessionActions.logout()); } -}); +} -export default MainLayoutHeader; +export default connect((store) => { + return { + session: store.session, + config: store.config + }; +})(MainLayoutHeader); diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.js b/client/src/app/main/main-recover-password/main-recover-password-page.js index 0909f457..4c6580ae 100644 --- a/client/src/app/main/main-recover-password/main-recover-password-page.js +++ b/client/src/app/main/main-recover-password/main-recover-password-page.js @@ -1,11 +1,8 @@ import React from 'react'; -import Reflux from 'reflux'; import _ from 'lodash'; -import CommonActions from 'actions/common-actions'; -import UserActions from 'actions/user-actions'; -import UserStore from 'stores/user-store'; import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; import Widget from 'core-components/widget'; import Form from 'core-components/form'; @@ -13,37 +10,27 @@ import Input from 'core-components/input'; import SubmitButton from 'core-components/submit-button'; import Message from 'core-components/message'; -const MainRecoverPasswordPage = React.createClass({ +class MainRecoverPasswordPage extends React.Component { - mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')], - - propTypes: { + static propTypes = { location: React.PropTypes.object, router: React.PropTypes.object - }, + }; - componentWillMount() { - if (UserStore.isLoggedIn()) { - CommonActions.logged(); - } + constructor(props) { + super(props); - if (!this.props.location.query.token || !this.props.location.query.email) { - CommonActions.loggedOut(); - } - }, - - getInitialState() { - return { + this.state = { recoverStatus: 'waiting', loading: false - }; - }, + } + } render() { return (
-
+
@@ -56,7 +43,7 @@ const MainRecoverPasswordPage = React.createClass({
); - }, + } renderRecoverStatus() { switch (this.state.recoverStatus) { @@ -67,33 +54,39 @@ const MainRecoverPasswordPage = React.createClass({ case 'waiting': return null; } - }, + } - handleRecoverPasswordSubmit(formState) { + onRecoverPasswordSubmit(formState) { let recoverData = _.clone(formState); recoverData.token = this.props.location.query.token; recoverData.email = this.props.location.query.email; - UserActions.recoverPassword(recoverData); this.setState({ loading: true - }); - }, - - onUserStoreChanged(event) { - if (event === 'VALID_RECOVER') { - setTimeout(CommonActions.loggedOut, 2000); - this.setState({ - recoverStatus: 'valid', - loading: false - }); - } else { - this.setState({ - recoverStatus: 'invalid', - loading: false - }); - } + }, this.callRecoverPassword.bind(this, recoverData)); } -}); + + callRecoverPassword(recoverData) { + API.call({ + path: '/user/recover-password', + data: recoverData + }).then(this.onPasswordRecovered.bind(this)).catch(this.onPasswordRecoverFail.bind(this)); + } + + onPasswordRecovered() { + setTimeout(() => {this.props.history.push('/app')}, 2000); + this.setState({ + recoverStatus: 'valid', + loading: false + }); + } + + onPasswordRecoverFail() { + this.setState({ + recoverStatus: 'invalid', + loading: false + }); + } +} export default MainRecoverPasswordPage; \ No newline at end of file diff --git a/client/src/app/main/main-signup/main-signup-page.js b/client/src/app/main/main-signup/main-signup-page.js index 490bc060..44c0e987 100644 --- a/client/src/app/main/main-signup/main-signup-page.js +++ b/client/src/app/main/main-signup/main-signup-page.js @@ -1,11 +1,8 @@ import React from 'react'; -import Reflux from 'reflux'; import ReCAPTCHA from 'react-google-recaptcha'; -import CommonActions from 'actions/common-actions'; -import UserActions from 'actions/user-actions'; -import UserStore from 'stores/user-store'; import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; import SubmitButton from 'core-components/submit-button'; import Message from 'core-components/message'; @@ -14,22 +11,16 @@ import Input from 'core-components/input'; import Widget from 'core-components/widget'; -let MainSignUpPageWidget = React.createClass({ - - mixins: [Reflux.listenTo(UserStore, 'onUserStoreChanged')], +class MainSignUpPageWidget extends React.Component { - componentDidMount() { - if (UserStore.isLoggedIn()) { - CommonActions.logged(); - } - }, + constructor(props) { + super(props); - getInitialState() { - return { + this.state = { loading: false, email: null }; - }, + } render() { return ( @@ -52,7 +43,7 @@ let MainSignUpPageWidget = React.createClass({
); - }, + } renderMessage() { switch (this.state.message) { @@ -63,37 +54,47 @@ let MainSignUpPageWidget = React.createClass({ default: return null; } - }, + } getFormProps() { return { loading: this.state.loading, className: 'signup-widget__form', - onSubmit: this.handleLoginFormSubmit + onSubmit: this.onLoginFormSubmit.bind(this) }; - }, + } getInputProps() { return { inputType: 'secondary', className: 'signup-widget__input' }; - }, + } - handleLoginFormSubmit(formState) { + onLoginFormSubmit(formState) { this.setState({ loading: true }); - UserActions.signup(formState); - }, - - onUserStoreChanged(event) { + API.call({ + path: '/user/signup', + data: formState + }).then(this.onSignupSuccess.bind(this)).catch(this.onSignupFail.bind(this)); + } + + onSignupSuccess() { this.setState({ loading: false, - message: (event === 'SIGNUP_FAIL') ? 'fail': 'success' + message: 'success' }); } -}); + + onSignupFail() { + this.setState({ + loading: false, + message: 'fail' + }); + } +} export default MainSignUpPageWidget; \ No newline at end of file diff --git a/client/src/app/store.js b/client/src/app/store.js new file mode 100644 index 00000000..5b8bfd37 --- /dev/null +++ b/client/src/app/store.js @@ -0,0 +1,5 @@ +import { createStore, applyMiddleware } from 'redux'; +import promise from 'redux-promise-middleware'; +import reducers from 'reducers/_reducers'; + +export default createStore(reducers, applyMiddleware(promise())); \ No newline at end of file diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index 78fdbd78..59a49753 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -1,6 +1,6 @@ module.exports = [ { - path: 'user/login', + path: '/user/login', time: 1000, response: function (data) { let response; @@ -26,7 +26,7 @@ module.exports = [ } }, { - path: 'user/logout', + path: '/user/logout', time: 100, response: function () { return { @@ -36,7 +36,7 @@ module.exports = [ } }, { - path: 'user/check-session', + path: '/user/check-session', time: 100, response: function () { return { @@ -48,7 +48,7 @@ module.exports = [ } }, { - path: 'user/send-recover-password', + path: '/user/send-recover-password', time: 2000, response: function (data) { @@ -67,7 +67,7 @@ module.exports = [ } }, { - path: 'user/recover-password', + path: '/user/recover-password', time: 1000, response: function (data) { @@ -86,7 +86,7 @@ module.exports = [ } }, { - path: 'user/signup', + path: '/user/signup', time: 1000, response: function (data) { diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js index 89d26ce1..d3488819 100644 --- a/client/src/lib-app/api-call.js +++ b/client/src/lib-app/api-call.js @@ -2,7 +2,7 @@ const _ = require('lodash'); const APIUtils = require('lib-core/APIUtils'); const SessionStore = require('lib-app/session-store'); -const root = 'http://localhost:3000/api/'; +const root = 'http://localhost:3000/api'; function processData (data) { return _.extend(SessionStore.getSessionData(), data); diff --git a/client/src/lib-app/fixtures-loader.js b/client/src/lib-app/fixtures-loader.js index fe8b9fc3..113c8d9d 100644 --- a/client/src/lib-app/fixtures-loader.js +++ b/client/src/lib-app/fixtures-loader.js @@ -21,7 +21,7 @@ fixtures.add(require('data/fixtures/user-fixtures')); _.each(fixtures.getAll(), function (fixture) { mockjax({ contentType: 'application/json', - url: 'http://localhost:3000/api/' + fixture.path, + url: 'http://localhost:3000/api' + fixture.path, responseTime: fixture.time || 500, response: function (settings) { this.responseText = fixture.response(settings.data); diff --git a/client/src/lib-app/i18n.js b/client/src/lib-app/i18n.js index 8334c99c..731f5a7f 100644 --- a/client/src/lib-app/i18n.js +++ b/client/src/lib-app/i18n.js @@ -1,12 +1,12 @@ import MessageFormat from 'messageformat'; -import CommonStore from 'stores/common-store'; +import store from 'app/store'; import i18nData from 'data/i18n-data'; let mf = new MessageFormat('en'); let i18n = function (key, params = null) { - let i18nKey = i18nData(key, CommonStore.language); + let i18nKey = i18nData(key, store.getState().config.language); let message = mf.compile(i18nKey); return message(params); diff --git a/client/src/lib-app/session-store.js b/client/src/lib-app/session-store.js index 8fbce685..bb05c992 100644 --- a/client/src/lib-app/session-store.js +++ b/client/src/lib-app/session-store.js @@ -6,7 +6,7 @@ class SessionStore { this.storage = LocalStorage; if (!this.getItem('language')) { - this.setItem('language', 'english'); + this.setItem('language', 'us'); } } diff --git a/client/src/reducers/_reducers.js b/client/src/reducers/_reducers.js new file mode 100644 index 00000000..b5745f14 --- /dev/null +++ b/client/src/reducers/_reducers.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux'; +import { routerReducer } from 'react-router-redux'; + +import sessionReducer from 'reducers/session-reducer'; +import configReducer from 'reducers/config-reducer'; + +export default combineReducers({ + session: sessionReducer, + config: configReducer, + routing: routerReducer +}); \ No newline at end of file diff --git a/client/src/reducers/config-reducer.js b/client/src/reducers/config-reducer.js new file mode 100644 index 00000000..4fcf57a5 --- /dev/null +++ b/client/src/reducers/config-reducer.js @@ -0,0 +1,29 @@ +import _ from 'lodash'; + +import Reducer from 'reducers/reducer'; +import sessionStore from 'lib-app/session-store'; + +class ConfigReducer extends Reducer { + + getInitialState() { + return { + language: sessionStore.getItem('language') + }; + } + + getTypeHandlers() { + return { + 'CHANGE_LANGUAGE': this.onLanguageChange + }; + } + + onLanguageChange(state, payload) { + sessionStore.setItem('language', payload); + + return _.extend({}, state, { + language: payload + }); + } +} + +export default ConfigReducer.getInstance(); \ No newline at end of file diff --git a/client/src/reducers/reducer.js b/client/src/reducers/reducer.js new file mode 100644 index 00000000..7fee619e --- /dev/null +++ b/client/src/reducers/reducer.js @@ -0,0 +1,13 @@ +class Reducer { + static getInstance() { + let reducer = new this(); + + return (state = reducer.getInitialState(), action) => { + const actionHandler = reducer.getTypeHandlers()[action.type]; + + return (actionHandler) ? actionHandler(state, action.payload) : state; + }; + } +} + +export default Reducer; \ No newline at end of file diff --git a/client/src/reducers/session-reducer.js b/client/src/reducers/session-reducer.js new file mode 100644 index 00000000..428331a9 --- /dev/null +++ b/client/src/reducers/session-reducer.js @@ -0,0 +1,90 @@ +import _ from 'lodash'; +import Reducer from 'reducers/reducer'; +import sessionStore from 'lib-app/session-store'; + +class SessionReducer extends Reducer { + + getInitialState() { + return { + initDone: false, + logged: false, + pending: false, + failed: false + }; + } + + getTypeHandlers() { + return { + 'LOGIN_PENDING': this.onLoginPending, + 'LOGIN_FULFILLED': this.onLoginCompleted, + 'LOGIN_REJECTED': this.onLoginFailed, + 'LOGOUT_FULFILLED': this.onLogout, + 'CHECK_SESSION_REJECTED': (state) => { return _.extend({}, state, {initDone: true})}, + 'SESSION_CHECKED': (state) => { return _.extend({}, state, {initDone: true})}, + 'LOGIN_AUTO_FULFILLED': this.onAutoLogin, + 'LOGIN_AUTO_REJECTED': this.onAutoLoginFail + }; + } + + onLoginPending(state) { + return _.extend({}, state, { + logged: false, + pending: true, + failed: false + }); + } + + onLoginCompleted(state, payload) { + if (payload.data.rememberToken) { + sessionStore.storeRememberData({ + token: payload.data.rememberToken, + userId: payload.data.userId, + expiration: payload.data.rememberExpiration + }); + } else { + sessionStore.createSession(payload.data.userId, payload.data.token); + } + + return _.extend({}, state, { + logged: true, + pending: false, + failed: false + }); + } + + onLoginFailed(state) { + return _.extend({}, state, { + logged: false, + pending: false, + failed: true + }); + } + + onLogout(state) { + sessionStore.closeSession(); + sessionStore.clearRememberData(); + + return _.extend({}, state, { + logged: false, + pending: false, + failed: false + }); + } + + onAutoLogin() { + return _.extend({}, state, { + initDone: true + }); + } + + onAutoLoginFail() { + sessionStore.closeSession(); + sessionStore.clearRememberData(); + + return _.extend({}, state, { + initDone: true + }); + } +} + +export default SessionReducer.getInstance(); \ No newline at end of file diff --git a/client/src/stores/__mocks__/common-store-mock.js b/client/src/stores/__mocks__/common-store-mock.js deleted file mode 100644 index 1a488e3d..00000000 --- a/client/src/stores/__mocks__/common-store-mock.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - changeLanguage: stub(), - logged: stub(), - loggedOut: stub(), - listen: stub() -}; \ No newline at end of file diff --git a/client/src/stores/__mocks__/user-store-mock.js b/client/src/stores/__mocks__/user-store-mock.js deleted file mode 100644 index abba4ba6..00000000 --- a/client/src/stores/__mocks__/user-store-mock.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - loginUser: stub(), - logoutUser: stub(), - isLoggedIn: stub().returns(false), - listen: stub() -}; \ No newline at end of file diff --git a/client/src/stores/__tests__/user-store-test.js b/client/src/stores/__tests__/user-store-test.js deleted file mode 100644 index d3af00b2..00000000 --- a/client/src/stores/__tests__/user-store-test.js +++ /dev/null @@ -1,289 +0,0 @@ -// MOCKS -const CommonActions = require('actions/__mocks__/common-actions-mock'); -const SessionStore = require('lib-app/__mocks__/session-store-mock'); -const API = require('lib-app/__mocks__/api-call-mock'); -const UserActions = { - checkLoginStatus: {listen: stub()}, - login: {listen: stub()}, - logout: {listen: stub()} -}; - -const UserStore = requireUnit('stores/user-store', { - 'actions/user-actions': UserActions, - 'actions/common-actions': CommonActions, - 'lib-app/session-store': SessionStore, - 'lib-app/api-call': API -}); - -describe('UserStore', function () { - it ('should inform is the user is logged based on SessionStores\' info', function () { - SessionStore.isLoggedIn.returns(true); - expect(UserStore.isLoggedIn()).to.equal(true); - SessionStore.isLoggedIn.returns(false); - expect(UserStore.isLoggedIn()).to.equal(false); - }); - - describe('when login user', function () { - it('should call /user/login api path', function () { - let mockLoginData = {email: 'mock', password: 'mock'}; - - UserStore.loginUser(mockLoginData); - expect(API.call).to.have.been.calledWith({ - path: 'user/login', - data: mockLoginData - }); - }); - - it('should create session, trigger success event and inform common action when having a successful login', function () { - let mockLoginData = {email: 'mock', password: 'mock'}; - let mockSuccessData = { - status: 'success', - data: { - userId: 12, - token: 'RANDOM_TOKEN' - } - }; - - spy(UserStore, 'trigger'); - CommonActions.logged.reset(); - SessionStore.createSession.reset(); - API.call.returns({ - then: (resolve) => {resolve(mockSuccessData)} - }); - - UserStore.loginUser(mockLoginData); - - expect(SessionStore.storeRememberData).to.have.not.been.called; - expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN'); - expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS'); - expect(CommonActions.logged).to.have.been.called; - UserStore.trigger.restore(); - }); - - it('should trigger fail event if login fails', function () { - let mockLoginData = {email: 'mock', password: 'mock'}; - - spy(UserStore, 'trigger'); - API.call.returns({ - then: (resolve, reject) => {reject()} - }); - - UserStore.loginUser(mockLoginData); - - expect(UserStore.trigger).to.have.been.calledWith('LOGIN_FAIL'); - UserStore.trigger.restore(); - }); - - it('should store remember data if remember is true', function () { - let mockLoginData = {email: 'mock', password: 'mock', remember: true}; - let mockSuccessData = { - status: 'success', - data: { - userId: 12, - token: 'RANDOM_TOKEN', - rememberToken: 'RANDOM_TOKEN_2', - rememberExpiration: 20150822 - } - }; - - spy(UserStore, 'trigger'); - CommonActions.logged.reset(); - SessionStore.createSession.reset(); - API.call.returns({ - then: (resolve) => {resolve(mockSuccessData)} - }); - - UserStore.loginUser(mockLoginData); - - expect(SessionStore.storeRememberData).to.have.been.calledWith({ - token: 'RANDOM_TOKEN_2', - userId: 12, - expiration: 20150822 - }); - expect(SessionStore.createSession).to.have.been.calledWith(12, 'RANDOM_TOKEN'); - expect(UserStore.trigger).to.have.been.calledWith('LOGIN_SUCCESS'); - expect(CommonActions.logged).to.have.been.called; - UserStore.trigger.restore(); - }); - }); - - describe('when login out', function () { - - it('should call /user/logout api path', function () { - API.call = stub().returns({ - then: (resolve) => {resolve()} - }); - - UserStore.logoutUser(); - expect(API.call).to.have.been.calledWith({ - path: 'user/logout' - }); - }); - - it('should delete session, trigger LOGOUT event and inform common action of logout', function () { - API.call = stub().returns({ - then: (resolve) => {resolve()} - }); - spy(UserStore, 'trigger'); - - UserStore.logoutUser(); - expect(SessionStore.closeSession).to.have.been.called; - expect(UserStore.trigger).to.have.been.calledWith('LOGOUT'); - expect(CommonActions.loggedOut).to.have.been.called; - UserStore.trigger.restore() - }) - }); - - describe('when calling initSession', function () {{ - - it('should check if session is active in the API', function () { - let mockSuccessData = { - status: 'success', - data: { - sessionActive: true - } - }; - API.call = stub().returns({ - then: (resolve) => {resolve(mockSuccessData)} - }); - - UserStore.initSession(); - - expect(API.call).to.have.been.calledWith({ - path: 'user/check-session', - data: {} - }); - }); - - describe('and no session is active', function () { - beforeEach(function () { - let mockSuccessData = { - status: 'success', - data: { - sessionActive: false - } - }; - API.call = stub().returns({ - then: (resolve) => {resolve(mockSuccessData)} - }); - }); - - it('should log out and delete remember data if expired', function () { - SessionStore.isRememberDataExpired.returns(true); - SessionStore.clearRememberData.reset(); - - UserStore.initSession(); - - expect(SessionStore.clearRememberData).to.have.been.called; - expect(SessionStore.closeSession).to.have.been.called; - expect(CommonActions.loggedOut).to.have.been.called; - }); - - it('should login with remember data', function () { - SessionStore.isRememberDataExpired.returns(false); - SessionStore.getRememberData.returns({ - userId: 'REMEMBER_USER_ID', - token: 'REMEMBER_TOKEN', - expiration: 20160721 - }); - - UserStore.initSession(); - - expect(API.call).to.have.been.calledWithMatch({ - path: 'user/login', - data: { - userId: 'REMEMBER_USER_ID', - rememberToken: 'REMEMBER_TOKEN' - } - }); - }); - }); - }}); - - describe('when recovering password', function () { - beforeEach(function () { - let mockSuccessData = { - status: 'success', - data: {} - }; - API.call = stub().returns({ - then: (resolve) => {resolve(mockSuccessData)} - }); - spy(UserStore, 'trigger'); - }); - - afterEach(function () { - UserStore.trigger.restore(); - }); - - it('should send recover password', function () { - UserStore.sendRecoverPassword({ - email: 'SOME_EMAIL' - }); - - expect(API.call).to.have.been.calledWithMatch({ - path: 'user/send-recover-password', - data: { - email: 'SOME_EMAIL' - } - }); - expect(UserStore.trigger).to.have.been.calledWith('SEND_RECOVER_SUCCESS'); - }); - - it('should trigger fail if send recover fails', function () { - API.call = stub().returns({ - then: (resolve, reject) => {reject({ status: 'fail'})} - }); - UserStore.sendRecoverPassword({ - email: 'SOME_EMAIL' - }); - - expect(API.call).to.have.been.calledWithMatch({ - path: 'user/send-recover-password', - data: { - email: 'SOME_EMAIL' - } - }); - expect(UserStore.trigger).to.have.been.calledWith('SEND_RECOVER_FAIL'); - }); - - it('should recover password', function () { - UserStore.recoverPassword({ - email: 'SOME_EMAIL', - token: 'SOME_TOKEN', - password: 'SOME_PASSWORD' - }); - - expect(API.call).to.have.been.calledWithMatch({ - path: 'user/recover-password', - data: { - email: 'SOME_EMAIL', - token: 'SOME_TOKEN', - password: 'SOME_PASSWORD' - } - }); - expect(UserStore.trigger).to.have.been.calledWith('VALID_RECOVER'); - }); - - it('should trigger fail if recover password fails', function () { - API.call = stub().returns({ - then: (resolve, reject) => {reject({ status: 'fail'})} - }); - UserStore.recoverPassword({ - email: 'SOME_EMAIL', - token: 'SOME_TOKEN', - password: 'SOME_PASSWORD' - }); - - expect(API.call).to.have.been.calledWithMatch({ - path: 'user/recover-password', - data: { - email: 'SOME_EMAIL', - token: 'SOME_TOKEN', - password: 'SOME_PASSWORD' - } - }); - expect(UserStore.trigger).to.have.been.calledWith('INVALID_RECOVER'); - }); - }); -}); diff --git a/client/src/stores/common-store.js b/client/src/stores/common-store.js deleted file mode 100644 index b4a82de7..00000000 --- a/client/src/stores/common-store.js +++ /dev/null @@ -1,29 +0,0 @@ -import Reflux from 'reflux'; - -import CommonActions from 'actions/common-actions'; - -let CommonStore = Reflux.createStore({ - - init() { - this.language = 'us'; - - this.listenTo(CommonActions.changeLanguage, this.changeLanguage); - this.listenTo(CommonActions.logged, this.logged); - this.listenTo(CommonActions.loggedOut, this.loggedOut); - }, - - changeLanguage(lang) { - this.language = lang; - this.trigger('i18n'); - }, - - logged() { - this.trigger('logged'); - }, - - loggedOut() { - this.trigger('loggedOut'); - } -}); - -export default CommonStore; diff --git a/client/src/stores/user-store.js b/client/src/stores/user-store.js deleted file mode 100644 index 800a4bd7..00000000 --- a/client/src/stores/user-store.js +++ /dev/null @@ -1,131 +0,0 @@ -const Reflux = require('reflux'); -const API = require('lib-app/api-call'); -const sessionStore = require('lib-app/session-store'); - -const UserActions = require('actions/user-actions'); -const CommonActions = require('actions/common-actions'); - -const UserStore = Reflux.createStore({ - - init() { - this.user = null; - - this.listenTo(UserActions.checkLoginStatus, this.checkLoginStatus); - this.listenTo(UserActions.login, this.loginUser); - this.listenTo(UserActions.signup, this.signupUser); - this.listenTo(UserActions.logout, this.logoutUser); - this.listenTo(UserActions.recoverPassword, this.recoverPassword); - this.listenTo(UserActions.sendRecoverPassword, this.sendRecoverPassword); - }, - - initSession() { - return API.call({ - path: 'user/check-session', - data: {} - }).then(this.tryLoginIfSessionIsInactive); - }, - - tryLoginIfSessionIsInactive(result) { - if (!result.data.sessionActive) { - if (sessionStore.isRememberDataExpired()) { - return this.logoutUser(); - } else { - return this.loginWithRememberData(); - } - } - }, - - signupUser(signupData) { - return API.call({ - path: 'user/signup', - data: signupData - }).then(this.handleSignupSuccess, this.handleSignupFail); - }, - - loginUser(loginData) { - let onSuccessLogin = (loginData.remember) ? this.handleLoginSuccessWithRemember : this.handleLoginSuccess; - let onFailedLogin = (loginData.isAutomatic) ? null : this.handleLoginFail; - - return API.call({ - path: 'user/login', - data: loginData - }).then(onSuccessLogin, onFailedLogin); - }, - - logoutUser() { - return API.call({ - path: 'user/logout' - }).then(() => { - sessionStore.closeSession(); - sessionStore.clearRememberData(); - CommonActions.loggedOut(); - this.trigger('LOGOUT'); - }); - }, - - sendRecoverPassword(recoverData) { - return API.call({ - path: 'user/send-recover-password', - data: recoverData - }).then(() => { - this.trigger('SEND_RECOVER_SUCCESS'); - }, () => { - this.trigger('SEND_RECOVER_FAIL') - }); - }, - - recoverPassword(recoverData) { - return API.call({ - path: 'user/recover-password', - data: recoverData - }).then(() => { - this.trigger('VALID_RECOVER'); - }, () => { - this.trigger('INVALID_RECOVER') - }); - }, - - isLoggedIn() { - return sessionStore.isLoggedIn(); - }, - - loginWithRememberData() { - let rememberData = sessionStore.getRememberData(); - - return this.loginUser({ - userId: rememberData.userId, - rememberToken: rememberData.token, - isAutomatic: true - }); - }, - - handleLoginSuccessWithRemember(result) { - sessionStore.storeRememberData({ - token: result.data.rememberToken, - userId: result.data.userId, - expiration: result.data.rememberExpiration - }); - - this.handleLoginSuccess(result) - }, - - handleLoginSuccess(result) { - sessionStore.createSession(result.data.userId, result.data.token); - CommonActions.logged(); - this.trigger('LOGIN_SUCCESS'); - }, - - handleLoginFail() { - this.trigger('LOGIN_FAIL'); - }, - - handleSignupSuccess() { - this.trigger('SIGNUP_SUCCESS'); - }, - - handleSignupFail() { - this.trigger('SIGNUP_FAIL'); - } -}); - -export default UserStore; \ No newline at end of file