Fix: map should redirect to current challenge (#15723)

* fix(routes): /map redirects to current challenge

* fix(map): Normalize server map building

Localize all server code dealing with the map

* refactor(server): Remove unused services

* feat(Nav): Show Map button when no panes

This gives user the ability to quickly return to their challenge using a
known feature

* fix(server.map): Add caching to nameIdMap

Add caching to nameIdMap on the server

* fix(services.map): Fix map service

Move map building utils to map util. Fix bad import. Normalize challenge
lookup
This commit is contained in:
Berkeley Martinez
2017-08-03 20:45:36 -07:00
committed by Quincy Larson
parent f92294bbda
commit c547c26bba
16 changed files with 352 additions and 364 deletions

View File

@ -1,24 +1,19 @@
import _ from 'lodash';
import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
import { nameify } from '../utils';
import { unDasherize, nameify } from '../utils';
import supportedLanguages from '../../common/utils/supported-languages';
import {
addNameIdMap as _addNameIdToMap,
checkMapData,
getFirstChallenge as _getFirstChallenge
} from '../../common/utils/map.js';
const challenge = new Schema('challenge', { idAttribute: 'dashedName' });
const block = new Schema('block', { idAttribute: 'dashedName' });
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
block.define({
challenges: arrayOf(challenge)
});
superBlock.define({
blocks: arrayOf(block)
});
const mapSchema = valuesOf(superBlock);
let mapObservableCache;
const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const addNameIdMap = _.once(_addNameIdToMap);
const getFirstChallenge = _.once(_getFirstChallenge);
/*
* interface ChallengeMap {
* result: {
@ -26,82 +21,99 @@ let mapObservableCache;
* },
* entities: {
* superBlock: {
* [ ...superBlockDashedName: String ]: SuperBlock
* [ ...superBlockDashedName ]: SuperBlock
* },
* block: {
* [ ...blockDashedName: String ]: Block,
* [ ...blockDashedNameg ]: Block,
* challenge: {
* [ ...challengeDashedName: String ]: Challenge
* [ ...challengeDashedNameg ]: Challenge
* }
* }
* }
*/
export function cachedMap(Block) {
if (mapObservableCache) {
return mapObservableCache;
}
const query = {
include: 'challenges',
order: ['superOrder ASC', 'order ASC']
};
const map$ = Block.find$(query)
.flatMap(blocks => Observable.from(blocks.map(block => block.toJSON())))
.reduce((map, block) => {
if (map[block.superBlock]) {
map[block.superBlock].blocks.push(block);
} else {
map[block.superBlock] = {
title: _.startCase(block.superBlock),
order: block.superOrder,
name: nameify(_.startCase(block.superBlock)),
dashedName: block.superBlock,
blocks: [block],
message: block.superBlockMessage
};
}
return map;
}, {})
.map(map => normalize(map, mapSchema))
.map(map => {
// make sure challenges are in the right order
map.entities.block = Object.keys(map.entities.block)
// turn map into array
.map(key => map.entities.block[key])
// perform re-order
.map(block => {
block.challenges = block.challenges.reduce((accu, dashedName) => {
const index = map.entities.challenge[dashedName].suborder;
accu[index - 1] = dashedName;
return accu;
}, []);
return block;
})
// turn back into map
.reduce((blockMap, block) => {
blockMap[block.dashedName] = block;
return blockMap;
}, {});
return map;
})
.map(map => {
// re-order superBlocks result
const superBlocks = Object.keys(map.result).reduce((result, supName) => {
const index = map.entities.superBlock[supName].order;
result[index] = supName;
return result;
}, []);
return {
...map,
result: {
superBlocks
export function _cachedMap({ Block, Challenge }) {
const challenges = Challenge.find$({
order: [ 'order ASC', 'suborder ASC' ]
});
const challengeMap = challenges
.map(
challenges => challenges
.map(challenge => challenge.toJSON())
.reduce((hash, challenge) => {
hash[challenge.dashedName] = challenge;
return hash;
}, {})
);
const blocks = Block.find$({ order: [ 'superOrder ASC', 'order ASC' ] });
const blockMap = Observable.combineLatest(
blocks.map(
blocks => blocks
.map(block => block.toJSON())
.reduce((hash, block) => {
hash[block.dashedName] = block;
return hash;
}, {})
),
challenges
)
.map(([ blocksMap, challenges ]) => {
return challenges.reduce((blocksMap, challenge) => {
if (blocksMap[challenge.block].challenges) {
blocksMap[challenge.block].challenges.push(challenge.dashedName);
} else {
blocksMap[challenge.block] = {
...blocksMap[challenge.block],
challenges: [ challenge.dashedName ]
};
}
return blocksMap;
}, blocksMap);
});
const superBlockMap = blocks.map(blocks => blocks.reduce((map, block) => {
if (
map[block.superBlock] &&
map[block.superBlock].blocks
) {
map[block.superBlock].blocks.push(block.dashedName);
} else {
map[block.superBlock] = {
title: _.startCase(block.superBlock),
order: block.superOrder,
name: nameify(_.startCase(block.superBlock)),
dashedName: block.superBlock,
blocks: [block.dashedName],
message: block.superBlockMessage
};
}
return map;
}, {}));
const superBlocks = superBlockMap.map(superBlockMap => {
return Object.keys(superBlockMap)
.map(key => superBlockMap[key])
.map(({ dashedName }) => dashedName);
});
return Observable.combineLatest(
superBlockMap,
blockMap,
challengeMap,
superBlocks,
(superBlock, block, challenge, superBlocks) => ({
entities: {
superBlock,
block,
challenge
},
result: {
superBlocks
}
})
)
.do(checkMapData)
.shareReplay();
mapObservableCache = map$;
return map$;
}
export const cachedMap = _.once(_cachedMap);
export function mapChallengeToLang(
{ translations = {}, ...challenge },
lang
@ -137,3 +149,126 @@ export function getMapForLang(lang) {
return { result, entities };
};
}
// type ObjectId: String;
// getChallengeById(
// map: Observable[map],
// id: ObjectId
// ) => Observable[Challenge] | Void;
export function getChallengeById(map, id) {
return Observable.if(
() => !id,
map.map(getFirstChallenge),
map.map(addNameIdMap)
.map(map => {
const {
entities: { challenge: challengeMap, challengeIdToName }
} = map;
let finalChallenge;
const dashedName = challengeIdToName[id];
finalChallenge = challengeMap[dashedName];
if (!finalChallenge) {
finalChallenge = getFirstChallenge(map);
}
return finalChallenge;
})
);
}
export function getChallengeInfo(map) {
return map.map(addNameIdMap)
.map(({
entities: {
challenge: challengeMap,
challengeIdToName
}
}) => ({
challengeMap,
challengeIdToName
}));
}
// if challenge is not isComingSoon or isBeta => load
// if challenge is ComingSoon we are in beta||dev => load
// if challenge is beta and we are in beta||dev => load
// else hide
function loadComingSoonOrBetaChallenge({
isComingSoon,
isBeta: challengeIsBeta
}) {
return !(isComingSoon || challengeIsBeta) || isDev || isBeta;
}
// this is a hard search
// falls back to soft search
export function getChallenge(
challengeDashedName,
blockDashedName,
map,
lang
) {
return map
.flatMap(({ entities, result: { superBlocks } }) => {
const block = entities.block[blockDashedName];
const challenge = entities.challenge[challengeDashedName];
return Observable.if(
() => (
!blockDashedName ||
!block ||
!challenge ||
!loadComingSoonOrBetaChallenge(challenge)
),
getChallengeByDashedName(challengeDashedName, map),
Observable.just({ block, challenge })
)
.map(({ challenge, block }) => ({
redirect: challenge.block !== blockDashedName ?
`/challenges/${block.dashedName}/${challenge.dashedName}` :
false,
entities: {
challenge: {
[challenge.dashedName]: mapChallengeToLang(challenge, lang)
}
},
result: {
block: block.dashedName,
challenge: challenge.dashedName,
superBlocks
}
}));
});
}
export function getBlockForChallenge(map, challenge) {
return map.map(({ entities: { block } }) => block[challenge.block]);
}
export function getChallengeByDashedName(dashedName, map) {
const challengeName = unDasherize(dashedName)
.replace(challengesRegex, '');
const testChallengeName = new RegExp(challengeName, 'i');
return map
.map(({ entities }) => entities.challenge)
.flatMap(challengeMap => {
return Observable.from(Object.keys(challengeMap))
.map(key => challengeMap[key]);
})
.filter(challenge => {
return loadComingSoonOrBetaChallenge(challenge) &&
testChallengeName.test(challenge.name);
})
.last({ defaultValue: null })
.flatMap(challengeOrNull => {
return Observable.if(
() => !!challengeOrNull,
Observable.just(challengeOrNull),
map.map(getFirstChallenge)
);
})
.flatMap(challenge => {
return getBlockForChallenge(map, challenge)
.map(block => ({ challenge, block }));
});
}