Merge pull request #10064 from BerkeleyTrue/fix/actual-current-route
feature(challenges): load current challenge on first visit
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='/challenges/current-challenge'
|
||||||
|
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,19 @@ 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,
|
||||||
|
null,
|
||||||
|
() => createEventMeta({
|
||||||
|
category: 'Nav',
|
||||||
|
action: 'clicked',
|
||||||
|
label: 'fcc logo clicked'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export const updateMyCurrentChallenge = createAction(
|
||||||
|
types.updateMyCurrentChallenge,
|
||||||
|
(username, currentChallengeId) => ({ username, currentChallengeId })
|
||||||
|
);
|
||||||
|
|
||||||
// updateUserPoints(username: String, points: Number) => Action
|
// updateUserPoints(username: String, points: Number) => Action
|
||||||
export const updateUserPoints = createAction(
|
export const updateUserPoints = createAction(
|
||||||
|
@ -85,5 +85,18 @@ export default function entities(state = initialState, action) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === types.updateMyCurrentChallenge) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
currentChallengeId: action.payload.currentChallengeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
];
|
||||||
|
92
common/app/redux/load-current-challenge-saga.js
Normal file
92
common/app/redux/load-current-challenge-saga.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { push } from 'react-router-redux';
|
||||||
|
|
||||||
|
import types from './types';
|
||||||
|
import {
|
||||||
|
updateMyCurrentChallenge,
|
||||||
|
createErrorObservable
|
||||||
|
} from './actions';
|
||||||
|
import {
|
||||||
|
userSelector,
|
||||||
|
firstChallengeSelector
|
||||||
|
} from './selectors';
|
||||||
|
import { updateCurrentChallenge } from '../routes/challenges/redux/actions';
|
||||||
|
import getActionsOfType from '../../utils/get-actions-of-type';
|
||||||
|
import combineSagas from '../utils/combine-sagas';
|
||||||
|
import { postJSON$ } from '../../utils/ajax-stream';
|
||||||
|
|
||||||
|
const log = debug('fcc:app/redux/load-current-challenge-saga');
|
||||||
|
export function updateMyCurrentChallengeSaga(actions, getState) {
|
||||||
|
const updateChallenge$ = getActionsOfType(
|
||||||
|
actions,
|
||||||
|
updateCurrentChallenge.toString()
|
||||||
|
)
|
||||||
|
.map(({ payload: { id } }) => id)
|
||||||
|
.filter(() => {
|
||||||
|
const { app: { user: username } } = getState();
|
||||||
|
return !!username;
|
||||||
|
})
|
||||||
|
.distinctUntilChanged();
|
||||||
|
const optimistic = updateChallenge$.map(id => {
|
||||||
|
const { app: { user: username } } = getState();
|
||||||
|
return updateMyCurrentChallenge(username, id);
|
||||||
|
});
|
||||||
|
const ajaxUpdate = updateChallenge$
|
||||||
|
.debounce(250)
|
||||||
|
.flatMapLatest(currentChallengeId => {
|
||||||
|
const { app: { csrfToken: _csrf } } = getState();
|
||||||
|
return postJSON$(
|
||||||
|
'/update-my-current-challenge',
|
||||||
|
{ _csrf, currentChallengeId }
|
||||||
|
)
|
||||||
|
.map(({ message }) => log(message))
|
||||||
|
.catch(createErrorObservable);
|
||||||
|
});
|
||||||
|
return Observable.merge(optimistic, ajaxUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadCurrentChallengeSaga(actions, getState) {
|
||||||
|
return getActionsOfType(actions, types.loadCurrentChallenge)
|
||||||
|
.flatMap(() => {
|
||||||
|
let finalChallenge;
|
||||||
|
const state = getState();
|
||||||
|
const {
|
||||||
|
entities: { challenge: challengeMap, challengeIdToName },
|
||||||
|
challengesApp: { id: currentlyLoadedChallengeId },
|
||||||
|
locationBeforeTransition: { pathname } = {}
|
||||||
|
} = state;
|
||||||
|
const firstChallenge = firstChallengeSelector(state);
|
||||||
|
const { user: { currentChallengeId } } = userSelector(state);
|
||||||
|
const isOnAChallenge = (/^\/[^\/]{2,6}\/challenges/).test(pathname);
|
||||||
|
|
||||||
|
if (!currentChallengeId) {
|
||||||
|
finalChallenge = firstChallenge;
|
||||||
|
} else {
|
||||||
|
finalChallenge = challengeMap[
|
||||||
|
challengeIdToName[ currentChallengeId ]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
// data might not be there yet, ignore for now
|
||||||
|
!finalChallenge ||
|
||||||
|
// are we already on that challenge?
|
||||||
|
(isOnAChallenge && 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}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default combineSagas(
|
||||||
|
updateMyCurrentChallengeSaga,
|
||||||
|
loadCurrentChallengeSaga
|
||||||
|
);
|
@ -7,3 +7,32 @@ 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 {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return challengeMap[
|
||||||
|
blockMap[
|
||||||
|
superBlockMap[
|
||||||
|
superBlocks[0]
|
||||||
|
].blocks[0]
|
||||||
|
].challenges[0]
|
||||||
|
];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -14,6 +14,8 @@ export default createTypes([
|
|||||||
'updateUserLang',
|
'updateUserLang',
|
||||||
'updateCompletedChallenges',
|
'updateCompletedChallenges',
|
||||||
'showSignIn',
|
'showSignIn',
|
||||||
|
'loadCurrentChallenge',
|
||||||
|
'updateMyCurrentChallenge',
|
||||||
|
|
||||||
'handleError',
|
'handleError',
|
||||||
'toggleNightMode',
|
'toggleNightMode',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
import { bonfire, html, js } from '../../utils/challengeTypes';
|
import { bonfire, html, js } from '../../utils/challengeTypes';
|
||||||
|
import protect from '../../utils/empty-protector';
|
||||||
|
|
||||||
export function encodeScriptTags(value) {
|
export function encodeScriptTags(value) {
|
||||||
return value
|
return value
|
||||||
@ -78,19 +79,6 @@ export function loggerToStr(args) {
|
|||||||
.reduce((str, arg) => str + arg + '\n', '');
|
.reduce((str, arg) => str + arg + '\n', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFirstChallenge(
|
|
||||||
{ superBlock, block, challenge },
|
|
||||||
result
|
|
||||||
) {
|
|
||||||
return challenge[
|
|
||||||
block[
|
|
||||||
superBlock[
|
|
||||||
result[0]
|
|
||||||
].blocks[0]
|
|
||||||
].challenges[0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNextChallenge(
|
export function getNextChallenge(
|
||||||
current,
|
current,
|
||||||
entities,
|
entities,
|
||||||
@ -285,20 +273,6 @@ export function getMouse(e, [dx, dy]) {
|
|||||||
return [pageX - dx, pageY - dy];
|
return [pageX - dx, pageY - dy];
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyProtector = {
|
|
||||||
blocks: [],
|
|
||||||
challenges: []
|
|
||||||
};
|
|
||||||
// protect against malformed data
|
|
||||||
function protect(block) {
|
|
||||||
// if no block or block has no challenges or blocks
|
|
||||||
// use protector
|
|
||||||
if (!block || !(block.challenges || block.blocks)) {
|
|
||||||
return emptyProtector;
|
|
||||||
}
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
|
||||||
// interface Node {
|
// interface Node {
|
||||||
// isHidden: Boolean,
|
// isHidden: Boolean,
|
||||||
// children: Void|[ ...Node ],
|
// children: Void|[ ...Node ],
|
||||||
|
13
common/app/utils/empty-protector.js
Normal file
13
common/app/utils/empty-protector.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const emptyProtector = {
|
||||||
|
blocks: [],
|
||||||
|
challenges: []
|
||||||
|
};
|
||||||
|
// protect against malformed map data
|
||||||
|
// protect(block: { challenges: [], block: [] }|Void) => block|emptyProtector
|
||||||
|
export default function protect(block) {
|
||||||
|
// if no block or block has no challenges or blocks
|
||||||
|
if (!block || !(block.challenges || block.blocks)) {
|
||||||
|
return emptyProtector;
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
44
common/utils/get-first-challenge.js
Normal file
44
common/utils/get-first-challenge.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import emptyProtector from '../app/utils/empty-protector';
|
||||||
|
|
||||||
|
export function checkMapData(
|
||||||
|
{
|
||||||
|
entities: {
|
||||||
|
challenge,
|
||||||
|
block,
|
||||||
|
superBlock,
|
||||||
|
challengeIdToName
|
||||||
|
},
|
||||||
|
result
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!challenge ||
|
||||||
|
!block ||
|
||||||
|
!superBlock ||
|
||||||
|
!challengeIdToName ||
|
||||||
|
!result ||
|
||||||
|
!result.length
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'entities not found, db may not be properly seeded. Crashing hard'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// getFirstChallenge(
|
||||||
|
// map: {
|
||||||
|
// entities: { challenge: Object, block: Object, superBlock: Object },
|
||||||
|
// result: [...superBlockDashedName: String]
|
||||||
|
// }
|
||||||
|
// ) => Challenge|Void
|
||||||
|
export function getFirstChallenge({
|
||||||
|
entities: { superBlock, block, challenge },
|
||||||
|
result
|
||||||
|
}) {
|
||||||
|
return challenge[
|
||||||
|
emptyProtector(block[
|
||||||
|
emptyProtector(superBlock[
|
||||||
|
result[0]
|
||||||
|
]).blocks[0]
|
||||||
|
]).challenges[0]
|
||||||
|
];
|
||||||
|
}
|
@ -1,11 +1,17 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
// import { Observable, Scheduler } from 'rx';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import accepts from 'accepts';
|
import accepts from 'accepts';
|
||||||
|
import dedent from 'dedent';
|
||||||
|
|
||||||
import { ifNoUserSend } from '../utils/middleware';
|
import { ifNoUserSend } from '../utils/middleware';
|
||||||
|
import { cachedMap } from '../utils/map';
|
||||||
|
import createNameIdMap from '../../common/utils/create-name-id-map';
|
||||||
|
import {
|
||||||
|
checkMapData,
|
||||||
|
getFirstChallenge
|
||||||
|
} from '../../common/utils/get-first-challenge';
|
||||||
|
|
||||||
const log = debug('fcc:challenges');
|
const log = debug('fcc:boot:challenges');
|
||||||
|
|
||||||
function buildUserUpdate(
|
function buildUserUpdate(
|
||||||
user,
|
user,
|
||||||
@ -58,11 +64,14 @@ function buildUserUpdate(
|
|||||||
return { alreadyCompleted, updateData };
|
return { alreadyCompleted, updateData };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function(app) {
|
export default function(app) {
|
||||||
const router = app.loopback.Router();
|
|
||||||
const send200toNonUser = ifNoUserSend(true);
|
const send200toNonUser = ifNoUserSend(true);
|
||||||
|
const api = app.loopback.Router();
|
||||||
|
const router = app.loopback.Router();
|
||||||
|
const Block = app.models.Block;
|
||||||
|
const map$ = cachedMap(Block);
|
||||||
|
|
||||||
router.post(
|
api.post(
|
||||||
'/modern-challenge-completed',
|
'/modern-challenge-completed',
|
||||||
send200toNonUser,
|
send200toNonUser,
|
||||||
modernChallengeCompleted
|
modernChallengeCompleted
|
||||||
@ -70,13 +79,13 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
// deprecate endpoint
|
// deprecate endpoint
|
||||||
// remove once new endpoint is live
|
// remove once new endpoint is live
|
||||||
router.post(
|
api.post(
|
||||||
'/completed-challenge',
|
'/completed-challenge',
|
||||||
send200toNonUser,
|
send200toNonUser,
|
||||||
completedChallenge
|
completedChallenge
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
api.post(
|
||||||
'/challenge-completed',
|
'/challenge-completed',
|
||||||
send200toNonUser,
|
send200toNonUser,
|
||||||
completedChallenge
|
completedChallenge
|
||||||
@ -84,19 +93,25 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
// deprecate endpoint
|
// deprecate endpoint
|
||||||
// remove once new endpoint is live
|
// remove once new endpoint is live
|
||||||
router.post(
|
api.post(
|
||||||
'/completed-zipline-or-basejump',
|
'/completed-zipline-or-basejump',
|
||||||
send200toNonUser,
|
send200toNonUser,
|
||||||
projectCompleted
|
projectCompleted
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
api.post(
|
||||||
'/project-completed',
|
'/project-completed',
|
||||||
send200toNonUser,
|
send200toNonUser,
|
||||||
projectCompleted
|
projectCompleted
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(router);
|
router.get(
|
||||||
|
'/challenges/current-challenge',
|
||||||
|
redirectToCurrentChallenge
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(api);
|
||||||
|
app.use('/:lang', router);
|
||||||
|
|
||||||
function modernChallengeCompleted(req, res, next) {
|
function modernChallengeCompleted(req, res, next) {
|
||||||
const type = accepts(req).type('html', 'json', 'text');
|
const type = accepts(req).type('html', 'json', 'text');
|
||||||
@ -255,4 +270,47 @@ module.exports = function(app) {
|
|||||||
})
|
})
|
||||||
.subscribe(() => {}, next);
|
.subscribe(() => {}, next);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
function redirectToCurrentChallenge(req, res, next) {
|
||||||
|
const { user } = req;
|
||||||
|
return map$
|
||||||
|
.map(({ entities, result }) => ({
|
||||||
|
result,
|
||||||
|
entities: createNameIdMap(entities)
|
||||||
|
}))
|
||||||
|
.map(map => {
|
||||||
|
checkMapData(map);
|
||||||
|
const {
|
||||||
|
entities: { challenge: challengeMap, challengeIdToName }
|
||||||
|
} = map;
|
||||||
|
let finalChallenge;
|
||||||
|
const dashedName = challengeIdToName[user && user.currentChallengeId];
|
||||||
|
finalChallenge = challengeMap[dashedName];
|
||||||
|
// redirect to first challenge
|
||||||
|
if (!finalChallenge) {
|
||||||
|
finalChallenge = getFirstChallenge(map);
|
||||||
|
}
|
||||||
|
const { block, dashedName: finalDashedName } = finalChallenge || {};
|
||||||
|
if (!finalDashedName || !block) {
|
||||||
|
// this should normally not be hit if database is properly seeded
|
||||||
|
console.error(new Error(dedent`
|
||||||
|
Attemped to find '${dashedName}'
|
||||||
|
from '${user && user.currentChallengeId || 'no challenge id found'}'
|
||||||
|
but came up empty.
|
||||||
|
db may not be properly seeded.
|
||||||
|
`));
|
||||||
|
if (dashedName) {
|
||||||
|
// attempt to find according to dashedName
|
||||||
|
return `/challenges/${dashedName}`;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `/challenges/${block}/${finalDashedName}`;
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
redirect => res.redirect(redirect || '/map'),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ifNoUser401 } from '../utils/middleware';
|
import { ifNoUser401 } from '../utils/middleware';
|
||||||
|
import { isMongoId } from 'validator';
|
||||||
import supportedLanguages from '../../common/utils/supported-languages.js';
|
import supportedLanguages from '../../common/utils/supported-languages.js';
|
||||||
|
|
||||||
export default function settingsController(app) {
|
export default function settingsController(app) {
|
||||||
@ -49,6 +50,20 @@ export default function settingsController(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMyCurrentChallenge(req, res, next) {
|
||||||
|
const { user, body: { currentChallengeId } } = req;
|
||||||
|
if (!isMongoId('' + currentChallengeId)) {
|
||||||
|
return next(new Error(`${currentChallengeId} is not a valid ObjectId`));
|
||||||
|
}
|
||||||
|
return user.update$({ currentChallengeId }).subscribe(
|
||||||
|
() => res.json({
|
||||||
|
message:
|
||||||
|
`your current challenge has been updated to ${currentChallengeId}`
|
||||||
|
}),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/toggle-lockdown',
|
'/toggle-lockdown',
|
||||||
toggleUserFlag('isLocked')
|
toggleUserFlag('isLocked')
|
||||||
@ -78,5 +93,11 @@ export default function settingsController(app) {
|
|||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
updateMyLang
|
updateMyLang
|
||||||
);
|
);
|
||||||
|
|
||||||
|
api.post(
|
||||||
|
'/update-my-current-challenge',
|
||||||
|
ifNoUser401,
|
||||||
|
updateMyCurrentChallenge
|
||||||
|
);
|
||||||
app.use(api);
|
app.use(api);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ const publicUserProps = [
|
|||||||
'sendNotificationEmail',
|
'sendNotificationEmail',
|
||||||
'sendQuincyEmail',
|
'sendQuincyEmail',
|
||||||
|
|
||||||
'currentChallenge',
|
'currentChallengeId',
|
||||||
'challengeMap'
|
'challengeMap'
|
||||||
];
|
];
|
||||||
const log = debug('fcc:services:user');
|
const log = debug('fcc:services:user');
|
||||||
|
@ -3,7 +3,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height
|
|||||||
button.hamburger.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
|
button.hamburger.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
|
||||||
.col-xs-12
|
.col-xs-12
|
||||||
span.hamburger-text Menu
|
span.hamburger-text Menu
|
||||||
a.navbar-brand(href='/challenges/next-challenge')
|
a.navbar-brand(href='/challenges/current-challenge')
|
||||||
img.img-responsive.nav-logo(src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt='learn to code javascript at Free Code Camp logo')
|
img.img-responsive.nav-logo(src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt='learn to code javascript at Free Code Camp logo')
|
||||||
.collapse.navbar-collapse
|
.collapse.navbar-collapse
|
||||||
ul.nav.navbar-nav.navbar-right.hamburger-dropdown
|
ul.nav.navbar-nav.navbar-right.hamburger-dropdown
|
||||||
|
Reference in New Issue
Block a user