Feature(nav): clicking on logo takes user to current challenge
This commit is contained in:
@ -11,7 +11,8 @@ import {
|
|||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
updateAppLang,
|
updateAppLang,
|
||||||
trackEvent
|
trackEvent,
|
||||||
|
loadCurrentChallenge
|
||||||
} from './redux/actions';
|
} from './redux/actions';
|
||||||
|
|
||||||
import { submitChallenge } from './routes/challenges/redux/actions';
|
import { submitChallenge } from './routes/challenges/redux/actions';
|
||||||
@ -28,7 +29,8 @@ const bindableActions = {
|
|||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
updateAppLang,
|
updateAppLang,
|
||||||
trackEvent
|
trackEvent,
|
||||||
|
loadCurrentChallenge
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
@ -80,7 +82,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
shouldShowSignIn: PropTypes.bool,
|
shouldShowSignIn: PropTypes.bool,
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
updateAppLang: PropTypes.func.isRequired,
|
updateAppLang: PropTypes.func.isRequired,
|
||||||
trackEvent: PropTypes.func.isRequired
|
trackEvent: PropTypes.func.isRequired,
|
||||||
|
loadCurrentChallenge: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
@ -124,7 +127,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
shouldShowSignIn,
|
shouldShowSignIn,
|
||||||
params: { lang },
|
params: { lang },
|
||||||
trackEvent
|
trackEvent,
|
||||||
|
loadCurrentChallenge
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const navProps = {
|
const navProps = {
|
||||||
isOnMap: router.isActive(`/${lang}/map`),
|
isOnMap: router.isActive(`/${lang}/map`),
|
||||||
@ -135,7 +139,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
shouldShowSignIn,
|
shouldShowSignIn,
|
||||||
trackEvent
|
trackEvent,
|
||||||
|
loadCurrentChallenge
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,16 +15,6 @@ import AvatarNavItem from './Avatar-Nav-Item.jsx';
|
|||||||
|
|
||||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
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 = (
|
const toggleButtonChild = (
|
||||||
<Col xs={ 12 }>
|
<Col xs={ 12 }>
|
||||||
<span className='hamburger-text'>Menu</span>
|
<span className='hamburger-text'>Menu</span>
|
||||||
@ -43,6 +33,7 @@ export default class extends React.Component {
|
|||||||
constructor(...props) {
|
constructor(...props) {
|
||||||
super(...props);
|
super(...props);
|
||||||
this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this);
|
this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this);
|
||||||
|
this.handleLogoClick = this.handleLogoClick.bind(this);
|
||||||
navLinks.forEach(({ content }) => {
|
navLinks.forEach(({ content }) => {
|
||||||
this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content);
|
this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content);
|
||||||
});
|
});
|
||||||
@ -58,7 +49,8 @@ export default class extends React.Component {
|
|||||||
toggleMapDrawer: PropTypes.func,
|
toggleMapDrawer: PropTypes.func,
|
||||||
toggleMainChat: PropTypes.func,
|
toggleMainChat: PropTypes.func,
|
||||||
shouldShowSignIn: PropTypes.bool,
|
shouldShowSignIn: PropTypes.bool,
|
||||||
trackEvent: PropTypes.func.isRequired
|
trackEvent: PropTypes.func.isRequired,
|
||||||
|
loadCurrentChallenge: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -83,6 +75,11 @@ export default class extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLogoClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.loadCurrentChallenge();
|
||||||
|
}
|
||||||
|
|
||||||
renderMapLink(isOnMap, toggleMapDrawer) {
|
renderMapLink(isOnMap, toggleMapDrawer) {
|
||||||
if (isOnMap) {
|
if (isOnMap) {
|
||||||
return (
|
return (
|
||||||
@ -218,7 +215,18 @@ export default class extends React.Component {
|
|||||||
className='nav-height'
|
className='nav-height'
|
||||||
fixedTop={ true }
|
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.Toggle children={ toggleButtonChild } />
|
||||||
<Navbar.Collapse>
|
<Navbar.Collapse>
|
||||||
<Nav
|
<Nav
|
||||||
|
@ -60,6 +60,7 @@ export const addUser = createAction(
|
|||||||
);
|
);
|
||||||
export const updateThisUser = createAction(types.updateThisUser);
|
export const updateThisUser = createAction(types.updateThisUser);
|
||||||
export const showSignIn = createAction(types.showSignIn);
|
export const showSignIn = createAction(types.showSignIn);
|
||||||
|
export const loadCurrentChallenge = createAction(types.loadCurrentChallenge);
|
||||||
|
|
||||||
// updateUserPoints(username: String, points: Number) => Action
|
// updateUserPoints(username: String, points: Number) => Action
|
||||||
export const updateUserPoints = createAction(
|
export const updateUserPoints = createAction(
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import fetchUserSaga from './fetch-user-saga';
|
import fetchUserSaga from './fetch-user-saga';
|
||||||
|
import loadCurrentChallengeSaga from './load-current-challenge-saga';
|
||||||
|
|
||||||
export { default as reducer } from './reducer';
|
export { default as reducer } from './reducer';
|
||||||
export * as actions from './actions';
|
export * as actions from './actions';
|
||||||
export { default as types } from './types';
|
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] || {}
|
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',
|
'updateUserLang',
|
||||||
'updateCompletedChallenges',
|
'updateCompletedChallenges',
|
||||||
'showSignIn',
|
'showSignIn',
|
||||||
|
'loadCurrentChallenge',
|
||||||
|
|
||||||
'handleError',
|
'handleError',
|
||||||
'toggleNightMode',
|
'toggleNightMode',
|
||||||
|
@ -120,8 +120,14 @@
|
|||||||
"description": "Campers profile does not show challenges/certificates to the public",
|
"description": "Campers profile does not show challenges/certificates to the public",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"currentChallengeId": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "the challenge last visited by the user"
|
||||||
|
},
|
||||||
"currentChallenge": {
|
"currentChallenge": {
|
||||||
"type": {}
|
"type": {},
|
||||||
|
"description": "deprecated"
|
||||||
},
|
},
|
||||||
"isUniqMigrated": {
|
"isUniqMigrated": {
|
||||||
"type": "boolean",
|
"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