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 })); + }); +} +