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:
committed by
Quincy Larson
parent
f92294bbda
commit
c547c26bba
@ -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 }));
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user