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
@ -20,6 +20,7 @@ import SignUp from './Sign-Up.jsx';
|
|||||||
import BinButton from './Bin-Button.jsx';
|
import BinButton from './Bin-Button.jsx';
|
||||||
import {
|
import {
|
||||||
clickOnLogo,
|
clickOnLogo,
|
||||||
|
clickOnMap,
|
||||||
openDropdown,
|
openDropdown,
|
||||||
closeDropdown,
|
closeDropdown,
|
||||||
createNavLinkActionCreator,
|
createNavLinkActionCreator,
|
||||||
@ -73,6 +74,10 @@ function mapDispatchToProps(dispatch) {
|
|||||||
return mdtp;
|
return mdtp;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
clickOnMap: e => {
|
||||||
|
e.preventDefault();
|
||||||
|
return clickOnMap();
|
||||||
|
},
|
||||||
clickOnLogo: e => {
|
clickOnLogo: e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return clickOnLogo();
|
return clickOnLogo();
|
||||||
@ -180,12 +185,14 @@ export class FCCNav extends React.Component {
|
|||||||
const {
|
const {
|
||||||
panes,
|
panes,
|
||||||
clickOnLogo,
|
clickOnLogo,
|
||||||
|
clickOnMap,
|
||||||
username,
|
username,
|
||||||
points,
|
points,
|
||||||
picture,
|
picture,
|
||||||
showLoading
|
showLoading
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const shouldShowMapButton = panes.length === 0;
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
className='nav-height'
|
className='nav-height'
|
||||||
@ -221,6 +228,14 @@ export class FCCNav extends React.Component {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
{ shouldShowMapButton ?
|
||||||
|
<BinButton
|
||||||
|
content='Map'
|
||||||
|
handleClick={ clickOnMap }
|
||||||
|
key='Map'
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
navLinks.map(
|
navLinks.map(
|
||||||
this.renderLink.bind(this, true)
|
this.renderLink.bind(this, true)
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import { entitiesSelector } from '../../entities';
|
import { entitiesSelector } from '../../entities';
|
||||||
|
|
||||||
export default function loadCurrentChallengeEpic(actions, { getState }) {
|
export default function loadCurrentChallengeEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.clickOnLogo)
|
return actions::ofType(types.clickOnLogo, types.clickOnMap)
|
||||||
.debounce(500)
|
.debounce(500)
|
||||||
.map(() => {
|
.map(() => {
|
||||||
let finalChallenge;
|
let finalChallenge;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import flowRight from 'lodash/flowRight';
|
import flowRight from 'lodash/flowRight';
|
||||||
import createNameIdMap from '../../utils/create-name-id-map.js';
|
import { createNameIdMap } from '../../utils/map.js';
|
||||||
|
|
||||||
|
|
||||||
export function filterComingSoonBetaChallenge(
|
export function filterComingSoonBetaChallenge(
|
||||||
isDev = false,
|
isDev = false,
|
||||||
@ -29,5 +28,8 @@ export function filterComingSoonBetaFromEntities(
|
|||||||
|
|
||||||
export const shapeChallenges = flowRight(
|
export const shapeChallenges = flowRight(
|
||||||
filterComingSoonBetaFromEntities,
|
filterComingSoonBetaFromEntities,
|
||||||
createNameIdMap
|
entities => ({
|
||||||
|
...entities,
|
||||||
|
...createNameIdMap(entities)
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,7 @@ export default function challengesRoutes() {
|
|||||||
onEnter(nextState, replace) {
|
onEnter(nextState, replace) {
|
||||||
// redirect /challenges to /map
|
// redirect /challenges to /map
|
||||||
if (nextState.location.pathname === '/challenges') {
|
if (nextState.location.pathname === '/challenges') {
|
||||||
replace('/map');
|
replace('/challenges/current-challenge');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import ShowMap from '../../Map';
|
|
||||||
|
|
||||||
export default function mapRoute() {
|
export default function mapRoute() {
|
||||||
return [{
|
return [{
|
||||||
path: 'map',
|
path: 'map',
|
||||||
component: ShowMap
|
onEnter: (_, replace) => replace('/challenges/current-challenge')
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
12
common/models/challenge.js
Normal file
12
common/models/challenge.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
}
|
@ -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;
|
|
||||||
}, {})
|
|
||||||
};
|
|
||||||
}
|
|
@ -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]
|
|
||||||
];
|
|
||||||
}
|
|
74
common/utils/map.js
Normal file
74
common/utils/map.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,14 +1,11 @@
|
|||||||
import Fetchr from 'fetchr';
|
import Fetchr from 'fetchr';
|
||||||
import getHikesService from '../services/hikes';
|
|
||||||
import getUserServices from '../services/user';
|
import getUserServices from '../services/user';
|
||||||
import getMapServices from '../services/map';
|
import getMapServices from '../services/map';
|
||||||
|
|
||||||
export default function bootServices(app) {
|
export default function bootServices(app) {
|
||||||
const hikesService = getHikesService(app);
|
|
||||||
const userServices = getUserServices(app);
|
const userServices = getUserServices(app);
|
||||||
const mapServices = getMapServices(app);
|
const mapServices = getMapServices(app);
|
||||||
|
|
||||||
Fetchr.registerFetcher(hikesService);
|
|
||||||
Fetchr.registerFetcher(userServices);
|
Fetchr.registerFetcher(userServices);
|
||||||
Fetchr.registerFetcher(mapServices);
|
Fetchr.registerFetcher(mapServices);
|
||||||
app.use('/services', Fetchr.middleware());
|
app.use('/services', Fetchr.middleware());
|
||||||
|
@ -4,12 +4,7 @@ import accepts from 'accepts';
|
|||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
|
|
||||||
import { ifNoUserSend } from '../utils/middleware';
|
import { ifNoUserSend } from '../utils/middleware';
|
||||||
import { cachedMap } from '../utils/map';
|
import { getChallengeById, 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:boot:challenges');
|
const log = debug('fcc:boot:challenges');
|
||||||
|
|
||||||
@ -73,8 +68,7 @@ export default function(app) {
|
|||||||
const send200toNonUser = ifNoUserSend(true);
|
const send200toNonUser = ifNoUserSend(true);
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const Block = app.models.Block;
|
const map = cachedMap(app.models);
|
||||||
const map$ = cachedMap(Block);
|
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/modern-challenge-completed',
|
'/modern-challenge-completed',
|
||||||
@ -344,43 +338,23 @@ export default function(app) {
|
|||||||
|
|
||||||
function redirectToCurrentChallenge(req, res, next) {
|
function redirectToCurrentChallenge(req, res, next) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
return map$
|
const challengeId = user && user.currentChallengeId;
|
||||||
.map(({ entities, result }) => ({
|
return getChallengeById(map, challengeId)
|
||||||
result,
|
.map(challenge => {
|
||||||
entities: createNameIdMap(entities)
|
const { block, dashedName } = challenge;
|
||||||
}))
|
if (!dashedName || !block) {
|
||||||
.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
|
// this should normally not be hit if database is properly seeded
|
||||||
console.error(new Error(dedent`
|
throw new Error(dedent`
|
||||||
Attemped to find '${dashedName}'
|
Attemped to find '${dashedName}'
|
||||||
from '${user && user.currentChallengeId || 'no challenge id found'}'
|
from '${ challengeId || 'no challenge id found'}'
|
||||||
but came up empty.
|
but came up empty.
|
||||||
db may not be properly seeded.
|
db may not be properly seeded.
|
||||||
`));
|
`);
|
||||||
if (dashedName) {
|
|
||||||
// attempt to find according to dashedName
|
|
||||||
return `/challenges/${dashedName}`;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
return `/challenges/${block}/${dashedName}`;
|
||||||
return `/challenges/${block}/${finalDashedName}`;
|
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
redirect => res.redirect(redirect || '/map'),
|
redirect => res.redirect(redirect || '/'),
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,7 @@ import {
|
|||||||
calcLongestStreak
|
calcLongestStreak
|
||||||
} from '../utils/user-stats';
|
} from '../utils/user-stats';
|
||||||
import supportedLanguages from '../../common/utils/supported-languages';
|
import supportedLanguages from '../../common/utils/supported-languages';
|
||||||
import createNameIdMap from '../../common/utils/create-name-id-map';
|
import { getChallengeInfo, cachedMap } from '../utils/map';
|
||||||
import { cachedMap } from '../utils/map';
|
|
||||||
|
|
||||||
const debug = debugFactory('fcc:boot:user');
|
const debug = debugFactory('fcc:boot:user');
|
||||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||||
@ -97,7 +96,7 @@ function getChallengeGroup(challenge) {
|
|||||||
// challenges: Array
|
// challenges: Array
|
||||||
// }]
|
// }]
|
||||||
function buildDisplayChallenges(
|
function buildDisplayChallenges(
|
||||||
{ challenge: challengeMap = {}, challengeIdToName },
|
{ challengeMap, challengeIdToName },
|
||||||
userChallengeMap = {},
|
userChallengeMap = {},
|
||||||
timezone
|
timezone
|
||||||
) {
|
) {
|
||||||
@ -139,10 +138,8 @@ function buildDisplayChallenges(
|
|||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const User = app.models.User;
|
const { User, Email } = app.models;
|
||||||
const Block = app.models.Block;
|
const map$ = cachedMap(app.models);
|
||||||
const { Email } = app.models;
|
|
||||||
const map$ = cachedMap(Block);
|
|
||||||
function findUserByUsername$(username, fields) {
|
function findUserByUsername$(username, fields) {
|
||||||
return observeQuery(
|
return observeQuery(
|
||||||
User,
|
User,
|
||||||
@ -436,9 +433,9 @@ module.exports = function(app) {
|
|||||||
userPortfolio.bio = emoji.emojify(userPortfolio.bio);
|
userPortfolio.bio = emoji.emojify(userPortfolio.bio);
|
||||||
}
|
}
|
||||||
|
|
||||||
return map$.map(({ entities }) => createNameIdMap(entities))
|
return getChallengeInfo(map$)
|
||||||
.flatMap(entities => buildDisplayChallenges(
|
.flatMap(challengeInfo => buildDisplayChallenges(
|
||||||
entities,
|
challengeInfo,
|
||||||
userPortfolio.challengeMap,
|
userPortfolio.challengeMap,
|
||||||
timezone
|
timezone
|
||||||
))
|
))
|
||||||
|
@ -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()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,105 +1,15 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { unDasherize } from '../utils';
|
import {
|
||||||
import { mapChallengeToLang, cachedMap, getMapForLang } from '../utils/map';
|
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');
|
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) {
|
export default function mapService(app) {
|
||||||
const Block = app.models.Block;
|
const challengeMap = cachedMap(app.models);
|
||||||
const challengeMap = cachedMap(Block);
|
|
||||||
return {
|
return {
|
||||||
name: 'map',
|
name: 'map',
|
||||||
read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
|
read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
|
||||||
@ -109,7 +19,10 @@ export default function mapService(app) {
|
|||||||
getChallenge(dashedName, block, challengeMap, lang),
|
getChallenge(dashedName, block, challengeMap, lang),
|
||||||
challengeMap.map(getMapForLang(lang))
|
challengeMap.map(getMapForLang(lang))
|
||||||
)
|
)
|
||||||
.subscribe(results => cb(null, results), cb);
|
.subscribe(
|
||||||
|
results => cb(null, results),
|
||||||
|
err => { log(err); cb(err); }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Observable } from 'rx';
|
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 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 isDev = process.env.NODE_ENV !== 'production';
|
||||||
const block = new Schema('block', { idAttribute: 'dashedName' });
|
const isBeta = !!process.env.BETA;
|
||||||
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
|
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
|
||||||
|
const addNameIdMap = _.once(_addNameIdToMap);
|
||||||
block.define({
|
const getFirstChallenge = _.once(_getFirstChallenge);
|
||||||
challenges: arrayOf(challenge)
|
|
||||||
});
|
|
||||||
|
|
||||||
superBlock.define({
|
|
||||||
blocks: arrayOf(block)
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapSchema = valuesOf(superBlock);
|
|
||||||
let mapObservableCache;
|
|
||||||
/*
|
/*
|
||||||
* interface ChallengeMap {
|
* interface ChallengeMap {
|
||||||
* result: {
|
* result: {
|
||||||
@ -26,82 +21,99 @@ let mapObservableCache;
|
|||||||
* },
|
* },
|
||||||
* entities: {
|
* entities: {
|
||||||
* superBlock: {
|
* superBlock: {
|
||||||
* [ ...superBlockDashedName: String ]: SuperBlock
|
* [ ...superBlockDashedName ]: SuperBlock
|
||||||
* },
|
* },
|
||||||
* block: {
|
* block: {
|
||||||
* [ ...blockDashedName: String ]: Block,
|
* [ ...blockDashedNameg ]: Block,
|
||||||
* challenge: {
|
* challenge: {
|
||||||
* [ ...challengeDashedName: String ]: Challenge
|
* [ ...challengeDashedNameg ]: Challenge
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export function cachedMap(Block) {
|
export function _cachedMap({ Block, Challenge }) {
|
||||||
if (mapObservableCache) {
|
const challenges = Challenge.find$({
|
||||||
return mapObservableCache;
|
order: [ 'order ASC', 'suborder ASC' ]
|
||||||
}
|
});
|
||||||
const query = {
|
const challengeMap = challenges
|
||||||
include: 'challenges',
|
.map(
|
||||||
order: ['superOrder ASC', 'order ASC']
|
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 ]
|
||||||
};
|
};
|
||||||
const map$ = Block.find$(query)
|
}
|
||||||
.flatMap(blocks => Observable.from(blocks.map(block => block.toJSON())))
|
return blocksMap;
|
||||||
.reduce((map, block) => {
|
}, blocksMap);
|
||||||
if (map[block.superBlock]) {
|
});
|
||||||
map[block.superBlock].blocks.push(block);
|
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 {
|
} else {
|
||||||
map[block.superBlock] = {
|
map[block.superBlock] = {
|
||||||
title: _.startCase(block.superBlock),
|
title: _.startCase(block.superBlock),
|
||||||
order: block.superOrder,
|
order: block.superOrder,
|
||||||
name: nameify(_.startCase(block.superBlock)),
|
name: nameify(_.startCase(block.superBlock)),
|
||||||
dashedName: block.superBlock,
|
dashedName: block.superBlock,
|
||||||
blocks: [block],
|
blocks: [block.dashedName],
|
||||||
message: block.superBlockMessage
|
message: block.superBlockMessage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, {})
|
}, {}));
|
||||||
.map(map => normalize(map, mapSchema))
|
const superBlocks = superBlockMap.map(superBlockMap => {
|
||||||
.map(map => {
|
return Object.keys(superBlockMap)
|
||||||
// make sure challenges are in the right order
|
.map(key => superBlockMap[key])
|
||||||
map.entities.block = Object.keys(map.entities.block)
|
.map(({ dashedName }) => dashedName);
|
||||||
// turn map into array
|
});
|
||||||
.map(key => map.entities.block[key])
|
return Observable.combineLatest(
|
||||||
// perform re-order
|
superBlockMap,
|
||||||
.map(block => {
|
blockMap,
|
||||||
block.challenges = block.challenges.reduce((accu, dashedName) => {
|
challengeMap,
|
||||||
const index = map.entities.challenge[dashedName].suborder;
|
superBlocks,
|
||||||
accu[index - 1] = dashedName;
|
(superBlock, block, challenge, superBlocks) => ({
|
||||||
return accu;
|
entities: {
|
||||||
}, []);
|
superBlock,
|
||||||
return block;
|
block,
|
||||||
})
|
challenge
|
||||||
// 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: {
|
result: {
|
||||||
superBlocks
|
superBlocks
|
||||||
}
|
}
|
||||||
};
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
.do(checkMapData)
|
||||||
.shareReplay();
|
.shareReplay();
|
||||||
mapObservableCache = map$;
|
|
||||||
return map$;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const cachedMap = _.once(_cachedMap);
|
||||||
|
|
||||||
export function mapChallengeToLang(
|
export function mapChallengeToLang(
|
||||||
{ translations = {}, ...challenge },
|
{ translations = {}, ...challenge },
|
||||||
lang
|
lang
|
||||||
@ -137,3 +149,126 @@ export function getMapForLang(lang) {
|
|||||||
return { result, entities };
|
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