feat(quiz): initial quiz view which can be used for multiple choice (#15743)

quizes
This commit is contained in:
Cody Seibert
2017-08-08 15:31:26 -04:00
committed by Quincy Larson
parent 93148665c6
commit 9f875e1d11
17 changed files with 645 additions and 6 deletions

View File

@ -10,6 +10,7 @@ import Classic from './views/classic';
import Step from './views/step';
import Project from './views/project';
import BackEnd from './views/backend';
import Quiz from './views/quiz';
import { challengeMetaSelector } from './redux';
import {
@ -27,7 +28,8 @@ const views = {
classic: Classic,
project: Project,
simple: Project,
step: Step
step: Step,
quiz: Quiz
};
const mapDispatchToProps = {

View File

@ -3,13 +3,15 @@ import { panesMap as backendPanesMap } from './views/backend';
import { panesMap as classicPanesMap } from './views/classic';
import { panesMap as stepPanesMap } from './views/step';
import { panesMap as projectPanesMap } from './views/project';
import { panesMap as quizPanesMap } from './views/quiz';
export function createPanesMap() {
return {
...backendPanesMap,
...classicPanesMap,
...stepPanesMap,
...projectPanesMap
...projectPanesMap,
...quizPanesMap
};
}

View File

@ -138,6 +138,7 @@ const submitters = {
backend: submitBackendChallenge,
step: submitSimpleChallenge,
video: submitSimpleChallenge,
quiz: submitSimpleChallenge,
'project.frontEnd': submitProject,
'project.backEnd': submitProject,
'project.simple': submitSimpleChallenge

View File

@ -27,6 +27,7 @@ import { bonfire, html, js } from '../../../utils/challengeTypes';
import blockNameify from '../../../utils/blockNameify';
import { createPoly, setContent } from '../../../../utils/polyvinyl';
import createStepReducer, { epics as stepEpics } from '../views/step/redux';
import createQuizReducer from '../views/quiz/redux';
import createProjectReducer from '../views/project/redux';
// this is not great but is ok until we move to a different form type
@ -361,6 +362,7 @@ export default function createReducers() {
return [
reducer,
...createStepReducer(),
...createProjectReducer()
...createProjectReducer(),
...createQuizReducer()
];
}

View File

@ -14,6 +14,7 @@ export const viewTypes = {
// formally hikes
[ challengeTypes.video ]: 'video',
[ challengeTypes.step ]: 'step',
[ challengeTypes.quiz ]: 'quiz',
backend: 'backend'
};
@ -34,6 +35,7 @@ export const submitTypes = {
// formally hikes
[ challengeTypes.video ]: 'video',
[ challengeTypes.step ]: 'step',
[ challengeTypes.quiz ]: 'quiz',
backend: 'backend'
};

View File

@ -1,2 +1,3 @@
&{ @import "./classic/classic.less"; }
&{ @import "./step/step.less"; }
&{ @import "./quiz/quiz.less"; }

View File

@ -2,12 +2,11 @@ import React from 'react';
import Main from './Project.jsx';
import { types } from '../../redux';
import Panes from '../../../../Panes';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
import Panes from '../../../../Panes';
const propTypes = {};
export const panesMap = {
[types.toggleMap]: 'Map',
[types.toggleMain]: 'Main'

View File

@ -0,0 +1,82 @@
import React, { PropTypes, PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { createSelector } from 'reselect';
import {
selectChoice,
incrementCorrect
} from './redux';
const mapStateToProps = createSelector(
() => ({})
);
function mapDispatchToProps(dispatch) {
return () => bindActionCreators({
selectChoice,
incrementCorrect
}, dispatch);
}
const propTypes = {
choice: PropTypes.string,
choiceIndex: PropTypes.number,
incrementCorrect: PropTypes.func,
isChoiceSelected: PropTypes.bool,
isCorrectChoice: PropTypes.bool,
selectChoice: PropTypes.func,
selected: PropTypes.bool
};
export class Choice extends PureComponent {
constructor(props) {
super(props);
this.selectChoice = this.selectChoice.bind(this);
}
selectChoice() {
if (this.props.isChoiceSelected) {
return;
}
if (this.props.isCorrectChoice) {
this.props.incrementCorrect();
}
this.props.selectChoice(this.props.choiceIndex);
}
render() {
const choiceClass = classnames({
choice: true,
selected: this.props.selected,
correct: this.props.isCorrectChoice,
reveal: this.props.isChoiceSelected
});
return (
<div
className={choiceClass}
onClick={this.selectChoice}
>
<div className='radio'>
<div className='inside' />
</div>
<div
className='text'
dangerouslySetInnerHTML={{__html: this.props.choice}}
/>
</div>
);
}
}
Choice.displayName = 'Choice';
Choice.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Choice);

View File

@ -0,0 +1,222 @@
import React, { PropTypes, PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Col, Row } from 'react-bootstrap';
import Choice from './Choice.jsx';
import {
currentIndexSelector,
selectedChoiceSelector,
nextQuestion,
selectChoice,
correctSelector,
incrementCorrect,
resetQuiz,
resetChoice
} from './redux';
import { submitChallenge, challengeMetaSelector } from '../../redux';
import { challengeSelector } from '../../../../redux';
const mapStateToProps = createSelector(
challengeSelector,
challengeMetaSelector,
currentIndexSelector,
selectedChoiceSelector,
correctSelector,
(
{
description = [],
title
},
meta,
currentIndex,
selectedChoice,
correct
) => ({
title,
description,
meta,
currentIndex,
selectedChoice,
correct
})
);
function mapDispatchToProps(dispatch) {
return () => bindActionCreators({
nextQuestion,
selectChoice,
incrementCorrect,
resetQuiz,
resetChoice,
submitChallenge
}, dispatch);
}
const propTypes = {
correct: PropTypes.number,
currentIndex: PropTypes.number,
description: PropTypes.string,
meta: PropTypes.object,
nextQuestion: PropTypes.fun,
resetChoice: PropTypes.fun,
resetQuiz: PropTypes.fun,
selectedChoice: PropTypes.number,
submitChallenge: PropTypes.fun
};
export class QuizChallenge extends PureComponent {
constructor(props) {
super(props);
this.nextQuestion = this.nextQuestion.bind(this);
this.submitChallenge = this.submitChallenge.bind(this);
}
nextQuestion() {
this.props.resetChoice();
this.props.nextQuestion();
}
submitChallenge() {
this.props.resetQuiz();
this.props.submitChallenge();
}
renderTitle() {
return (
<Row className='quizTitle'>
<Col md={12}>
<h4>{this.props.meta.title}</h4>
<hr/>
</Col>
</Row>
);
}
renderResults() {
const isQuizPassed = this.props.correct === this.props.description.length;
return (
<div>
{this.renderTitle()}
<Row className='quizResults'>
<Col md={12}>
<h2>Quiz Results:</h2>
<p>
You got {this.props.correct} out of
{this.props.description.length} correct!
</p>
{isQuizPassed === false ? (
<div>
<p>
You will need to get all the questions
correct in order to mark this quiz as completed.
</p>
<button
className='btn btn-lg btn-primary'
onClick={this.props.resetQuiz}
> Try Again
</button>
</div>
) : (
<button
className='btn btn-lg btn-primary'
onClick={this.submitChallenge}
> Finish Quiz
</button>
)}
</Col>
</Row>
</div>
);
}
renderQuiz() {
const currentIndex = this.props.currentIndex;
const question = this.props.description[currentIndex];
return (
<div>
{this.renderTitle()}
<Row>
<Col md={6}>
<h2 className='textCenter'>
Question {currentIndex + 1} of {this.props.description.length}:
</h2>
<h3>
{question.subtitle}:
</h3>
<p dangerouslySetInnerHTML={{__html: question.question}} />
</Col>
<Col md={6}>
<h2 className='textCenter'>Choices</h2>
{question.choices.map((choice, i) => (
<Choice
choice={choice}
choiceIndex={i}
isChoiceSelected={this.props.selectedChoice !== null}
isCorrectChoice={question.answer === i}
key={choice}
selected={i === this.props.selectedChoice}
/>
))}
</Col>
</Row>
{this.props.selectedChoice !== null &&
<Row className='quizResults'>
<Col md={6} mdPush={3}>
<div className='messageDiv'>
{this.props.selectedChoice === question.answer
? <h2 className='correctAnswer'>
Correct, great work!
</h2>
: <h2 className='wrongAnswer'>
Sorry, that is not correct!
</h2>}
</div>
{this.props.selectedChoice !== question.answer &&
<div className='explanation'>
<h2>Explanation:</h2>
<p dangerouslySetInnerHTML={{__html: question.explanation}} />
</div>}
</Col>
<Col md={12}>
<button
className='btn btn-lg btn-primary'
onClick={this.nextQuestion}
>
{currentIndex + 1 === this.props.description.length ?
'View Results' : 'Next Question'}
</button>
</Col>
</Row>}
</div>
);
}
render() {
return (
<div className='quiz'>{
this.props.currentIndex >= this.props.description.length ?
this.renderResults() : this.renderQuiz()}
</div>
);
}
}
QuizChallenge.displayName = 'QuizChallenge';
QuizChallenge.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(QuizChallenge);

View File

@ -0,0 +1,33 @@
import React from 'react';
import Main from './Quiz.jsx';
import { types } from '../../redux';
import Panes from '../../../../Panes';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
const propTypes = {};
export const panesMap = {
[types.toggleMap]: 'Map',
[types.toggleMain]: 'Main'
};
const nameToComponent = {
Map: {
Component: _Map
},
Main: {
Component: Main
}
};
export default function ShowQuiz() {
return (
<ChildContainer isFullWidth={ true }>
<Panes nameToComponent={ nameToComponent }/>
</ChildContainer>
);
}
ShowQuiz.displayName = 'ShowQuiz';
ShowQuiz.propTypes = propTypes;

View File

@ -0,0 +1 @@
export { default, panesMap } from './Show.jsx';

View File

@ -0,0 +1 @@
"quiz"

View File

@ -0,0 +1,93 @@
// should match ./ns.json value and filename
@ns: quiz;
.quiz {
padding-left: 20px;
padding-right: 20px;
.quizTitle {
margin-top: 10px;
text-align: center;
}
.textCenter {
text-align: center;
}
.explanation p {
text-align: left;
}
.quizResults {
text-align: center;
}
.wrongAnswer {
color: red;
}
.correctAnswer {
color: green;
}
.choice.selected {
.radio {
.inside {
opacity: 1;
}
}
}
.choice:not(.reveal):hover > .radio {
.inside {
opacity: 1;
}
}
.choice {
position: relative;
padding: 10px;
margin-bottom: 10px;
border-radius: 10px;
.text {
}
.radio {
width: 24px;
height: 24px;
border-radius: 100%;
border: 4px solid #333;
float: left;
margin-right: 10px;
.inside {
position: absolute;
width: 12px;
height: 12px;
left: 2px;
top: 2px;
background-color: #333;
border-radius: 100%;
opacity: 0;
}
}
}
.choice:not(.reveal):hover {
background-color: rgba(0, 100, 0, 0.3);
}
.choice:not(.reveal) {
cursor: pointer;
}
.choice.reveal {
background-color: rgba(255, 65, 77, 0.3);
}
.choice.reveal.correct {
background-color: rgba(0, 100, 0, 0.3);
}
}

View File

@ -0,0 +1,79 @@
import { createTypes } from 'redux-create-types';
import { createAction, handleActions } from 'redux-actions';
import noop from 'lodash/noop';
import ns from '../ns.json';
export const types = createTypes([
'nextQuestion',
'selectChoice',
'incrementCorrect',
'resetQuiz',
'resetChoice'
], ns);
export const nextQuestion = createAction(
types.nextQuestion,
noop
);
export const selectChoice = createAction(
types.selectChoice,
(selectedChoice) => ({ selectedChoice })
);
export const incrementCorrect = createAction(
types.incrementCorrect,
noop
);
export const resetQuiz = createAction(
types.resetQuiz,
noop
);
export const resetChoice = createAction(
types.resetChoice,
noop
);
const initialState = {
currentIndex: 0,
selectedChoice: null,
correct: 0
};
export const getNS = state => state[ns];
export const currentIndexSelector = state => getNS(state).currentIndex;
export const selectedChoiceSelector = state => getNS(state).selectedChoice;
export const correctSelector = state => getNS(state).correct;
export default function createReducers() {
const reducer = handleActions({
[types.nextQuestion]: state => ({
...state,
currentIndex: state.currentIndex + 1
}),
[types.selectChoice]: (state, {payload}) => ({
...state,
selectedChoice: payload.selectedChoice
}),
[types.incrementCorrect]: state => ({
...state,
correct: state.correct + 1
}),
[types.resetQuiz]: state => ({
...state,
currentIndex: 0,
correct: 0,
selectedChoice: null
}),
[types.resetChoice]: state => ({
...state,
selectedChoice: null
})
}, initialState);
reducer.toString = () => ns;
return [ reducer ];
}

View File

@ -9,3 +9,4 @@ export const backEndProject = 4;
export const bonfire = 5;
export const video = 6;
export const step = 7;
export const quiz = 8;

View File

@ -81,7 +81,13 @@
""
]
],
"challengeSeed": [],
"challengeSeed": [
"function sym(args) {",
" return args;",
"}",
"",
"sym([1, 2, 3], [5, 2, 1, 4]);"
],
"tests": [],
"type": "Waypoint",
"challengeType": 7,

View File

@ -0,0 +1,112 @@
{
"name": "JavaScript Multiple Choice Questions",
"order": 8,
"time": "",
"helpRoom": "HelpJavaScript",
"challenges": [
{
"id": "59874fc749228906236a3275",
"title": "Array.prototype.map",
"description": [
{
"subtitle": "Flooring an Array",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = [1.32, 2.43, 3.9]\n .map(Math.floor);\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>1.32 2.43 3.9</code></pre>",
"<pre><code class='language-javascript'>['1.32', '2.43', '3.9']</code></pre>",
"<pre><code class='language-javascript'>[1, 2, 3]</code></pre>",
"<pre><code class='language-javascript'>'1 2 3'</code></pre>"
],
"answer": 2,
"explanation": "The map function takes a callback function as it's first parameter and applies that function against every value inside the array. In this example, our callback function is the <code>Math.floor</code> function which will truncate the decimal points of all the numbers and convert them to integers."
},
{
"subtitle": "Custom Map Functions",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = ['a', 'b', 'c']\n .map(a => [a.toUpperCase()]);\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>[['A'], ['B'], ['C']]</code></pre>",
"<pre><code class='language-javascript'>['A', 'B', 'C']</code></pre>",
"<pre><code class='language-javascript'>['a', 'b', 'c]</code></pre>",
"<pre><code class='language-javascript'>'ABC'</code></pre>"
],
"answer": 0,
"explanation": "The map function will return a new array with each element equal to the old element ran through a callback function. Our callback function takes our original element, changes it to a upper case, and then wraps it in an array; thus, leaving us with <code>[['A', 'B', 'C']]</code>"
},
{
"subtitle": "Maps on Maps",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = [[4, 1], [2, 0], [3, 3]]\n .map(a => \n a.map(b => b % 2)[0] + a.map(b => b - 1)[1]\n )\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>[[0, 1], [0, 0], [1, 1]]</code></pre>",
"<pre><code class='language-javascript'>[[0, 0], [0, -1], [1, 2]]</code></pre>",
"<pre><code class='language-javascript'>[1, 1, 2]</code></pre>",
"<pre><code class='language-javascript'>[0, -1, 3]</code></pre>"
],
"answer": 3,
"explanation": "This answer can be explained by first looking at the example of what happens with the first element in our array, <code>[4, 1]</code>. Our first map callback will run a mod 2 map function over <code>[4, 1]</code> leaving us with a new array of <code>[0, 1]</code>. The second map call which is inside our callback will subtract 1 from every element, leaving us with <code>[3, 0]</code>. Last, we take element at index 0, <code>0</code>, and add it to element of index 1 from our second map function, <code>0</code>, leaving us with 0; thus, after the first iteration of the top level map function, we are left with an array that looks like so: <code>[1, [2, 0], [3, 3]]</code>. We simply keep doing that logic for the other elements until we finish: <code>[1, -1, [3, 3]]</code>, and <code>[1, -1, 3]</code>"
},
{
"subtitle": "Words Containing 'a'",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = ['apple', 'dog', 'cat']\n .map((a, i) => \n a.indexOf('a') !== -1 ? i : null)\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>[0, -1, 1]</code></pre>",
"<pre><code class='language-javascript'>[0, null, 2]</code></pre>",
"<pre><code class='language-javascript'>[null, null, null]</code></pre>",
"<pre><code class='language-javascript'>[-1, null, 2]</code></pre>"
],
"answer": 1,
"explanation": "This map example will return an array where each elements of the new array is either the original array index when the element contains the character 'a'; otherwise, an element of null for any words that do not have the character 'a'."
},
{
"subtitle": "Accessing the Original Array Elements",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = [1, 2, 3]\n .map((a, _, o) => a + o[0])\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>[1, 2, 3]</code></pre>",
"<pre><code class='language-javascript'>[0, 0, 0]</code></pre>",
"<pre><code class='language-javascript'>[3, 2, 1]</code></pre>",
"<pre><code class='language-javascript'>[2, 3, 4]</code></pre>"
],
"answer": 3,
"explanation": "This map example will add the value of the first element in the original array to all the other elements in the array."
},
{
"subtitle": "More Map Hacking",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = [8, 5, 3]\n .map((a, i, o) => o[o.length - i - i])\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>[3, 5, 8]</code></pre>",
"<pre><code class='language-javascript'>[5, 3, 8]</code></pre>",
"<pre><code class='language-javascript'>[8, 5, 3]</code></pre>",
"<pre><code class='language-javascript'>[3, 8, 5]</code></pre>"
],
"answer": 0,
"explanation": "This map example will reverse the array. The third argument to the map callback function is the original array; therefore, we can use the current index in the map function, and work our way backwards from the end of the array using the o.length."
},
{
"subtitle": "Custom Scoping",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = ['a', 'b', 'c']\n .map(function(a) { return this[a]; }, {a: 9, b: 3, c: 1})\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>['a', 'b', 'c']</code></pre>",
"<pre><code class='language-javascript'>[9, 3, 1]</code></pre>",
"<pre><code class='language-javascript'>[3, 9, 1]</code></pre>",
"<pre><code class='language-javascript'>[{a: 9}, {b: 3}, {c: 1}]</code></pre>"
],
"answer": 1,
"explanation": "This map example will reverse the array. The third argument to the map callback function is the original array; therefore, we can use the current index in the map function, and work our way backwards from the end of the array using the o.length."
},
{
"subtitle": "Reversing in Map, Just Because",
"question": "What will the following code print out?\n<pre><code class='language-javascript'>const results = [1, 2, 3, 4, 5]\n .map((a, _, o) => o.reverse() && a)\nconsole.log(results);</code></pre>",
"choices": [
"<pre><code class='language-javascript'>[5, 4, 3, 2, 1]</code></pre>",
"<pre><code class='language-javascript'>[5, 2, 3, 5, 1]</code></pre>",
"<pre><code class='language-javascript'>[1, 2, 3, 4, 5]</code></pre>",
"<pre><code class='language-javascript'>[1, 4, 3, 2, 5]</code></pre>"
],
"answer": 3,
"explanation": "This map example will reverse the array. The third argument to the map callback function is the original array; therefore, we can use the current index in the map function, and work our way backwards from the end of the array using the o.length."
}
],
"tests": [],
"challengeType": 8
}
]
}