Add initial dynamic challenge rendering

This commit is contained in:
Berkeley Martinez
2016-05-09 13:42:39 -07:00
parent 59dcabb588
commit a0f6ecfca2
20 changed files with 376 additions and 171 deletions

View File

@ -48,9 +48,18 @@ export const createErrorObserable = error => Observable.just({
// challenges // challenges
// these need to be used by more than one route so we put them here // these need to be used by more than one route so we put them here
export const fetchChallenge = createAction(types.fetchChallenge);
export const fetchChallengeCompleted = createAction(
types.fetchChallengeCompleted,
(_, challenge) => challenge,
entities => ({ entities })
);
export const fetchChallenges = createAction(types.fetchChallenges); export const fetchChallenges = createAction(types.fetchChallenges);
export const fetchChallengesCompleted = createAction( export const fetchChallengesCompleted = createAction(
types.fetchChallengesCompleted, types.fetchChallengesCompleted,
(_, superBlocks) => superBlocks, (_, superBlocks) => superBlocks,
entities => ({ entities }) entities => ({ entities })
); );
export const setChallenge = createAction(types.setChallenge);

View File

@ -1,10 +1,12 @@
const initialState = { const initialState = {
hike: {}, hike: {},
superBlock: {},
block: {},
challenge: {}, challenge: {},
job: {} job: {}
}; };
export default function dataReducer(state = initialState, action) { export default function entities(state = initialState, action) {
if (action.meta && action.meta.entities) { if (action.meta && action.meta.entities) {
return { return {
...state, ...state,

View File

@ -1,13 +1,31 @@
import { fetchChallenges } from './types'; import { Observable } from 'rx';
import { createErrorObserable, fetchChallengesCompleted } from './actions'; import { fetchChallenge, fetchChallenges } from './types';
import {
createErrorObserable,
fetchChallengeCompleted,
fetchChallengesCompleted,
setChallenge
} from './actions';
export default function fetchChallengesSaga(action$, getState, { services }) { export default function fetchChallengesSaga(action$, getState, { services }) {
return action$ return action$
.filter(action => action.type === fetchChallenges) .filter(
.flatMap(() => { ({ type }) => type === fetchChallenges || type === fetchChallenge
return services.readService$({ service: 'map' }) )
.map(({ entities, result } = {}) => { .flatMap(({ type, payload })=> {
return fetchChallengesCompleted(entities, result); const options = { service: 'map' };
if (type === fetchChallenge) {
options.params = { dashedName: payload };
}
return services.readService$(options)
.flatMap(({ entities, result } = {}) => {
if (type === fetchChallenge) {
return Observable.of(
fetchChallengeCompleted(entities, result),
setChallenge(entities.challenge[result])
);
}
return Observable.just(fetchChallengesCompleted(entities, result));
}) })
.catch(createErrorObserable); .catch(createErrorObserable);
}); });

View File

@ -19,9 +19,11 @@ export default createTypes([
'updateChallengesData', 'updateChallengesData',
'updateJobsData', 'updateJobsData',
'updateHikesData', 'updateHikesData',
// challenges // challenges
'fetchChallenge',
'fetchChallenges', 'fetchChallenges',
'fetchChallengesCompleted' 'fetchChallengeCompleted',
'fetchChallengesCompleted',
'setChallenge'
], 'app'); ], 'app');

View File

@ -11,7 +11,12 @@ export default class extends PureComponent {
static displayName = 'Challenge'; static displayName = 'Challenge';
static propTypes = { static propTypes = {
showPreview: PropTypes.bool showPreview: PropTypes.bool,
challenge: PropTypes.object
};
static defaultProps = {
challenge: {}
}; };
renderPreview(showPreview) { renderPreview(showPreview) {
@ -21,26 +26,28 @@ export default class extends PureComponent {
return ( return (
<Col <Col
lg={ 3 } lg={ 3 }
md={ 5 }> md={ 4 }>
<Preview /> <Preview />
</Col> </Col>
); );
} }
render() { render() {
const { showPreview } = this.props; const { content, challenge, mode, showPreview } = this.props;
return ( return (
<div> <div>
<Col <Col
lg={ 3 } lg={ 3 }
md={ showPreview ? 3 : 4 }> md={ showPreview ? 3 : 4 }>
<SidePanel /> <SidePanel { ...challenge }/>
</Col> </Col>
<Col <Col
lg={ showPreview ? 6 : 9 } lg={ showPreview ? 6 : 9 }
md={ showPreview ? 5 : 8 }> md={ showPreview ? 5 : 8 }>
<Editor /> <Editor
content={ content }
mode={ mode }
/>
</Col> </Col>
{ this.renderPreview(showPreview) } { this.renderPreview(showPreview) }
</div> </div>

View File

@ -1,19 +1,72 @@
import React from 'react'; import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { contain } from 'redux-epic';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
// import Challenge from './Challenge.jsx'; import Challenge from './Challenge.jsx';
import Step from './step/Step.jsx'; import Step from './step/Step.jsx';
import { fetchChallenge } from '../../../redux/actions';
import { STEP, HTML } from '../../../utils/challengeTypes';
const bindableActions = {
fetchChallenge
};
// <Challenge showPreview={ false } /> const challengeSelector = createSelector(
export default class extends PureComponent { state => state.challengesApp.challenge,
state => state.entities.challenge,
(challengeName, challengeMap) => {
const challenge = challengeMap[challengeName];
return {
challenge: challenge,
showPreview: !!challenge && challenge.challengeType === HTML,
isStep: !!challenge && challenge.challengeType === STEP,
mode: !!challenge && challenge.challengeType === HTML ?
'text/html' :
'javascript'
};
}
);
const mapStateToProps = createSelector(
challengeSelector,
state => state.challengesApp.content,
(challengeProps, content) => ({
...challengeProps,
content
})
);
const fetchOptions = {
fetchAction: 'fetchChallenge',
getActionArgs({ params: { dashedName } }) {
return [ dashedName ];
},
isPrimed({ challenge }) {
return challenge;
}
};
export class Challenges extends PureComponent {
static displayName = 'Challenges'; static displayName = 'Challenges';
static propTypes = {}; static propTypes = {
challenge: PropTypes.object,
showPreview: PropTypes.bool,
mode: PropTypes.string,
isStep: PropTypes.bool
};
render() { render() {
return ( if (this.props.isStep) {
<Step /> return <Step { ...this.props }/>;
); }
return <Challenge { ...this.props }/>;
} }
} }
export default compose(
connect(mapStateToProps, bindableActions),
contain(fetchOptions)
)(Challenges);

View File

@ -28,11 +28,18 @@ const options = {
export class Editor extends PureComponent { export class Editor extends PureComponent {
static displayName = 'Editor'; static displayName = 'Editor';
static propTypes = { static propTypes = {
height: PropTypes.number height: PropTypes.number,
content: PropTypes.string,
mode: PropTypes.string
};
static defaultProps = {
content: '// Happy Coding!',
mode: 'javascript'
}; };
render() { render() {
const { height } = this.props; const { content, height, mode } = this.props;
const style = {}; const style = {};
if (height) { if (height) {
style.height = height + 'px'; style.height = height + 'px';
@ -43,8 +50,8 @@ export class Editor extends PureComponent {
style={ style }> style={ style }>
<NoSSR> <NoSSR>
<Codemirror <Codemirror
options={ options } options={{ ...options, mode }}
value='foo test' /> value={ content } />
</NoSSR> </NoSSR>
</div> </div>
); );

View File

@ -15,20 +15,6 @@ const mapStateToProps = createSelector(
(windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 }) (windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 })
); );
/* eslint-disable max-len */
const description = [
'Comments are lines of code that JavaScript will intentionally ignore. Comments are a great way to leave notes to yourself and to other people who will later need to figure out what that code does.',
'There are two ways to write comments in JavaScript:',
'Using <code>//</code> will tell JavaScript to ignore the remainder of the text on the current line:',
'<blockquote>// This is an in-line comment.</blockquote>',
'You can make a multi-line comment beginning with <code>/*</code> and ending with <code>*/</code>:',
'<blockquote>/* This is a <br> multi-line comment */</blockquote>',
'<strong>Best Practice</strong><br>As you write code, you should regularly add comments to clarify the function of parts of your code. Good commenting can help communicate the intent of your code&mdash;both for others <em>and</em> for your future self.',
'<h4>Instructions</h4>',
'Try creating one of each type of comment.'
];
/* eslint-enable max-len */
export class SidePanel extends PureComponent { export class SidePanel extends PureComponent {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -42,7 +28,7 @@ export class SidePanel extends PureComponent {
}; };
static defaultProps = { static defaultProps = {
description description: [ 'Happy Coding!' ]
}; };
renderDescription(description, descriptionRegex) { renderDescription(description, descriptionRegex) {
@ -64,7 +50,7 @@ export class SidePanel extends PureComponent {
} }
render() { render() {
const { height } = this.props; const { title, description, height } = this.props;
const style = { const style = {
overflowX: 'hidden', overflowX: 'hidden',
overflowY: 'auto' overflowY: 'auto'
@ -78,7 +64,7 @@ export class SidePanel extends PureComponent {
style={ style }> style={ style }>
<div> <div>
<h4 className='text-center challenge-instructions-title'> <h4 className='text-center challenge-instructions-title'>
Build JavaScript Objects { title }
</h4> </h4>
<hr /> <hr />
<Row> <Row>

View File

@ -8,6 +8,7 @@ import ReactTransitionReplace from 'react-css-transition-replace';
import { Button, Col, Image, Row } from 'react-bootstrap'; import { Button, Col, Image, Row } from 'react-bootstrap';
const transitionTimeout = 1000;
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
state => state.challengesApp.currentStep, state => state.challengesApp.currentStep,
state => state.challengesApp.previousStep, state => state.challengesApp.previousStep,
@ -22,87 +23,6 @@ const dispatchActions = {
goToStep goToStep
}; };
const transitionTimeout = 1000;
/* eslint-disable max-len, quotes */
const challenge = {
title: "Learn how Free Code Camp Works",
description: [
[
"http://i.imgur.com/6ibIavQ.jpg",
"A picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits",
"Welcome to Free Code Camp. We're an open source community of busy people who learn to code and help nonprofits.",
"http://www.foo.com"
],
[
"http://i.imgur.com/Elb3dfj.jpg",
"A screenshot of some of our campers coding together in Toronto.",
"<bold>Learning to code is hard.</bold> To succeed, you'll need lots of practice and support. That's why we've created a rigorous curriculum and supportive community.",
""
],
[
"http://i.imgur.com/D7Y5luw.jpg",
"A graph of the rate of job growth against growth in computer science degree graduates. There are 1.4 million jobs and only 400 thousand people to fill them.",
"There are thousands of coding jobs currently going unfilled, and the demand for coders grows every year.",
""
],
[
"http://i.imgur.com/WD3STY6.jpg",
"Photos of three campers who've gotten jobs after learning to code at Free Code Camp.",
"Free Code Camp is a proven path to your first coding job. In fact, no one has actually completed our entire program, because campers get jobs before they're able to.",
""
],
[
"http://i.imgur.com/vLNso6h.jpg",
"An illustration showing that you will learn HTML5, CSS3, JavaScript, Databases, Git, Node.js, React and D3.",
"We have hundreds of optional coding challenges that will teach you fundamental web development technologies like HTML5, Node.js and databases.",
""
],
[
"http://i.imgur.com/UVB9hxp.jpg",
"An image of a camper at a cafe building projects on Free Code Camp.",
"We believe humans learn best by doing. So you'll spend most of your time actually building projects. We'll give you a list of specifications (agile user stories), and you'll figure out how to build apps that fulfill those specifications.",
""
],
[
"http://i.imgur.com/pbW7K5S.jpg",
"An image of showing our front end, back end, and data visualization certifications (400 hours each), our nonprofit projects (800 hours), and interview prep (80 hours) for a total of 2,080 hours of coding experience.",
"Our curriculum is divided into 4 certifications. These certifications are standardized, and instantly verifiable by your freelance clients and future employers. Like everything else at Free Code Camp, these certifications are free. We recommend doing them in order, but you are free to jump around. The first three certifications take 400 hours each, and the final certification takes 800 hours, and involves building real-life projects for nonprofits.",
""
],
[
"http://i.imgur.com/k8btNUB.jpg",
"A screenshot of our Front End Development Certificate",
"To earn our verified Front End Development Certification, you'll build 10 projects using HTML, CSS, jQuery, and JavaScript.",
""
],
[
"http://i.imgur.com/Et3iD74.jpg",
"A screenshot of our Data Visualization Certificate",
"To earn our Data Visualization Certification, you'll build 10 projects using React, Sass and D3.js.",
""
],
[
"http://i.imgur.com/8v3t84p.jpg",
"A screenshot of our Back End Development Certificate",
"To earn our Back End Development Certification, you'll build 10 projects using Node.js, Express, and MongoDB. You'll use Git and Heroku to deploy them to the cloud.",
""
],
[
"http://i.imgur.com/yXyxbDd.jpg",
"A screen shot of our nonprofit project directory.",
"After you complete all three of these certificates, you'll team up with another camper and use agile software development methodologies to build two real-life projects for nonprofits. You'll also add functionality to two legacy code nonprofit projects. By the time you finish, you'll have a portfolio of real apps that people use every day.",
""
],
[
"http://i.imgur.com/PDGQ9ZM.jpg",
"An image of campers building projects together in a cafe in Seoul.",
"If you complete all 2,080 hours worth of challenges and projects, you'll earn our Full Stack Development Certification. We'll offer you free coding interview practice. We even offer a job board where employers specifically hire campers who've earned Free Code Camp certifications.",
"http://foo.com"
]
]
};
/* eslint-disable max-len, quotes */
export class StepChallenge extends PureComponent { export class StepChallenge extends PureComponent {
static displayName = 'StepChallenge'; static displayName = 'StepChallenge';
static defaultProps = { static defaultProps = {
@ -111,6 +31,7 @@ export class StepChallenge extends PureComponent {
}; };
static propTypes = { static propTypes = {
challenge: PropTypes.object,
currentStep: PropTypes.number, currentStep: PropTypes.number,
previousStep: PropTypes.number, previousStep: PropTypes.number,
isGoingForward: PropTypes.bool, isGoingForward: PropTypes.bool,
@ -163,7 +84,7 @@ export class StepChallenge extends PureComponent {
const isLastStep = index + 1 >= numOfSteps; const isLastStep = index + 1 >= numOfSteps;
const btnClass = classnames({ const btnClass = classnames({
'col-sm-4 col-xs-12': true, 'col-sm-4 col-xs-12': true,
'disabled': hasAction && !isCompleted disabled: hasAction && !isCompleted
}); });
return ( return (
<Button <Button
@ -244,7 +165,7 @@ export class StepChallenge extends PureComponent {
} }
render() { render() {
const { currentStep, isGoingForward } = this.props; const { challenge, currentStep, isGoingForward } = this.props;
const numOfSteps = Array.isArray(challenge.description) ? const numOfSteps = Array.isArray(challenge.description) ?
challenge.description.length : challenge.description.length :
0; 0;

View File

@ -3,3 +3,4 @@ import { createAction } from 'redux-actions';
import types from './types'; import types from './types';
export const goToStep = createAction(types.goToStep); export const goToStep = createAction(types.goToStep);
export const setChallenge = createAction(types.setChallenge);

View File

@ -1,3 +1,3 @@
export actions from './actions'; export actions from './actions';
export reducer from './step-reducer'; export reducer from './reducer';
export types from './types'; export types from './types';

View File

@ -0,0 +1,41 @@
import { handleActions } from 'redux-actions';
import types from './types';
import { setChallenge, fetchChallengeCompleted } from '../../../redux/types';
const initialState = {
challenge: '',
currentStep: 0,
previousStep: -1,
content: null
};
function arrayToNewLineString(seedData = []) {
seedData = Array.isArray(seedData) ? seedData : [seedData];
return seedData.reduce((seed, line) => '' + seed + line + '\n', '\n');
}
function buildSeed({ challengeSeed = [] } = {}) {
return arrayToNewLineString(challengeSeed);
}
export default handleActions(
{
[types.resetStep]: () => initialState,
[types.goToStep]: (state, { payload: step = 0 }) => ({
...state,
currentStep: step,
previousStep: state.currentStep
}),
[fetchChallengeCompleted]: (state, { payload = '' }) => ({
...state,
challenge: payload
}),
[setChallenge]: (state, { payload: challenge }) => ({
...state,
challenge: challenge.dashedName,
content: buildSeed(challenge)
})
},
initialState
);

View File

@ -1,20 +0,0 @@
import { handleActions } from 'redux-actions';
import types from './types';
const initialState = {
currentStep: 0,
previousStep: -1
};
export default handleActions(
{
[types.resetStep]: () => initialState,
[types.goToStep]: (state, { payload: step = 0 }) => ({
...state,
currentStep: step,
previousStep: state.currentStep
})
},
initialState
);

View File

@ -2,5 +2,6 @@ import createTypes from '../../../utils/create-types';
export default createTypes([ export default createTypes([
// step // step
'goToStep' 'goToStep',
'setChallenge'
], 'challenges'); ], 'challenges');

View File

@ -11,10 +11,11 @@ export default class Block extends PureComponent {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
time: PropTypes.string, time: PropTypes.string,
challenges: PropTypes.array challenges: PropTypes.array,
setChallenge: PropTypes.func
}; };
renderChallenges(challenges) { renderChallenges(challenges, setChallenge) {
if (!Array.isArray(challenges) || !challenges.length) { if (!Array.isArray(challenges) || !challenges.length) {
return <div>No Challenges Found</div>; return <div>No Challenges Found</div>;
} }
@ -48,13 +49,16 @@ export default class Block extends PureComponent {
className={ challengeClassName } className={ challengeClassName }
key={ title }> key={ title }>
<Link to={ `/challenges/${dashedName}` }> <Link to={ `/challenges/${dashedName}` }>
{ title } <span
<span className='sr-only'>complete</span> onClick={ () => setChallenge(challenge) }>
{ { title }
isRequired ? <span className='sr-only'>complete</span>
<span className='text-primary'><strong>*</strong></span> : {
'' isRequired ?
} <span className='text-primary'><strong>*</strong></span> :
''
}
</span>
</Link> </Link>
</p> </p>
); );
@ -62,7 +66,7 @@ export default class Block extends PureComponent {
} }
render() { render() {
const { title, time, challenges } = this.props; const { title, time, challenges, setChallenge } = this.props;
return ( return (
<Panel <Panel
bsClass='map-accordion-panel-nested' bsClass='map-accordion-panel-nested'
@ -76,7 +80,7 @@ export default class Block extends PureComponent {
} }
id={ title } id={ title }
key={ title }> key={ title }>
{ this.renderChallenges(challenges) } { this.renderChallenges(challenges, setChallenge) }
</Panel> </Panel>
); );
} }

View File

@ -18,7 +18,7 @@ export default class ShowMap extends PureComponent {
updateFilter: PropTypes.func updateFilter: PropTypes.func
}; };
renderSuperBlocks(superBlocks) { renderSuperBlocks(superBlocks, setChallenge) {
if (!Array.isArray(superBlocks) || !superBlocks.length) { if (!Array.isArray(superBlocks) || !superBlocks.length) {
return <div>No Super Blocks</div>; return <div>No Super Blocks</div>;
} }
@ -26,13 +26,20 @@ export default class ShowMap extends PureComponent {
return ( return (
<SuperBlock <SuperBlock
key={ superBlock.title } key={ superBlock.title }
setChallenge={ setChallenge }
{ ...superBlock }/> { ...superBlock }/>
); );
}); });
} }
render() { render() {
const { superBlocks, updateFilter, clearFilter, filter } = this.props; const {
setChallenge,
superBlocks,
updateFilter,
clearFilter,
filter
} = this.props;
const inputIcon = !filter ? const inputIcon = !filter ?
searchIcon : searchIcon :
<span onClick={ clearFilter }>{ clearIcon }</span>; <span onClick={ clearFilter }>{ clearIcon }</span>;
@ -70,7 +77,7 @@ export default class ShowMap extends PureComponent {
</div> </div>
<div <div
className='map-accordion'> className='map-accordion'>
{ this.renderSuperBlocks(superBlocks) } { this.renderSuperBlocks(superBlocks, setChallenge) }
<FullStack /> <FullStack />
<CodingPrep /> <CodingPrep />
</div> </div>

View File

@ -10,13 +10,15 @@ import {
clearFilter, clearFilter,
updateFilter updateFilter
} from '../redux/actions'; } from '../redux/actions';
import { fetchChallenges } from '../../../redux/actions'; import { setChallenge, fetchChallenges } from '../../../redux/actions';
const bindableActions = { const bindableActions = {
clearFilter, clearFilter,
fetchChallenges, fetchChallenges,
updateFilter updateFilter,
setChallenge
}; };
const superBlocksSelector = createSelector( const superBlocksSelector = createSelector(
state => state.app.superBlocks, state => state.app.superBlocks,
state => state.entities.superBlock, state => state.entities.superBlock,
@ -69,7 +71,8 @@ export class ShowMap extends PureComponent {
clearFilter: PropTypes.func, clearFilter: PropTypes.func,
filter: PropTypes.string, filter: PropTypes.string,
superBlocks: PropTypes.array, superBlocks: PropTypes.array,
updateFilter: PropTypes.func updateFilter: PropTypes.func,
setChallenge: PropTypes.func
}; };
render() { render() {

View File

@ -0,0 +1,8 @@
export const HTML = '0';
export const JS = '1';
export const VIDEO = '2';
export const ZIPLINE = '3';
export const BASEJUMP = '4';
export const BONFIRE = '5';
export const HIKES = '6';
export const STEP = '7';

98
common/utils/polyvinyl.js Normal file
View File

@ -0,0 +1,98 @@
// originally base off of https://github.com/gulpjs/vinyl
import path from 'path';
import replaceExt from 'replace-ext';
export default class File {
constructor({
path,
history = [],
base,
contents = ''
} = {}) {
// Record path change
this.history = path ? [path] : history;
this.base = base;
this.contents = contents;
this._isPolyVinyl = true;
this.error = null;
}
static isPolyVinyl = function(file) {
return file && file._isPolyVinyl === true || false;
};
isEmpty() {
return !this._contents;
}
toJSON() {
return Object
.keys(this)
.reduce((obj, key) => (obj[key] = this[key], obj), {});
}
get contents() {
return this._contents;
}
set contents(val) {
if (typeof val !== 'string') {
throw new TypeError('File.contents can only a String');
}
this._contents = val;
}
get basename() {
if (!this.path) {
throw new Error('No path specified! Can not get basename.');
}
return path.basename(this.path);
}
set basename(basename) {
if (!this.path) {
throw new Error('No path specified! Can not set basename.');
}
this.path = path.join(path.dirname(this.path), basename);
}
get extname() {
if (!this.path) {
throw new Error('No path specified! Can not get extname.');
}
return path.extname(this.path);
}
set extname(extname) {
if (!this.path) {
throw new Error('No path specified! Can not set extname.');
}
this.path = replaceExt(this.path, extname);
}
get path() {
return this.history[this.history.length - 1];
}
set path(path) {
if (typeof path !== 'string') {
throw new TypeError('path should be string');
}
// Record history only when path changed
if (path && path !== this.path) {
this.history.push(path);
}
}
get error() {
return this._error;
}
set error(err) {
if (typeof err !== 'object') {
throw new TypeError('error must be an object or null');
}
this.error = err;
}
}

View File

@ -1,7 +1,12 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr'; import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
import { nameify, dasherize } from '../utils'; import { nameify, dasherize, unDasherize } from '../utils';
import debug from 'debug';
const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const log = debug('fcc:challenges');
const challenge = new Schema('challenge', { idAttribute: 'dashedName' }); const challenge = new Schema('challenge', { idAttribute: 'dashedName' });
const block = new Schema('block', { idAttribute: 'dashedName' }); const block = new Schema('block', { idAttribute: 'dashedName' });
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' }); const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
@ -72,12 +77,64 @@ function cachedMap(Block) {
.shareReplay(); .shareReplay();
} }
function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) {
return isDev ||
!isComingSoon ||
(isBeta && challengeIsBeta);
}
function getFirstChallenge(challengeMap$) {
return challengeMap$
.map(({ entities: { superBlock, block, challenge }, result }) => {
return challenge[
block[
superBlock[
result[0]
].blocks[0]
].challenges[0]
];
});
}
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 shouldNotFilterComingSoon(challenge) &&
testChallengeName.test(challenge.name);
})
.last({ defaultValue: null })
.flatMap(challengeOrNull => {
if (challengeOrNull) {
return Observable.just(challengeOrNull);
}
return getFirstChallenge(challengeMap$);
})
.map(challenge => ({
entities: { challenge: { [challenge.dashedName]: challenge } },
result: challenge.dashedName
}));
}
export default function mapService(app) { export default function mapService(app) {
const Block = app.models.Block; const Block = app.models.Block;
const challengeMap$ = cachedMap(Block); const challengeMap$ = cachedMap(Block);
return { return {
name: 'map', name: 'map',
read: (req, resource, params, config, cb) => { read: (req, resource, { dashedName } = {}, config, cb) => {
if (dashedName) {
return getChallengeByDashedName(dashedName, challengeMap$)
.subscribe(challenge => cb(null, challenge), cb);
}
return challengeMap$.subscribe(map => cb(null, map), cb); return challengeMap$.subscribe(map => cb(null, map), cb);
} }
}; };