feat(app): render spinner on /settings
This commit is contained in:
@ -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
36
client/less/sk-wave.less
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
19
common/app/components/SK-Wave.jsx
Normal file
19
common/app/components/SK-Wave.jsx
Normal 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;
|
@ -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 } }) => ({
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
]
|
||||
|
Reference in New Issue
Block a user