From 2400ea04c5d84c3d76b0f7b174034d1673a274e2 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 28 Oct 2016 22:14:39 -0700 Subject: [PATCH] feat(app): render spinner on /settings --- client/less/main.less | 1 + client/less/sk-wave.less | 36 ++++++ common/app/App.jsx | 67 +++++------ common/app/components/Nav/Nav.jsx | 41 +++---- common/app/components/SK-Wave.jsx | 19 ++++ common/app/redux/reducer.js | 6 +- .../routes/settings/components/Settings.jsx | 105 +++++++++++------- common/app/routes/settings/index.js | 7 -- 8 files changed, 177 insertions(+), 105 deletions(-) create mode 100644 client/less/sk-wave.less create mode 100644 common/app/components/SK-Wave.jsx diff --git a/client/less/main.less b/client/less/main.less index 0a54914552..e1400a0ad3 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -1151,3 +1151,4 @@ and (max-width : 400px) { @import "toastr.less"; @import "map.less"; @import "drawers.less"; +@import "sk-wave.less"; diff --git a/client/less/sk-wave.less b/client/less/sk-wave.less new file mode 100644 index 0000000000..2427c92cb8 --- /dev/null +++ b/client/less/sk-wave.less @@ -0,0 +1,36 @@ +// original source: +// https://github.com/tobiasahlin/SpinKit +@duration: 3s; +@delayRange: 1s; + +.create-wave-child(@numOfCol, @iter: 2) when (@iter <= @numOfCol) { + div:nth-child(@{iter}) { + animation-delay: -(@duration - (@delayRange / (@numOfCol - 1)) * (@iter - 1)); + } + .create-wave-child(@numOfCol, (@iter + 1)); +} + +.sk-wave { + height: 100px; + margin: 100px auto; + text-align: center; + width: 50px; + > div { + animation: sk-stretchdelay @duration infinite ease-in-out; + background-color: @brand-primary; + display: inline-block; + height: 100%; + margin-right: 2px; + width: 6px; + } + .create-wave-child(5) +} + +@keyframes sk-stretchdelay { + 0%, 40%, 100% { + transform: scaleY(0.4); + } + 20% { + transform: scaleY(1.0); + } +} diff --git a/common/app/App.jsx b/common/app/App.jsx index de4fb7722d..1f566ea8e7 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -21,7 +21,7 @@ import Nav from './components/Nav'; import Toasts from './toasts/Toasts.jsx'; import { userSelector } from './redux/selectors'; -const bindableActions = { +const mapDispatchToProps = { initWindowHeight, updateNavHeight, fetchUser, @@ -35,14 +35,14 @@ const bindableActions = { const mapStateToProps = createSelector( userSelector, - state => state.app.shouldShowSignIn, + state => state.app.isSignInAttempted, state => state.app.toast, state => state.app.isMapDrawerOpen, state => state.app.isMapAlreadyLoaded, state => state.challengesApp.toast, ( { user: { username, points, picture } }, - shouldShowSignIn, + isSignInAttempted, toast, isMapDrawerOpen, isMapAlreadyLoaded, @@ -51,41 +51,38 @@ const mapStateToProps = createSelector( points, picture, toast, - shouldShowSignIn, + showLoading: !isSignInAttempted, isMapDrawerOpen, isMapAlreadyLoaded, isSignedIn: !!username }) ); +const propTypes = { + children: PropTypes.node, + username: PropTypes.string, + isSignedIn: PropTypes.bool, + points: PropTypes.number, + picture: PropTypes.string, + toast: PropTypes.object, + updateNavHeight: PropTypes.func, + initWindowHeight: PropTypes.func, + submitChallenge: PropTypes.func, + isMapDrawerOpen: PropTypes.bool, + isMapAlreadyLoaded: PropTypes.bool, + toggleMapDrawer: PropTypes.func, + toggleMainChat: PropTypes.func, + fetchUser: PropTypes.func, + showLoading: PropTypes.bool, + params: PropTypes.object, + updateAppLang: PropTypes.func.isRequired, + trackEvent: PropTypes.func.isRequired, + loadCurrentChallenge: PropTypes.func.isRequired +}; +const contextTypes = { router: PropTypes.object }; + // export plain class for testing export class FreeCodeCamp extends React.Component { - static displayName = 'FreeCodeCamp'; - static contextTypes = { - router: PropTypes.object - }; - static propTypes = { - children: PropTypes.node, - username: PropTypes.string, - isSignedIn: PropTypes.bool, - points: PropTypes.number, - picture: PropTypes.string, - toast: PropTypes.object, - updateNavHeight: PropTypes.func, - initWindowHeight: PropTypes.func, - submitChallenge: PropTypes.func, - isMapDrawerOpen: PropTypes.bool, - isMapAlreadyLoaded: PropTypes.bool, - toggleMapDrawer: PropTypes.func, - toggleMainChat: PropTypes.func, - fetchUser: PropTypes.func, - shouldShowSignIn: PropTypes.bool, - params: PropTypes.object, - updateAppLang: PropTypes.func.isRequired, - trackEvent: PropTypes.func.isRequired, - loadCurrentChallenge: PropTypes.func.isRequired - }; - componentWillReceiveProps(nextProps) { if (this.props.params.lang !== nextProps.params.lang) { this.props.updateAppLang(nextProps.params.lang); @@ -125,7 +122,7 @@ export class FreeCodeCamp extends React.Component { isMapAlreadyLoaded, toggleMapDrawer, toggleMainChat, - shouldShowSignIn, + showLoading, params: { lang }, trackEvent, loadCurrentChallenge @@ -138,7 +135,7 @@ export class FreeCodeCamp extends React.Component { updateNavHeight, toggleMapDrawer, toggleMainChat, - shouldShowSignIn, + showLoading, trackEvent, loadCurrentChallenge }; @@ -160,7 +157,11 @@ export class FreeCodeCamp extends React.Component { } } +FreeCodeCamp.displayName = 'FreeCodeCamp'; +FreeCodeCamp.contextTypes = contextTypes; +FreeCodeCamp.propTypes = propTypes; + export default connect( mapStateToProps, - bindableActions + mapDispatchToProps )(FreeCodeCamp); diff --git a/common/app/components/Nav/Nav.jsx b/common/app/components/Nav/Nav.jsx index feac803b52..666a1c912d 100644 --- a/common/app/components/Nav/Nav.jsx +++ b/common/app/components/Nav/Nav.jsx @@ -29,7 +29,21 @@ function handleNavLinkEvent(content) { }); } -export default class extends React.Component { +const propTypes = { + points: PropTypes.number, + picture: PropTypes.string, + signedIn: PropTypes.bool, + username: PropTypes.string, + isOnMap: PropTypes.bool, + updateNavHeight: PropTypes.func, + toggleMapDrawer: PropTypes.func, + toggleMainChat: PropTypes.func, + showLoading: PropTypes.bool, + trackEvent: PropTypes.func.isRequired, + loadCurrentChallenge: PropTypes.func.isRequired +}; + +export default class FCCNav extends React.Component { constructor(...props) { super(...props); this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this); @@ -38,20 +52,6 @@ export default class extends React.Component { this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content); }); } - static displayName = 'Nav'; - static propTypes = { - points: PropTypes.number, - picture: PropTypes.string, - signedIn: PropTypes.bool, - username: PropTypes.string, - isOnMap: PropTypes.bool, - updateNavHeight: PropTypes.func, - toggleMapDrawer: PropTypes.func, - toggleMainChat: PropTypes.func, - shouldShowSignIn: PropTypes.bool, - trackEvent: PropTypes.func.isRequired, - loadCurrentChallenge: PropTypes.func.isRequired - }; componentDidMount() { const navBar = ReactDOM.findDOMNode(this); @@ -165,8 +165,8 @@ export default class extends React.Component { }); } - renderSignIn(username, points, picture, shouldShowSignIn) { - if (!shouldShowSignIn) { + renderSignIn(username, points, picture, showLoading) { + if (showLoading) { return null; } if (username) { @@ -198,7 +198,7 @@ export default class extends React.Component { isOnMap, toggleMapDrawer, toggleMainChat, - shouldShowSignIn + showLoading } = this.props; return ( @@ -230,10 +230,13 @@ export default class extends React.Component { { this.renderMapLink(isOnMap, toggleMapDrawer) } { this.renderChat(toggleMainChat) } { this.renderLinks() } - { this.renderSignIn(username, points, picture, shouldShowSignIn) } + { this.renderSignIn(username, points, picture, showLoading) } ); } } + +FCCNav.displayName = 'Nav'; +FCCNav.propTypes = propTypes; diff --git a/common/app/components/SK-Wave.jsx b/common/app/components/SK-Wave.jsx new file mode 100644 index 0000000000..8f8080a0fb --- /dev/null +++ b/common/app/components/SK-Wave.jsx @@ -0,0 +1,19 @@ +import React, { PureComponent } from 'react'; + +const propTypes = { +}; +export default class SKWave extends PureComponent { + render() { + return ( +
+
+
+
+
+
+
+ ); + } +} +SKWave.displayName = 'SKWave'; +SKWave.propTypes = propTypes; diff --git a/common/app/redux/reducer.js b/common/app/redux/reducer.js index 4ccf742824..609990231e 100644 --- a/common/app/redux/reducer.js +++ b/common/app/redux/reducer.js @@ -3,7 +3,7 @@ import types from './types'; const initialState = { title: 'Learn To Code | Free Code Camp', - shouldShowSignIn: false, + isSignInAttempted: false, user: '', lang: '', csrfToken: '', @@ -24,7 +24,7 @@ export default handleActions( [types.updateThisUser]: (state, { payload: user }) => ({ ...state, user, - shouldShowSignIn: true + isSignInAttempted: true }), [types.updateAppLang]: (state, { payload = 'en' }) =>({ ...state, @@ -36,7 +36,7 @@ export default handleActions( }), [types.showSignIn]: state => ({ ...state, - shouldShowSignIn: true + isSignInAttempted: true }), [types.challengeSaved]: (state, { payload: { points = 0 } }) => ({ diff --git a/common/app/routes/settings/components/Settings.jsx b/common/app/routes/settings/components/Settings.jsx index 8247f06c38..b886f9f8e0 100644 --- a/common/app/routes/settings/components/Settings.jsx +++ b/common/app/routes/settings/components/Settings.jsx @@ -1,5 +1,7 @@ import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + import { Button, Row, Col } from 'react-bootstrap'; import FA from 'react-fontawesome'; @@ -7,11 +9,14 @@ import LockedSettings from './Locked-Settings.jsx'; import SocialSettings from './Social-Settings.jsx'; import EmailSettings from './Email-Setting.jsx'; import LanguageSettings from './Language-Settings.jsx'; +import SKWave from '../../../components/SK-Wave.jsx'; -import { toggleUserFlag } from '../redux/actions'; -import { toggleNightMode, updateTitle } from '../../../redux/actions'; -const actions = { +import { toggleUserFlag } from '../redux/actions.js'; +import { userSelector } from '../../../redux/selectors.js'; +import { toggleNightMode, updateTitle } from '../../../redux/actions.js'; + +const mapDispatchToProps = { updateTitle, toggleNightMode, toggleIsLocked: () => toggleUserFlag('isLocked'), @@ -20,22 +25,26 @@ const actions = { toggleMonthlyEmail: () => toggleUserFlag('sendMonthlyEmail') }; -const mapStateToProps = state => { - const { - app: { user: username }, - entities: { user: userMap } - } = state; - const { - email, - isLocked, - isGithubCool, - isTwitter, - isLinkedIn, - sendMonthlyEmail, - sendNotificationEmail, - sendQuincyEmail - } = userMap[username] || {}; - return { +const mapStateToProps = createSelector( + userSelector, + state => state.app.isSignInAttempted, + ( + { + user: { + username, + email, + isLocked, + isGithubCool, + isTwitter, + isLinkedIn, + sendMonthlyEmail, + sendNotificationEmail, + sendQuincyEmail + } + }, + isSignInAttempted + ) => ({ + showLoading: isSignInAttempted, username, email, isLocked, @@ -45,7 +54,29 @@ const mapStateToProps = state => { sendMonthlyEmail, sendNotificationEmail, sendQuincyEmail - }; + }) +); +const propTypes = { + children: PropTypes.element, + username: PropTypes.string, + isLocked: PropTypes.bool, + isGithubCool: PropTypes.bool, + isTwitter: PropTypes.bool, + isLinkedIn: PropTypes.bool, + showLoading: PropTypes.bool, + email: PropTypes.string, + sendMonthlyEmail: PropTypes.bool, + sendNotificationEmail: PropTypes.bool, + sendQuincyEmail: PropTypes.bool, + updateTitle: PropTypes.func.isRequired, + toggleNightMode: PropTypes.func.isRequired, + toggleIsLocked: PropTypes.func.isRequired, + toggleQuincyEmail: PropTypes.func.isRequired, + toggleMonthlyEmail: PropTypes.func.isRequired, + toggleNotificationEmail: PropTypes.func.isRequired, + lang: PropTypes.string, + initialLang: PropTypes.string, + updateMyLang: PropTypes.func }; export class Settings extends React.Component { @@ -53,28 +84,6 @@ export class Settings extends React.Component { super(...props); this.updateMyLang = this.updateMyLang.bind(this); } - static displayName = 'Settings'; - static propTypes = { - children: PropTypes.element, - username: PropTypes.string, - isLocked: PropTypes.bool, - isGithubCool: PropTypes.bool, - isTwitter: PropTypes.bool, - isLinkedIn: PropTypes.bool, - email: PropTypes.string, - sendMonthlyEmail: PropTypes.bool, - sendNotificationEmail: PropTypes.bool, - sendQuincyEmail: PropTypes.bool, - updateTitle: PropTypes.func.isRequired, - toggleNightMode: PropTypes.func.isRequired, - toggleIsLocked: PropTypes.func.isRequired, - toggleQuincyEmail: PropTypes.func.isRequired, - toggleMonthlyEmail: PropTypes.func.isRequired, - toggleNotificationEmail: PropTypes.func.isRequired, - lang: PropTypes.string, - initialLang: PropTypes.string, - updateMyLang: PropTypes.func - }; updateMyLang(e) { e.preventDefault(); @@ -94,6 +103,7 @@ export class Settings extends React.Component { isGithubCool, isTwitter, isLinkedIn, + showLoading, email, sendMonthlyEmail, sendNotificationEmail, @@ -104,6 +114,9 @@ export class Settings extends React.Component { toggleMonthlyEmail, toggleNotificationEmail } = this.props; + if (!username && !showLoading) { + return ; + } if (children) { return ( @@ -274,4 +287,10 @@ export class Settings extends React.Component { } } -export default connect(mapStateToProps, actions)(Settings); +Settings.displayName = 'Settings'; +Settings.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Settings); diff --git a/common/app/routes/settings/index.js b/common/app/routes/settings/index.js index f1606eb44b..839ba9a0d3 100644 --- a/common/app/routes/settings/index.js +++ b/common/app/routes/settings/index.js @@ -2,16 +2,9 @@ import Settings from './components/Settings.jsx'; import updateEmailRoute from './routes/update-email'; export default function settingsRoute(deps) { - const { getState } = deps; return { path: 'settings', component: Settings, - onEnter(nextState, replace) { - const { app: { user } } = getState(); - if (!user) { - replace('/map'); - } - }, childRoutes: [ updateEmailRoute(deps) ]