Feature(nav): clicking on logo takes user to current challenge

This commit is contained in:
Berkeley Martinez
2016-08-03 12:56:00 -07:00
parent 58ed93bacc
commit 42de7c57ef
9 changed files with 120 additions and 19 deletions

View File

@ -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 (

View File

@ -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

View File

@ -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(

View File

@ -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
];

View 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}`
)
);
});
}

View File

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

View File

@ -14,6 +14,7 @@ export default createTypes([
'updateUserLang',
'updateCompletedChallenges',
'showSignIn',
'loadCurrentChallenge',
'handleError',
'toggleNightMode',

View File

@ -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",

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