Feature(challenges): Add hint system to challenges
This commit is contained in:
@ -170,7 +170,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
<ToastContainer
|
<ToastContainer
|
||||||
className='toast-bottom-right'
|
className='toast-bottom-right'
|
||||||
ref='toaster'
|
ref='toaster'
|
||||||
toastMessageFactory={ toastMessageFactory } />
|
toastMessageFactory={ toastMessageFactory }
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,8 @@ export class Challenge extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
lg={ 3 }
|
lg={ 3 }
|
||||||
md={ 4 }>
|
md={ 4 }
|
||||||
|
>
|
||||||
<Preview />
|
<Preview />
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
@ -85,12 +86,14 @@ export class Challenge extends PureComponent {
|
|||||||
<div>
|
<div>
|
||||||
<Col
|
<Col
|
||||||
lg={ 3 }
|
lg={ 3 }
|
||||||
md={ showPreview ? 3 : 4 }>
|
md={ showPreview ? 3 : 4 }
|
||||||
|
>
|
||||||
<SidePanel />
|
<SidePanel />
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
lg={ showPreview ? 6 : 9 }
|
lg={ showPreview ? 6 : 9 }
|
||||||
md={ showPreview ? 5 : 8 }>
|
md={ showPreview ? 5 : 8 }
|
||||||
|
>
|
||||||
<Editor
|
<Editor
|
||||||
content={ content }
|
content={ content }
|
||||||
executeChallenge={ executeChallenge }
|
executeChallenge={ executeChallenge }
|
||||||
|
@ -9,25 +9,31 @@ import TestSuite from './Test-Suite.jsx';
|
|||||||
import Output from './Output.jsx';
|
import Output from './Output.jsx';
|
||||||
import ToolPanel from './Tool-Panel.jsx';
|
import ToolPanel from './Tool-Panel.jsx';
|
||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
|
import { updateHint, executeChallenge } from '../../redux/actions';
|
||||||
|
import { makeToast } from '../../../../redux/actions';
|
||||||
|
|
||||||
|
const bindableActions = { makeToast, executeChallenge, updateHint };
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
state => state.app.windowHeight,
|
state => state.app.windowHeight,
|
||||||
state => state.app.navHeight,
|
state => state.app.navHeight,
|
||||||
state => state.challengesApp.tests,
|
state => state.challengesApp.tests,
|
||||||
state => state.challengesApp.output,
|
state => state.challengesApp.output,
|
||||||
|
state => state.challengesApp.hintIndex,
|
||||||
(
|
(
|
||||||
{ challenge: { title, description } = {} },
|
{ challenge: { title, description, hints = [] } = {} },
|
||||||
windowHeight,
|
windowHeight,
|
||||||
navHeight,
|
navHeight,
|
||||||
tests,
|
tests,
|
||||||
output
|
output,
|
||||||
|
hintIndex
|
||||||
) => ({
|
) => ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
height: windowHeight - navHeight - 20,
|
height: windowHeight - navHeight - 20,
|
||||||
tests,
|
tests,
|
||||||
output
|
output,
|
||||||
|
hint: hints[hintIndex]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -43,7 +49,10 @@ export class SidePanel extends PureComponent {
|
|||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
tests: PropTypes.arrayOf(PropTypes.object),
|
tests: PropTypes.arrayOf(PropTypes.object),
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
output: PropTypes.string
|
output: PropTypes.string,
|
||||||
|
hints: PropTypes.string,
|
||||||
|
updateHint: PropTypes.func,
|
||||||
|
makeToast: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||||
@ -52,14 +61,16 @@ export class SidePanel extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: line }}
|
dangerouslySetInnerHTML={{ __html: line }}
|
||||||
key={ line.slice(-6) + index } />
|
key={ line.slice(-6) + index }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className='wrappable'
|
className='wrappable'
|
||||||
dangerouslySetInnerHTML= {{ __html: line }}
|
dangerouslySetInnerHTML= {{ __html: line }}
|
||||||
key={ line.slice(-6) + index }/>
|
key={ line.slice(-6) + index }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -70,7 +81,11 @@ export class SidePanel extends PureComponent {
|
|||||||
description,
|
description,
|
||||||
height,
|
height,
|
||||||
tests = [],
|
tests = [],
|
||||||
output
|
output,
|
||||||
|
hint,
|
||||||
|
executeChallenge,
|
||||||
|
updateHint,
|
||||||
|
makeToast
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const style = {
|
const style = {
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
@ -82,7 +97,8 @@ export class SidePanel extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref='panel'
|
ref='panel'
|
||||||
style={ style }>
|
style={ style }
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h4 className='text-center challenge-instructions-title'>
|
<h4 className='text-center challenge-instructions-title'>
|
||||||
{ title || 'Happy Coding!' }
|
{ title || 'Happy Coding!' }
|
||||||
@ -91,12 +107,18 @@ export class SidePanel extends PureComponent {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
className='challenge-instructions'
|
className='challenge-instructions'
|
||||||
xs={ 12 }>
|
xs={ 12 }
|
||||||
|
>
|
||||||
{ this.renderDescription(description, this.descriptionRegex) }
|
{ this.renderDescription(description, this.descriptionRegex) }
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
<ToolPanel />
|
<ToolPanel
|
||||||
|
executeChallenge={ executeChallenge }
|
||||||
|
hint={ hint }
|
||||||
|
makeToast={ makeToast }
|
||||||
|
updateHint={ updateHint }
|
||||||
|
/>
|
||||||
<Output output={ output }/>
|
<Output output={ output }/>
|
||||||
<br />
|
<br />
|
||||||
<TestSuite tests={ tests } />
|
<TestSuite tests={ tests } />
|
||||||
@ -105,4 +127,7 @@ export class SidePanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(SidePanel);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
bindableActions
|
||||||
|
)(SidePanel);
|
||||||
|
@ -25,13 +25,15 @@ export default class extends PureComponent {
|
|||||||
<Row key={ text.slice(-6) + index }>
|
<Row key={ text.slice(-6) + index }>
|
||||||
<Col
|
<Col
|
||||||
className='text-center'
|
className='text-center'
|
||||||
xs={ 2 }>
|
xs={ 2 }
|
||||||
|
>
|
||||||
<i className={ iconClass } />
|
<i className={ iconClass } />
|
||||||
</Col>
|
</Col>
|
||||||
<Col
|
<Col
|
||||||
className='test-output'
|
className='test-output'
|
||||||
dangerouslySetInnerHTML={{ __html: text }}
|
dangerouslySetInnerHTML={{ __html: text }}
|
||||||
xs={ 10 } />
|
xs={ 10 }
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -42,7 +44,8 @@ export default class extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='challenge-test-suite'
|
className='challenge-test-suite'
|
||||||
style={{ marginTop: '10px' }}>
|
style={{ marginTop: '10px' }}
|
||||||
|
>
|
||||||
{ this.renderTests(tests) }
|
{ this.renderTests(tests) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,23 +1,48 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
import { Button, ButtonGroup } from 'react-bootstrap';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
|
||||||
import { executeChallenge } from '../../redux/actions';
|
export default class ToolPanel extends PureComponent {
|
||||||
|
constructor(...props) {
|
||||||
const bindableActions = { executeChallenge };
|
super(...props);
|
||||||
|
this.makeHint = this.makeHint.bind(this);
|
||||||
export class ToolPanel extends PureComponent {
|
}
|
||||||
static displayName = 'ToolPanel';
|
static displayName = 'ToolPanel';
|
||||||
|
|
||||||
static propTypes = {
|
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() {
|
render() {
|
||||||
const { executeChallenge } = this.props;
|
const { hint, executeChallenge } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{ this.renderHint(hint, this.makeHint) }
|
||||||
<Button
|
<Button
|
||||||
block={ true }
|
block={ true }
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
@ -58,5 +83,3 @@ export class ToolPanel extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null, bindableActions)(ToolPanel);
|
|
||||||
|
@ -19,6 +19,7 @@ export const fetchChallengeCompleted = createAction(
|
|||||||
entities => ({ entities })
|
entities => ({ entities })
|
||||||
);
|
);
|
||||||
export const resetUi = createAction(types.resetUi);
|
export const resetUi = createAction(types.resetUi);
|
||||||
|
export const updateHint = createAction(types.updateHint);
|
||||||
|
|
||||||
export const fetchChallenges = createAction(types.fetchChallenges);
|
export const fetchChallenges = createAction(types.fetchChallenges);
|
||||||
export const fetchChallengesCompleted = createAction(
|
export const fetchChallengesCompleted = createAction(
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
const initialUiState = {
|
const initialUiState = {
|
||||||
|
hintIndex: 0,
|
||||||
// step index tracing
|
// step index tracing
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
previousIndex: -1,
|
previousIndex: -1,
|
||||||
@ -64,12 +65,19 @@ const mainReducer = handleActions(
|
|||||||
legacyKey: challenge.name,
|
legacyKey: challenge.name,
|
||||||
challenge: challenge.dashedName,
|
challenge: challenge.dashedName,
|
||||||
key: getFileKey(challenge),
|
key: getFileKey(challenge),
|
||||||
tests: createTests(challenge)
|
tests: createTests(challenge),
|
||||||
|
numOfHints: Array.isArray(challenge.hints) ? challenge.hints.length : 0
|
||||||
}),
|
}),
|
||||||
[types.updateTests]: (state, { payload: tests }) => ({
|
[types.updateTests]: (state, { payload: tests }) => ({
|
||||||
...state,
|
...state,
|
||||||
tests
|
tests
|
||||||
}),
|
}),
|
||||||
|
[types.updateHint]: state => ({
|
||||||
|
...state,
|
||||||
|
hintIndex: state.hintIndex + 1 >= state.numOfHints ?
|
||||||
|
0 :
|
||||||
|
state.hintIndex + 1
|
||||||
|
}),
|
||||||
[types.executeChallenge]: state => ({
|
[types.executeChallenge]: state => ({
|
||||||
...state,
|
...state,
|
||||||
tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
|
tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
|
||||||
|
@ -13,6 +13,7 @@ export default createTypes([
|
|||||||
'updateCurrentChallenge',
|
'updateCurrentChallenge',
|
||||||
'replaceChallenge',
|
'replaceChallenge',
|
||||||
'resetUi',
|
'resetUi',
|
||||||
|
'updateHint',
|
||||||
|
|
||||||
// map
|
// map
|
||||||
'updateFilter',
|
'updateFilter',
|
||||||
|
@ -51,6 +51,11 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Used to determine super block order, set by prepending two digit number to super block folder name"
|
"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": {
|
"block": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,11 @@
|
|||||||
"",
|
"",
|
||||||
"sumAll([1, 4]);"
|
"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": [
|
"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}"
|
"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}"
|
||||||
],
|
],
|
||||||
|
Reference in New Issue
Block a user