Feature(nav): clicking on logo takes user to current challenge
This commit is contained in:
@ -11,7 +11,8 @@ import {
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
updateAppLang,
|
||||
trackEvent
|
||||
trackEvent,
|
||||
loadCurrentChallenge
|
||||
} from './redux/actions';
|
||||
|
||||
import { submitChallenge } from './routes/challenges/redux/actions';
|
||||
@ -28,7 +29,8 @@ const bindableActions = {
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
updateAppLang,
|
||||
trackEvent
|
||||
trackEvent,
|
||||
loadCurrentChallenge
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
@ -80,7 +82,8 @@ export class FreeCodeCamp extends React.Component {
|
||||
shouldShowSignIn: PropTypes.bool,
|
||||
params: PropTypes.object,
|
||||
updateAppLang: PropTypes.func.isRequired,
|
||||
trackEvent: PropTypes.func.isRequired
|
||||
trackEvent: PropTypes.func.isRequired,
|
||||
loadCurrentChallenge: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@ -124,7 +127,8 @@ export class FreeCodeCamp extends React.Component {
|
||||
toggleMainChat,
|
||||
shouldShowSignIn,
|
||||
params: { lang },
|
||||
trackEvent
|
||||
trackEvent,
|
||||
loadCurrentChallenge
|
||||
} = this.props;
|
||||
const navProps = {
|
||||
isOnMap: router.isActive(`/${lang}/map`),
|
||||
@ -135,7 +139,8 @@ export class FreeCodeCamp extends React.Component {
|
||||
toggleMapDrawer,
|
||||
toggleMainChat,
|
||||
shouldShowSignIn,
|
||||
trackEvent
|
||||
trackEvent,
|
||||
loadCurrentChallenge
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -15,16 +15,6 @@ import AvatarNavItem from './Avatar-Nav-Item.jsx';
|
||||
|
||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||
|
||||
const logoElement = (
|
||||
<a href='/'>
|
||||
<img
|
||||
alt='learn to code javascript at Free Code Camp logo'
|
||||
className='img-responsive nav-logo'
|
||||
src={ fCClogo }
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
const toggleButtonChild = (
|
||||
<Col xs={ 12 }>
|
||||
<span className='hamburger-text'>Menu</span>
|
||||
@ -43,6 +33,7 @@ export default class extends React.Component {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this);
|
||||
this.handleLogoClick = this.handleLogoClick.bind(this);
|
||||
navLinks.forEach(({ content }) => {
|
||||
this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content);
|
||||
});
|
||||
@ -58,7 +49,8 @@ export default class extends React.Component {
|
||||
toggleMapDrawer: PropTypes.func,
|
||||
toggleMainChat: PropTypes.func,
|
||||
shouldShowSignIn: PropTypes.bool,
|
||||
trackEvent: PropTypes.func.isRequired
|
||||
trackEvent: PropTypes.func.isRequired,
|
||||
loadCurrentChallenge: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -83,6 +75,11 @@ export default class extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleLogoClick(e) {
|
||||
e.preventDefault();
|
||||
this.props.loadCurrentChallenge();
|
||||
}
|
||||
|
||||
renderMapLink(isOnMap, toggleMapDrawer) {
|
||||
if (isOnMap) {
|
||||
return (
|
||||
@ -218,7 +215,18 @@ export default class extends React.Component {
|
||||
className='nav-height'
|
||||
fixedTop={ true }
|
||||
>
|
||||
<NavbarBrand>{ logoElement }</NavbarBrand>
|
||||
<NavbarBrand>
|
||||
<a
|
||||
href='/'
|
||||
onClick={ this.handleLogoClick }
|
||||
>
|
||||
<img
|
||||
alt='learn to code javascript at Free Code Camp logo'
|
||||
className='img-responsive nav-logo'
|
||||
src={ fCClogo }
|
||||
/>
|
||||
</a>
|
||||
</NavbarBrand>
|
||||
<Navbar.Toggle children={ toggleButtonChild } />
|
||||
<Navbar.Collapse>
|
||||
<Nav
|
||||
|
@ -60,6 +60,7 @@ export const addUser = createAction(
|
||||
);
|
||||
export const updateThisUser = createAction(types.updateThisUser);
|
||||
export const showSignIn = createAction(types.showSignIn);
|
||||
export const loadCurrentChallenge = createAction(types.loadCurrentChallenge);
|
||||
|
||||
// updateUserPoints(username: String, points: Number) => Action
|
||||
export const updateUserPoints = createAction(
|
||||
|
@ -1,6 +1,10 @@
|
||||
import fetchUserSaga from './fetch-user-saga';
|
||||
import loadCurrentChallengeSaga from './load-current-challenge-saga';
|
||||
|
||||
export { default as reducer } from './reducer';
|
||||
export * as actions from './actions';
|
||||
export { default as types } from './types';
|
||||
export const sagas = [ fetchUserSaga ];
|
||||
export const sagas = [
|
||||
fetchUserSaga,
|
||||
loadCurrentChallengeSaga
|
||||
];
|
||||
|
42
common/app/redux/load-current-challenge-saga.js
Normal file
42
common/app/redux/load-current-challenge-saga.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { Observable } from 'rx';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import types from './types';
|
||||
import {
|
||||
userSelector,
|
||||
firstChallengeSelector
|
||||
} from './selectors';
|
||||
import getActionsOfType from '../../utils/get-actions-of-type';
|
||||
import { updateCurrentChallenge } from '../routes/challenges/redux/actions';
|
||||
|
||||
export default function loadCurrentChallengeSaga(actions, getState) {
|
||||
return getActionsOfType(actions, types.loadCurrentChallenge)
|
||||
.flatMap(() => {
|
||||
let finalChallenge;
|
||||
const state = getState();
|
||||
const {
|
||||
entities: { challenge: challengeMap, challengeIdToName },
|
||||
challengesApp: { id: currentlyLoadedChallengeId }
|
||||
} = state;
|
||||
const firstChallenge = firstChallengeSelector(state);
|
||||
const { user: { currentChallengeId } } = userSelector(state);
|
||||
if (!currentChallengeId) {
|
||||
finalChallenge = firstChallenge;
|
||||
} else {
|
||||
finalChallenge = challengeMap[
|
||||
challengeIdToName[ currentChallengeId ]
|
||||
];
|
||||
}
|
||||
if (finalChallenge.id === currentlyLoadedChallengeId) {
|
||||
// don't reload if the challenge is already loaded.
|
||||
// This may change to toast to avoid user confusion
|
||||
return Observable.empty();
|
||||
}
|
||||
return Observable.of(
|
||||
updateCurrentChallenge(finalChallenge),
|
||||
push(
|
||||
`/challenges/${finalChallenge.block}/${finalChallenge.dashedName}`
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
@ -7,3 +7,27 @@ export const userSelector = createSelector(
|
||||
user: userMap[username] || {}
|
||||
})
|
||||
);
|
||||
|
||||
export const firstChallengeSelector = createSelector(
|
||||
state => state.entities.challenge,
|
||||
state => state.entities.block,
|
||||
state => state.entities.superBlock,
|
||||
state => state.challengesApp.superBlocks,
|
||||
(challengeMap, blockMap, superBlockMap, superBlocks) => {
|
||||
if (
|
||||
!challengeMap ||
|
||||
!blockMap ||
|
||||
!superBlockMap ||
|
||||
!superBlocks
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return challengeMap[
|
||||
blockMap[
|
||||
superBlockMap[
|
||||
superBlocks[0]
|
||||
].blocks[0]
|
||||
].challenges[0]
|
||||
];
|
||||
}
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ export default createTypes([
|
||||
'updateUserLang',
|
||||
'updateCompletedChallenges',
|
||||
'showSignIn',
|
||||
'loadCurrentChallenge',
|
||||
|
||||
'handleError',
|
||||
'toggleNightMode',
|
||||
|
@ -120,8 +120,14 @@
|
||||
"description": "Campers profile does not show challenges/certificates to the public",
|
||||
"default": false
|
||||
},
|
||||
"currentChallengeId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "the challenge last visited by the user"
|
||||
},
|
||||
"currentChallenge": {
|
||||
"type": {}
|
||||
"type": {},
|
||||
"description": "deprecated"
|
||||
},
|
||||
"isUniqMigrated": {
|
||||
"type": "boolean",
|
||||
|
10
common/utils/get-actions-of-type.js
Normal file
10
common/utils/get-actions-of-type.js
Normal file
@ -0,0 +1,10 @@
|
||||
export default function getActionsOfType(actions, ...types) {
|
||||
const length = types.length;
|
||||
return actions
|
||||
.filter(({ type }) => {
|
||||
if (length === 1) {
|
||||
return type === types[0];
|
||||
}
|
||||
return types.some(_type => _type === type);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user