diff --git a/common/app/Nav/Nav.jsx b/common/app/Nav/Nav.jsx
index 1198275416..354c1a62e0 100644
--- a/common/app/Nav/Nav.jsx
+++ b/common/app/Nav/Nav.jsx
@@ -20,6 +20,7 @@ import SignUp from './Sign-Up.jsx';
import BinButton from './Bin-Button.jsx';
import {
clickOnLogo,
+ clickOnMap,
openDropdown,
closeDropdown,
createNavLinkActionCreator,
@@ -73,6 +74,10 @@ function mapDispatchToProps(dispatch) {
return mdtp;
},
{
+ clickOnMap: e => {
+ e.preventDefault();
+ return clickOnMap();
+ },
clickOnLogo: e => {
e.preventDefault();
return clickOnLogo();
@@ -180,12 +185,14 @@ export class FCCNav extends React.Component {
const {
panes,
clickOnLogo,
+ clickOnMap,
username,
points,
picture,
showLoading
} = this.props;
+ const shouldShowMapButton = panes.length === 0;
return (
))
}
+ { shouldShowMapButton ?
+ :
+ null
+ }
{
navLinks.map(
this.renderLink.bind(this, true)
diff --git a/common/app/Nav/redux/load-current-challenge-epic.js b/common/app/Nav/redux/load-current-challenge-epic.js
index 9e609ecbfa..52efe9352f 100644
--- a/common/app/Nav/redux/load-current-challenge-epic.js
+++ b/common/app/Nav/redux/load-current-challenge-epic.js
@@ -11,7 +11,7 @@ import {
import { entitiesSelector } from '../../entities';
export default function loadCurrentChallengeEpic(actions, { getState }) {
- return actions::ofType(types.clickOnLogo)
+ return actions::ofType(types.clickOnLogo, types.clickOnMap)
.debounce(500)
.map(() => {
let finalChallenge;
diff --git a/common/app/redux/utils.js b/common/app/redux/utils.js
index 9be79e8804..ee6eb694ca 100644
--- a/common/app/redux/utils.js
+++ b/common/app/redux/utils.js
@@ -1,6 +1,5 @@
import flowRight from 'lodash/flowRight';
-import createNameIdMap from '../../utils/create-name-id-map.js';
-
+import { createNameIdMap } from '../../utils/map.js';
export function filterComingSoonBetaChallenge(
isDev = false,
@@ -29,5 +28,8 @@ export function filterComingSoonBetaFromEntities(
export const shapeChallenges = flowRight(
filterComingSoonBetaFromEntities,
- createNameIdMap
+ entities => ({
+ ...entities,
+ ...createNameIdMap(entities)
+ })
);
diff --git a/common/app/routes/challenges/index.js b/common/app/routes/challenges/index.js
index 77768a2197..fff3d571d1 100644
--- a/common/app/routes/challenges/index.js
+++ b/common/app/routes/challenges/index.js
@@ -20,7 +20,7 @@ export default function challengesRoutes() {
onEnter(nextState, replace) {
// redirect /challenges to /map
if (nextState.location.pathname === '/challenges') {
- replace('/map');
+ replace('/challenges/current-challenge');
}
}
}, {
diff --git a/common/app/routes/map/index.js b/common/app/routes/map/index.js
index 40326d564b..91e74e5838 100644
--- a/common/app/routes/map/index.js
+++ b/common/app/routes/map/index.js
@@ -1,8 +1,6 @@
-import ShowMap from '../../Map';
-
export default function mapRoute() {
return [{
path: 'map',
- component: ShowMap
+ onEnter: (_, replace) => replace('/challenges/current-challenge')
}];
}
diff --git a/common/models/challenge.js b/common/models/challenge.js
new file mode 100644
index 0000000000..74bbedce18
--- /dev/null
+++ b/common/models/challenge.js
@@ -0,0 +1,12 @@
+import { Observable } from 'rx';
+
+export default function(Challenge) {
+ Challenge.on('dataSourceAttached', () => {
+ Challenge.findOne$ =
+ Observable.fromNodeCallback(Challenge.findOne, Challenge);
+ Challenge.findById$ =
+ Observable.fromNodeCallback(Challenge.findById, Challenge);
+ Challenge.find$ =
+ Observable.fromNodeCallback(Challenge.find, Challenge);
+ });
+}
diff --git a/common/utils/create-name-id-map.js b/common/utils/create-name-id-map.js
deleted file mode 100644
index 5d2ea1a106..0000000000
--- a/common/utils/create-name-id-map.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// createNameIdMap(entities: Object) => Object
-export default function createNameIdMap(entities) {
- const { challenge } = entities;
- return {
- ...entities,
- challengeIdToName: Object.keys(challenge)
- .reduce((map, challengeName) => {
- map[challenge[challengeName].id] = challenge[challengeName].dashedName;
- return map;
- }, {})
- };
-}
diff --git a/common/utils/get-first-challenge.js b/common/utils/get-first-challenge.js
deleted file mode 100644
index 2c33d9c4ea..0000000000
--- a/common/utils/get-first-challenge.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import emptyProtector from '../app/utils/empty-protector';
-
-export function checkMapData(
- {
- entities: {
- challenge,
- block,
- superBlock,
- challengeIdToName
- },
- result: { superBlocks }
- }
-) {
- if (
- !challenge ||
- !block ||
- !superBlock ||
- !challengeIdToName ||
- !superBlocks ||
- !superBlocks.length
- ) {
- throw new Error(
- 'entities not found, db may not be properly seeded'
- );
- }
-}
-// 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]
- ];
-}
diff --git a/common/utils/map.js b/common/utils/map.js
new file mode 100644
index 0000000000..60544ae491
--- /dev/null
+++ b/common/utils/map.js
@@ -0,0 +1,74 @@
+import emptyProtector from '../app/utils/empty-protector';
+
+export function checkMapData(
+ {
+ entities: {
+ challenge,
+ block,
+ superBlock
+ },
+ result: { superBlocks }
+ }
+) {
+ if (
+ !challenge ||
+ !block ||
+ !superBlock ||
+ !superBlocks ||
+ !superBlocks.length
+ ) {
+ throw new Error(
+ 'entities not found, db may not be properly seeded'
+ );
+ }
+}
+// getFirstChallenge(
+// map: {
+// entities: { challenge: Object, block: Object, superBlock: Object },
+// result: [...superBlockDashedName: String]
+// }
+// ) => Challenge|Void
+export function getFirstChallenge({
+ entities: { superBlock, block, challenge },
+ result: { superBlocks }
+}) {
+ return challenge[
+ emptyProtector(block[
+ emptyProtector(superBlock[
+ superBlocks[0]
+ ]).blocks[0]
+ ]).challenges[0]
+ ];
+}
+
+// let challengeDashedName: String;
+// createNameIdMap({
+// challenge: {
+// [...challengeDashedName ]: Challenge
+// }) => {
+// challengeIdToName: {
+// [ ...challengeId ]: challengeDashedName
+// }
+// };
+export function createNameIdMap({ challenge }) {
+ return {
+ challengeIdToName: Object.keys(challenge)
+ .reduce((map, challengeName) => {
+ map[challenge[challengeName].id] =
+ challenge[challengeName].dashedName;
+ return map;
+ }, {})
+ };
+}
+// addNameIdMap(
+// map: { entities; Object, ...rest }
+// ) => { ...rest, entities: Object };
+export function addNameIdMap({ entities, ...rest }) {
+ return {
+ ...rest,
+ entities: {
+ ...entities,
+ ...createNameIdMap(entities)
+ }
+ };
+}
diff --git a/server/boot/a-services.js b/server/boot/a-services.js
index 6aeb0440fc..f9bdeb60c1 100644
--- a/server/boot/a-services.js
+++ b/server/boot/a-services.js
@@ -1,14 +1,11 @@
import Fetchr from 'fetchr';
-import getHikesService from '../services/hikes';
import getUserServices from '../services/user';
import getMapServices from '../services/map';
export default function bootServices(app) {
- const hikesService = getHikesService(app);
const userServices = getUserServices(app);
const mapServices = getMapServices(app);
- Fetchr.registerFetcher(hikesService);
Fetchr.registerFetcher(userServices);
Fetchr.registerFetcher(mapServices);
app.use('/services', Fetchr.middleware());
diff --git a/server/boot/challenge.js b/server/boot/challenge.js
index b24cb7b117..fb7da84af7 100644
--- a/server/boot/challenge.js
+++ b/server/boot/challenge.js
@@ -4,12 +4,7 @@ import accepts from 'accepts';
import dedent from 'dedent';
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';
+import { getChallengeById, cachedMap } from '../utils/map';
const log = debug('fcc:boot:challenges');
@@ -73,8 +68,7 @@ export default function(app) {
const send200toNonUser = ifNoUserSend(true);
const api = app.loopback.Router();
const router = app.loopback.Router();
- const Block = app.models.Block;
- const map$ = cachedMap(Block);
+ const map = cachedMap(app.models);
api.post(
'/modern-challenge-completed',
@@ -344,43 +338,23 @@ export default function(app) {
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) {
+ const challengeId = user && user.currentChallengeId;
+ return getChallengeById(map, challengeId)
+ .map(challenge => {
+ const { block, dashedName } = challenge;
+ if (!dashedName || !block) {
// this should normally not be hit if database is properly seeded
- console.error(new Error(dedent`
+ throw new Error(dedent`
Attemped to find '${dashedName}'
- from '${user && user.currentChallengeId || 'no challenge id found'}'
+ from '${ challengeId || '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}`;
+ return `/challenges/${block}/${dashedName}`;
})
.subscribe(
- redirect => res.redirect(redirect || '/map'),
+ redirect => res.redirect(redirect || '/'),
next
);
}
diff --git a/server/boot/user.js b/server/boot/user.js
index 3bc31ce6a6..d698718a3c 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -22,8 +22,7 @@ import {
calcLongestStreak
} from '../utils/user-stats';
import supportedLanguages from '../../common/utils/supported-languages';
-import createNameIdMap from '../../common/utils/create-name-id-map';
-import { cachedMap } from '../utils/map';
+import { getChallengeInfo, cachedMap } from '../utils/map';
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
@@ -97,7 +96,7 @@ function getChallengeGroup(challenge) {
// challenges: Array
// }]
function buildDisplayChallenges(
- { challenge: challengeMap = {}, challengeIdToName },
+ { challengeMap, challengeIdToName },
userChallengeMap = {},
timezone
) {
@@ -139,10 +138,8 @@ function buildDisplayChallenges(
module.exports = function(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
- const User = app.models.User;
- const Block = app.models.Block;
- const { Email } = app.models;
- const map$ = cachedMap(Block);
+ const { User, Email } = app.models;
+ const map$ = cachedMap(app.models);
function findUserByUsername$(username, fields) {
return observeQuery(
User,
@@ -436,9 +433,9 @@ module.exports = function(app) {
userPortfolio.bio = emoji.emojify(userPortfolio.bio);
}
- return map$.map(({ entities }) => createNameIdMap(entities))
- .flatMap(entities => buildDisplayChallenges(
- entities,
+ return getChallengeInfo(map$)
+ .flatMap(challengeInfo => buildDisplayChallenges(
+ challengeInfo,
userPortfolio.challengeMap,
timezone
))
diff --git a/server/services/hikes.js b/server/services/hikes.js
deleted file mode 100644
index ef5263a990..0000000000
--- a/server/services/hikes.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import debugFactory from 'debug';
-
-const debug = debugFactory('fcc:services:hikes');
-
-export default function hikesService(app) {
- const Challenge = app.models.Challenge;
-
- return {
- name: 'hikes',
- read: (req, resource, { dashedName } = {}, config, cb) => {
- const query = {
- where: {
- challengeType: '6',
- isComingSoon: false
- },
- order: ['order ASC', 'suborder ASC' ]
- };
-
- debug('dashedName', dashedName);
- if (dashedName) {
- query.where.dashedName = { like: dashedName, options: 'i' };
- }
- debug('query', query);
- Challenge.find(query, (err, hikes) => {
- if (err) {
- return cb(err);
- }
- return cb(null, hikes.map(hike => hike.toJSON()));
- });
- }
- };
-}
diff --git a/server/services/job.js b/server/services/job.js
deleted file mode 100644
index ad0bd35cea..0000000000
--- a/server/services/job.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const whereFilt = {
- where: {
- isFilled: false,
- isPaid: true,
- isApproved: true
- },
- order: 'postedOn DESC'
-};
-
-export default function getJobServices(app) {
- const { Job } = app.models;
-
- return {
- name: 'jobs',
- create(req, resource, { job } = {}, body, config, cb) {
- if (!job) {
- return cb(new Error('job creation should get a job object'));
- }
-
- Object.assign(job, {
- isPaid: false,
- isApproved: false
- });
-
- return Job.create(job, (err, savedJob) => {
- cb(err, savedJob.toJSON());
- });
- },
- read(req, resource, params, config, cb) {
- const id = params ? params.id : null;
- if (id) {
- return Job.findById(id)
- .then(job => cb(null, job.toJSON()))
- .catch(cb);
- }
- return Job.find(whereFilt)
- .then(jobs => cb(null, jobs.map(job => job.toJSON())))
- .catch(cb);
- }
- };
-}
diff --git a/server/services/map.js b/server/services/map.js
index 8b534d903a..7c8b71eee2 100644
--- a/server/services/map.js
+++ b/server/services/map.js
@@ -1,105 +1,15 @@
import { Observable } from 'rx';
import debug from 'debug';
-import { unDasherize } from '../utils';
-import { mapChallengeToLang, cachedMap, getMapForLang } from '../utils/map';
+import {
+ cachedMap,
+ getChallenge,
+ getMapForLang
+} from '../utils/map';
-const isDev = process.env.NODE_ENV !== 'production';
-const isBeta = !!process.env.BETA;
-const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const log = debug('fcc:services:map');
-// 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;
-}
-
-function getFirstChallenge(challengeMap$) {
- return challengeMap$
- .map(({ entities: { superBlock, block, challenge }, result }) => {
- return challenge[
- block[
- superBlock[
- result[0]
- ].blocks[0]
- ].challenges[0]
- ];
- });
-}
-
-// this is a hard search
-// falls back to soft search
-function getChallenge(
- challengeDashedName,
- blockDashedName,
- challengeMap$,
- lang
-) {
- return challengeMap$
- .flatMap(({ entities, result: { superBlocks } }) => {
- const block = entities.block[blockDashedName];
- const challenge = entities.challenge[challengeDashedName];
- return Observable.if(
- () => (
- !blockDashedName ||
- !block ||
- !challenge ||
- !loadComingSoonOrBetaChallenge(challenge)
- ),
- getChallengeByDashedName(challengeDashedName, challengeMap$),
- Observable.just(challenge)
- )
- .map(challenge => ({
- 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
- }
- }));
- });
-}
-
-function getChallengeByDashedName(dashedName, challengeMap$) {
- const challengeName = unDasherize(dashedName)
- .replace(challengesRegex, '');
- const testChallengeName = new RegExp(challengeName, 'i');
- log('looking for %s', testChallengeName);
-
- return challengeMap$
- .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 => {
- if (challengeOrNull) {
- return Observable.just(challengeOrNull);
- }
- return getFirstChallenge(challengeMap$);
- });
-}
-
export default function mapService(app) {
- const Block = app.models.Block;
- const challengeMap = cachedMap(Block);
+ const challengeMap = cachedMap(app.models);
return {
name: 'map',
read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
@@ -109,7 +19,10 @@ export default function mapService(app) {
getChallenge(dashedName, block, challengeMap, lang),
challengeMap.map(getMapForLang(lang))
)
- .subscribe(results => cb(null, results), cb);
+ .subscribe(
+ results => cb(null, results),
+ err => { log(err); cb(err); }
+ );
}
};
}
diff --git a/server/utils/map.js b/server/utils/map.js
index d5a4a3ff78..848197abe0 100644
--- a/server/utils/map.js
+++ b/server/utils/map.js
@@ -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 }));
+ });
+}
+