Feature(challenges): Add hint system to challenges

This commit is contained in:
Berkeley Martinez
2016-06-14 18:47:43 -07:00
parent 4b1d191fd5
commit ccc5c3ec24
10 changed files with 105 additions and 30 deletions

View File

@ -130,7 +130,7 @@ export class FreeCodeCamp extends React.Component {
bsStyle='primary'
className='animated fadeIn'
onClick={ submitChallenge }
>
>
Submit and go to my next challenge
</Button>
);
@ -170,7 +170,8 @@ export class FreeCodeCamp extends React.Component {
<ToastContainer
className='toast-bottom-right'
ref='toaster'
toastMessageFactory={ toastMessageFactory } />
toastMessageFactory={ toastMessageFactory }
/>
</div>
);
}

View File

@ -66,7 +66,8 @@ export class Challenge extends PureComponent {
return (
<Col
lg={ 3 }
md={ 4 }>
md={ 4 }
>
<Preview />
</Col>
);
@ -85,12 +86,14 @@ export class Challenge extends PureComponent {
<div>
<Col
lg={ 3 }
md={ showPreview ? 3 : 4 }>
md={ showPreview ? 3 : 4 }
>
<SidePanel />
</Col>
<Col
lg={ showPreview ? 6 : 9 }
md={ showPreview ? 5 : 8 }>
md={ showPreview ? 5 : 8 }
>
<Editor
content={ content }
executeChallenge={ executeChallenge }

View File

@ -9,25 +9,31 @@ import TestSuite from './Test-Suite.jsx';
import Output from './Output.jsx';
import ToolPanel from './Tool-Panel.jsx';
import { challengeSelector } from '../../redux/selectors';
import { updateHint, executeChallenge } from '../../redux/actions';
import { makeToast } from '../../../../redux/actions';
const bindableActions = { makeToast, executeChallenge, updateHint };
const mapStateToProps = createSelector(
challengeSelector,
state => state.app.windowHeight,
state => state.app.navHeight,
state => state.challengesApp.tests,
state => state.challengesApp.output,
state => state.challengesApp.hintIndex,
(
{ challenge: { title, description } = {} },
{ challenge: { title, description, hints = [] } = {} },
windowHeight,
navHeight,
tests,
output
output,
hintIndex
) => ({
title,
description,
height: windowHeight - navHeight - 20,
tests,
output
output,
hint: hints[hintIndex]
})
);
@ -43,7 +49,10 @@ export class SidePanel extends PureComponent {
height: PropTypes.number,
tests: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string,
output: PropTypes.string
output: PropTypes.string,
hints: PropTypes.string,
updateHint: PropTypes.func,
makeToast: PropTypes.func
};
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
@ -52,14 +61,16 @@ export class SidePanel extends PureComponent {
return (
<div
dangerouslySetInnerHTML={{ __html: line }}
key={ line.slice(-6) + index } />
key={ line.slice(-6) + index }
/>
);
}
return (
<p
className='wrappable'
dangerouslySetInnerHTML= {{ __html: line }}
key={ line.slice(-6) + index }/>
key={ line.slice(-6) + index }
/>
);
});
}
@ -70,7 +81,11 @@ export class SidePanel extends PureComponent {
description,
height,
tests = [],
output
output,
hint,
executeChallenge,
updateHint,
makeToast
} = this.props;
const style = {
overflowX: 'hidden',
@ -82,7 +97,8 @@ export class SidePanel extends PureComponent {
return (
<div
ref='panel'
style={ style }>
style={ style }
>
<div>
<h4 className='text-center challenge-instructions-title'>
{ title || 'Happy Coding!' }
@ -91,12 +107,18 @@ export class SidePanel extends PureComponent {
<Row>
<Col
className='challenge-instructions'
xs={ 12 }>
xs={ 12 }
>
{ this.renderDescription(description, this.descriptionRegex) }
</Col>
</Row>
</div>
<ToolPanel />
<ToolPanel
executeChallenge={ executeChallenge }
hint={ hint }
makeToast={ makeToast }
updateHint={ updateHint }
/>
<Output output={ output }/>
<br />
<TestSuite tests={ tests } />
@ -105,4 +127,7 @@ export class SidePanel extends PureComponent {
}
}
export default connect(mapStateToProps)(SidePanel);
export default connect(
mapStateToProps,
bindableActions
)(SidePanel);

View File

@ -25,13 +25,15 @@ export default class extends PureComponent {
<Row key={ text.slice(-6) + index }>
<Col
className='text-center'
xs={ 2 }>
xs={ 2 }
>
<i className={ iconClass } />
</Col>
<Col
className='test-output'
dangerouslySetInnerHTML={{ __html: text }}
xs={ 10 } />
xs={ 10 }
/>
</Row>
);
});
@ -42,7 +44,8 @@ export default class extends PureComponent {
return (
<div
className='challenge-test-suite'
style={{ marginTop: '10px' }}>
style={{ marginTop: '10px' }}
>
{ this.renderTests(tests) }
</div>
);

View File

@ -1,23 +1,48 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Button, ButtonGroup } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component';
import { executeChallenge } from '../../redux/actions';
const bindableActions = { executeChallenge };
export class ToolPanel extends PureComponent {
export default class ToolPanel extends PureComponent {
constructor(...props) {
super(...props);
this.makeHint = this.makeHint.bind(this);
}
static displayName = 'ToolPanel';
static propTypes = {
executeChallenge: PropTypes.func
executeChallenge: PropTypes.func,
updateHint: PropTypes.func,
hint: PropTypes.string
};
makeHint() {
this.props.makeToast({
message: this.props.hint
});
this.props.updateHint();
}
renderHint(hint, makeHint) {
if (!hint) {
return null;
}
return (
<Button
block={ true }
bsStyle='primary'
className='btn-big'
onClick={ makeHint }
>
Hint
</Button>
);
}
render() {
const { executeChallenge } = this.props;
const { hint, executeChallenge } = this.props;
return (
<div>
{ this.renderHint(hint, this.makeHint) }
<Button
block={ true }
bsStyle='primary'
@ -58,5 +83,3 @@ export class ToolPanel extends PureComponent {
);
}
}
export default connect(null, bindableActions)(ToolPanel);

View File

@ -19,6 +19,7 @@ export const fetchChallengeCompleted = createAction(
entities => ({ entities })
);
export const resetUi = createAction(types.resetUi);
export const updateHint = createAction(types.updateHint);
export const fetchChallenges = createAction(types.fetchChallenges);
export const fetchChallengesCompleted = createAction(

View File

@ -12,6 +12,7 @@ import {
} from '../utils';
const initialUiState = {
hintIndex: 0,
// step index tracing
currentIndex: 0,
previousIndex: -1,
@ -64,12 +65,19 @@ const mainReducer = handleActions(
legacyKey: challenge.name,
challenge: challenge.dashedName,
key: getFileKey(challenge),
tests: createTests(challenge)
tests: createTests(challenge),
numOfHints: Array.isArray(challenge.hints) ? challenge.hints.length : 0
}),
[types.updateTests]: (state, { payload: tests }) => ({
...state,
tests
}),
[types.updateHint]: state => ({
...state,
hintIndex: state.hintIndex + 1 >= state.numOfHints ?
0 :
state.hintIndex + 1
}),
[types.executeChallenge]: state => ({
...state,
tests: state.tests.map(test => ({ ...test, err: false, pass: false }))

View File

@ -13,6 +13,7 @@ export default createTypes([
'updateCurrentChallenge',
'replaceChallenge',
'resetUi',
'updateHint',
// map
'updateFilter',

View File

@ -51,6 +51,11 @@
"type": "number",
"description": "Used to determine super block order, set by prepending two digit number to super block folder name"
},
"hint": {
"type": "array",
"default": [],
"description": "hints must be a single line of plain text"
},
"block": {
"type": "string"
},

View File

@ -19,6 +19,11 @@
"",
"sumAll([1, 4]);"
],
"hints": [
"Use Math.max() to find the maximum value of two numbers.",
"Use Math.min() to find the minimum value of two numbers.",
"Remember to that you must add all the numbers in between so this would require a way to get those numbers."
],
"solutions": [
"function sumAll(arr) {\n var sum = 0;\n arr.sort(function(a,b) {return a-b;});\n for (var i = arr[0]; i <= arr[1]; i++) {\n sum += i; \n }\n return sum;\n}"
],