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 "map.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 { 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);

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) {
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) }
</Nav>
</Navbar.Collapse>
</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 = {
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 } }) => ({

View File

@ -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 <SKWave />;
}
if (children) {
return (
<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';
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)
]