feat(app): render spinner on /settings

This commit is contained in:
Berkeley Martinez
2016-10-28 22:14:39 -07:00
parent e1512bfa52
commit 2400ea04c5
8 changed files with 177 additions and 105 deletions

View File

@ -1151,3 +1151,4 @@ and (max-width : 400px) {
@import "toastr.less"; @import "toastr.less";
@import "map.less"; @import "map.less";
@import "drawers.less"; @import "drawers.less";
@import "sk-wave.less";

36
client/less/sk-wave.less Normal file
View File

@ -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);
}
}

View File

@ -21,7 +21,7 @@ import Nav from './components/Nav';
import Toasts from './toasts/Toasts.jsx'; import Toasts from './toasts/Toasts.jsx';
import { userSelector } from './redux/selectors'; import { userSelector } from './redux/selectors';
const bindableActions = { const mapDispatchToProps = {
initWindowHeight, initWindowHeight,
updateNavHeight, updateNavHeight,
fetchUser, fetchUser,
@ -35,14 +35,14 @@ const bindableActions = {
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
userSelector, userSelector,
state => state.app.shouldShowSignIn, state => state.app.isSignInAttempted,
state => state.app.toast, state => state.app.toast,
state => state.app.isMapDrawerOpen, state => state.app.isMapDrawerOpen,
state => state.app.isMapAlreadyLoaded, state => state.app.isMapAlreadyLoaded,
state => state.challengesApp.toast, state => state.challengesApp.toast,
( (
{ user: { username, points, picture } }, { user: { username, points, picture } },
shouldShowSignIn, isSignInAttempted,
toast, toast,
isMapDrawerOpen, isMapDrawerOpen,
isMapAlreadyLoaded, isMapAlreadyLoaded,
@ -51,41 +51,38 @@ const mapStateToProps = createSelector(
points, points,
picture, picture,
toast, toast,
shouldShowSignIn, showLoading: !isSignInAttempted,
isMapDrawerOpen, isMapDrawerOpen,
isMapAlreadyLoaded, isMapAlreadyLoaded,
isSignedIn: !!username 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 plain class for testing
export class FreeCodeCamp extends React.Component { 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) { componentWillReceiveProps(nextProps) {
if (this.props.params.lang !== nextProps.params.lang) { if (this.props.params.lang !== nextProps.params.lang) {
this.props.updateAppLang(nextProps.params.lang); this.props.updateAppLang(nextProps.params.lang);
@ -125,7 +122,7 @@ export class FreeCodeCamp extends React.Component {
isMapAlreadyLoaded, isMapAlreadyLoaded,
toggleMapDrawer, toggleMapDrawer,
toggleMainChat, toggleMainChat,
shouldShowSignIn, showLoading,
params: { lang }, params: { lang },
trackEvent, trackEvent,
loadCurrentChallenge loadCurrentChallenge
@ -138,7 +135,7 @@ export class FreeCodeCamp extends React.Component {
updateNavHeight, updateNavHeight,
toggleMapDrawer, toggleMapDrawer,
toggleMainChat, toggleMainChat,
shouldShowSignIn, showLoading,
trackEvent, trackEvent,
loadCurrentChallenge loadCurrentChallenge
}; };
@ -160,7 +157,11 @@ export class FreeCodeCamp extends React.Component {
} }
} }
FreeCodeCamp.displayName = 'FreeCodeCamp';
FreeCodeCamp.contextTypes = contextTypes;
FreeCodeCamp.propTypes = propTypes;
export default connect( export default connect(
mapStateToProps, mapStateToProps,
bindableActions mapDispatchToProps
)(FreeCodeCamp); )(FreeCodeCamp);

View File

@ -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) { constructor(...props) {
super(...props); super(...props);
this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this); this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this);
@ -38,20 +52,6 @@ export default class extends React.Component {
this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content); 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() { componentDidMount() {
const navBar = ReactDOM.findDOMNode(this); const navBar = ReactDOM.findDOMNode(this);
@ -165,8 +165,8 @@ export default class extends React.Component {
}); });
} }
renderSignIn(username, points, picture, shouldShowSignIn) { renderSignIn(username, points, picture, showLoading) {
if (!shouldShowSignIn) { if (showLoading) {
return null; return null;
} }
if (username) { if (username) {
@ -198,7 +198,7 @@ export default class extends React.Component {
isOnMap, isOnMap,
toggleMapDrawer, toggleMapDrawer,
toggleMainChat, toggleMainChat,
shouldShowSignIn showLoading
} = this.props; } = this.props;
return ( return (
@ -230,10 +230,13 @@ export default class extends React.Component {
{ this.renderMapLink(isOnMap, toggleMapDrawer) } { this.renderMapLink(isOnMap, toggleMapDrawer) }
{ this.renderChat(toggleMainChat) } { this.renderChat(toggleMainChat) }
{ this.renderLinks() } { this.renderLinks() }
{ this.renderSignIn(username, points, picture, shouldShowSignIn) } { this.renderSignIn(username, points, picture, showLoading) }
</Nav> </Nav>
</Navbar.Collapse> </Navbar.Collapse>
</Navbar> </Navbar>
); );
} }
} }
FCCNav.displayName = 'Nav';
FCCNav.propTypes = propTypes;

View File

@ -0,0 +1,19 @@
import React, { PureComponent } from 'react';
const propTypes = {
};
export default class SKWave extends PureComponent {
render() {
return (
<div className='sk-wave'>
<div />
<div />
<div />
<div />
<div />
</div>
);
}
}
SKWave.displayName = 'SKWave';
SKWave.propTypes = propTypes;

View File

@ -3,7 +3,7 @@ import types from './types';
const initialState = { const initialState = {
title: 'Learn To Code | Free Code Camp', title: 'Learn To Code | Free Code Camp',
shouldShowSignIn: false, isSignInAttempted: false,
user: '', user: '',
lang: '', lang: '',
csrfToken: '', csrfToken: '',
@ -24,7 +24,7 @@ export default handleActions(
[types.updateThisUser]: (state, { payload: user }) => ({ [types.updateThisUser]: (state, { payload: user }) => ({
...state, ...state,
user, user,
shouldShowSignIn: true isSignInAttempted: true
}), }),
[types.updateAppLang]: (state, { payload = 'en' }) =>({ [types.updateAppLang]: (state, { payload = 'en' }) =>({
...state, ...state,
@ -36,7 +36,7 @@ export default handleActions(
}), }),
[types.showSignIn]: state => ({ [types.showSignIn]: state => ({
...state, ...state,
shouldShowSignIn: true isSignInAttempted: true
}), }),
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({ [types.challengeSaved]: (state, { payload: { points = 0 } }) => ({

View File

@ -1,5 +1,7 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Row, Col } from 'react-bootstrap'; import { Button, Row, Col } from 'react-bootstrap';
import FA from 'react-fontawesome'; import FA from 'react-fontawesome';
@ -7,11 +9,14 @@ import LockedSettings from './Locked-Settings.jsx';
import SocialSettings from './Social-Settings.jsx'; import SocialSettings from './Social-Settings.jsx';
import EmailSettings from './Email-Setting.jsx'; import EmailSettings from './Email-Setting.jsx';
import LanguageSettings from './Language-Settings.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, updateTitle,
toggleNightMode, toggleNightMode,
toggleIsLocked: () => toggleUserFlag('isLocked'), toggleIsLocked: () => toggleUserFlag('isLocked'),
@ -20,22 +25,26 @@ const actions = {
toggleMonthlyEmail: () => toggleUserFlag('sendMonthlyEmail') toggleMonthlyEmail: () => toggleUserFlag('sendMonthlyEmail')
}; };
const mapStateToProps = state => { const mapStateToProps = createSelector(
const { userSelector,
app: { user: username }, state => state.app.isSignInAttempted,
entities: { user: userMap } (
} = state; {
const { user: {
email, username,
isLocked, email,
isGithubCool, isLocked,
isTwitter, isGithubCool,
isLinkedIn, isTwitter,
sendMonthlyEmail, isLinkedIn,
sendNotificationEmail, sendMonthlyEmail,
sendQuincyEmail sendNotificationEmail,
} = userMap[username] || {}; sendQuincyEmail
return { }
},
isSignInAttempted
) => ({
showLoading: isSignInAttempted,
username, username,
email, email,
isLocked, isLocked,
@ -45,7 +54,29 @@ const mapStateToProps = state => {
sendMonthlyEmail, sendMonthlyEmail,
sendNotificationEmail, sendNotificationEmail,
sendQuincyEmail 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 { export class Settings extends React.Component {
@ -53,28 +84,6 @@ export class Settings extends React.Component {
super(...props); super(...props);
this.updateMyLang = this.updateMyLang.bind(this); 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) { updateMyLang(e) {
e.preventDefault(); e.preventDefault();
@ -94,6 +103,7 @@ export class Settings extends React.Component {
isGithubCool, isGithubCool,
isTwitter, isTwitter,
isLinkedIn, isLinkedIn,
showLoading,
email, email,
sendMonthlyEmail, sendMonthlyEmail,
sendNotificationEmail, sendNotificationEmail,
@ -104,6 +114,9 @@ export class Settings extends React.Component {
toggleMonthlyEmail, toggleMonthlyEmail,
toggleNotificationEmail toggleNotificationEmail
} = this.props; } = this.props;
if (!username && !showLoading) {
return <SKWave />;
}
if (children) { if (children) {
return ( return (
<Row> <Row>
@ -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);

View File

@ -2,16 +2,9 @@ import Settings from './components/Settings.jsx';
import updateEmailRoute from './routes/update-email'; import updateEmailRoute from './routes/update-email';
export default function settingsRoute(deps) { export default function settingsRoute(deps) {
const { getState } = deps;
return { return {
path: 'settings', path: 'settings',
component: Settings, component: Settings,
onEnter(nextState, replace) {
const { app: { user } } = getState();
if (!user) {
replace('/map');
}
},
childRoutes: [ childRoutes: [
updateEmailRoute(deps) updateEmailRoute(deps)
] ]