mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor(client): use generic comp for multiple choice (#56825)
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9c73159f10
commit
50f0c23d15
@@ -32,8 +32,6 @@ import {
|
||||
import Scene from '../components/scene/scene';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
|
||||
// Styles
|
||||
import '../video.css';
|
||||
import './show.css';
|
||||
|
||||
// Redux Setup
|
||||
|
||||
@@ -4,6 +4,7 @@ import Helmet from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { Container, Col, Row, Button, Spacer } from '@freecodecamp/ui';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
// Local Utilities
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
@@ -24,6 +25,13 @@ import {
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import { BlockTypes } from '../../../../../shared/config/blocks';
|
||||
import Scene from '../components/scene/scene';
|
||||
import MultipleChoiceQuestions from '../components/multiple-choice-questions';
|
||||
import ChallengeExplanation from '../components/challenge-explanation';
|
||||
import HelpModal from '../components/help-modal';
|
||||
|
||||
// Styles
|
||||
import './show.css';
|
||||
import '../video.css';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = (state: unknown) => ({
|
||||
@@ -35,7 +43,8 @@ const mapDispatchToProps = {
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
updateSolutionFormValues,
|
||||
openCompletionModal: () => openModal('completion')
|
||||
openCompletionModal: () => openModal('completion'),
|
||||
openHelpModal: () => openModal('help')
|
||||
};
|
||||
|
||||
// Types
|
||||
@@ -46,6 +55,7 @@ interface ShowQuizProps {
|
||||
initTests: (xs: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
openCompletionModal: () => void;
|
||||
openHelpModal: () => void;
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
};
|
||||
@@ -63,10 +73,12 @@ const ShowGeneric = ({
|
||||
block,
|
||||
blockType,
|
||||
description,
|
||||
explanation,
|
||||
challengeType,
|
||||
fields: { tests },
|
||||
fields: { blockName, tests },
|
||||
helpCategory,
|
||||
instructions,
|
||||
questions,
|
||||
title,
|
||||
translationPending,
|
||||
scene,
|
||||
@@ -80,6 +92,7 @@ const ShowGeneric = ({
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
openCompletionModal,
|
||||
openHelpModal,
|
||||
isChallengeCompleted
|
||||
}: ShowQuizProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -142,9 +155,36 @@ const ShowGeneric = ({
|
||||
setAssignmentsCompleted(a => (isCompleted ? a + 1 : a - 1));
|
||||
};
|
||||
|
||||
// multiple choice questions
|
||||
const [selectedMcqOptions, setSelectedMcqOptions] = useState(
|
||||
questions.map<number | null>(() => null)
|
||||
);
|
||||
const [submittedMcqAnswers, setSubmittedMcqAnswers] = useState(
|
||||
questions.map<number | null>(() => null)
|
||||
);
|
||||
const [showFeedback, setShowFeedback] = useState(false);
|
||||
|
||||
const handleMcqOptionChange = (
|
||||
questionIndex: number,
|
||||
answerIndex: number
|
||||
): void => {
|
||||
setSelectedMcqOptions(prev =>
|
||||
prev.map((option, index) =>
|
||||
index === questionIndex ? answerIndex : option
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// submit
|
||||
const handleSubmit = () => {
|
||||
if (assignments.length == 0 || allAssignmentsCompleted) {
|
||||
const hasCompletedAssignments =
|
||||
assignments.length === 0 || allAssignmentsCompleted;
|
||||
const mcqSolutions = questions.map(question => question.solution - 1);
|
||||
const mcqCorrect = isEqual(mcqSolutions, selectedMcqOptions);
|
||||
|
||||
setSubmittedMcqAnswers(selectedMcqOptions);
|
||||
setShowFeedback(true);
|
||||
if (hasCompletedAssignments && mcqCorrect) {
|
||||
openCompletionModal();
|
||||
}
|
||||
};
|
||||
@@ -214,15 +254,34 @@ const ShowGeneric = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!questions && (
|
||||
<MultipleChoiceQuestions
|
||||
questions={questions}
|
||||
selectedOptions={selectedMcqOptions}
|
||||
handleOptionChange={handleMcqOptionChange}
|
||||
submittedMcqAnswers={submittedMcqAnswers}
|
||||
showFeedback={showFeedback}
|
||||
/>
|
||||
)}
|
||||
|
||||
{explanation ? (
|
||||
<ChallengeExplanation explanation={explanation} />
|
||||
) : null}
|
||||
|
||||
<Button block={true} variant='primary' onClick={handleSubmit}>
|
||||
{blockType === BlockTypes.review
|
||||
? t('buttons.submit')
|
||||
: t('buttons.check-answer')}
|
||||
</Button>
|
||||
<Spacer size='xxs' />
|
||||
<Button block={true} variant='primary' onClick={openHelpModal}>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Button>
|
||||
|
||||
<Spacer size='l' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
</Row>
|
||||
</Container>
|
||||
</LearnLayout>
|
||||
@@ -248,6 +307,7 @@ export const query = graphql`
|
||||
blockType
|
||||
challengeType
|
||||
description
|
||||
explanation
|
||||
helpCategory
|
||||
instructions
|
||||
fields {
|
||||
@@ -258,6 +318,14 @@ export const query = graphql`
|
||||
testString
|
||||
}
|
||||
}
|
||||
questions {
|
||||
text
|
||||
answers {
|
||||
answer
|
||||
feedback
|
||||
}
|
||||
solution
|
||||
}
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
// Package Utilities
|
||||
import { graphql } from 'gatsby';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { ObserveKeys } from 'react-hotkeys';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Container, Col, Row, Button, Spacer } from '@freecodecamp/ui';
|
||||
import ShortcutsModal from '../components/shortcuts-modal';
|
||||
|
||||
// Local Utilities
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import VideoPlayer from '../components/video-player';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import Scene from '../components/scene/scene';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import ChallegeExplanation from '../components/challenge-explanation';
|
||||
import MultipleChoiceQuestions from '../components/multiple-choice-questions';
|
||||
import Assignments from '../components/assignments';
|
||||
import {
|
||||
challengeMounted,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
updateSolutionFormValues,
|
||||
initTests
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
|
||||
// Styles
|
||||
import './show.css';
|
||||
import '../video.css';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
isChallengeCompletedSelector,
|
||||
(isChallengeCompleted: boolean) => ({
|
||||
isChallengeCompleted
|
||||
})
|
||||
);
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
updateSolutionFormValues,
|
||||
openCompletionModal: () => openModal('completion'),
|
||||
openHelpModal: () => openModal('help')
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
// Types
|
||||
interface ShowOdinProps {
|
||||
challengeMounted: (arg0: string) => void;
|
||||
data: { challengeNode: ChallengeNode };
|
||||
initTests: (xs: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
openCompletionModal: () => void;
|
||||
openHelpModal: () => void;
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
};
|
||||
t: TFunction;
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
updateSolutionFormValues: () => void;
|
||||
}
|
||||
|
||||
interface ShowOdinState {
|
||||
subtitles: string;
|
||||
downloadURL: string | null;
|
||||
selectedMcqOptions: (number | null)[];
|
||||
submittedMcqAnswers: (number | null)[];
|
||||
showFeedback: boolean;
|
||||
assignmentsCompleted: number;
|
||||
allAssignmentsCompleted: boolean;
|
||||
videoIsLoaded: boolean;
|
||||
isScenePlaying: boolean;
|
||||
}
|
||||
|
||||
// Component
|
||||
class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
static displayName: string;
|
||||
private container: React.RefObject<HTMLElement> = React.createRef();
|
||||
|
||||
constructor(props: ShowOdinProps) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { assignments, questions }
|
||||
}
|
||||
}
|
||||
} = this.props;
|
||||
|
||||
this.state = {
|
||||
subtitles: '',
|
||||
downloadURL: null,
|
||||
selectedMcqOptions: questions.map(() => null),
|
||||
submittedMcqAnswers: questions.map(() => null),
|
||||
showFeedback: false,
|
||||
assignmentsCompleted: 0,
|
||||
allAssignmentsCompleted: assignments.length == 0,
|
||||
videoIsLoaded: false,
|
||||
isScenePlaying: false
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const {
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { tests },
|
||||
title,
|
||||
challengeType,
|
||||
helpCategory
|
||||
}
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
initTests,
|
||||
updateChallengeMeta
|
||||
} = this.props;
|
||||
initTests(tests);
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title,
|
||||
challengeType,
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
this.container.current?.focus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ShowOdinProps): void {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { title: prevTitle }
|
||||
}
|
||||
}
|
||||
} = prevProps;
|
||||
const {
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { title: currentTitle, challengeType, helpCategory }
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
updateChallengeMeta
|
||||
} = this.props;
|
||||
if (prevTitle !== currentTitle) {
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title: currentTitle,
|
||||
challengeType,
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { questions }
|
||||
}
|
||||
},
|
||||
openCompletionModal
|
||||
} = this.props;
|
||||
|
||||
// subract 1 because the solutions are 1-indexed
|
||||
const mcqSolutions = questions.map(question => question.solution - 1);
|
||||
|
||||
this.setState({
|
||||
submittedMcqAnswers: this.state.selectedMcqOptions,
|
||||
showFeedback: true
|
||||
});
|
||||
|
||||
const allMcqAnswersCorrect = isEqual(
|
||||
mcqSolutions,
|
||||
this.state.selectedMcqOptions
|
||||
);
|
||||
|
||||
if (this.state.allAssignmentsCompleted && allMcqAnswersCorrect) {
|
||||
openCompletionModal();
|
||||
}
|
||||
};
|
||||
|
||||
handleMcqOptionChange = (
|
||||
questionIndex: number,
|
||||
answerIndex: number
|
||||
): void => {
|
||||
this.setState(state => ({
|
||||
selectedMcqOptions: state.selectedMcqOptions.map((option, index) =>
|
||||
index === questionIndex ? answerIndex : option
|
||||
)
|
||||
}));
|
||||
};
|
||||
|
||||
handleAssignmentChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
totalAssignments: number
|
||||
): void => {
|
||||
const assignmentsCompleted = event.target.checked
|
||||
? this.state.assignmentsCompleted + 1
|
||||
: this.state.assignmentsCompleted - 1;
|
||||
const allAssignmentsCompleted = totalAssignments === assignmentsCompleted;
|
||||
|
||||
this.setState({
|
||||
assignmentsCompleted,
|
||||
allAssignmentsCompleted
|
||||
});
|
||||
};
|
||||
|
||||
onVideoLoad = () => {
|
||||
this.setState({
|
||||
videoIsLoaded: true
|
||||
});
|
||||
};
|
||||
|
||||
setIsScenePlaying = (shouldPlay: boolean) => {
|
||||
this.setState({
|
||||
isScenePlaying: shouldPlay
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
explanation,
|
||||
superBlock,
|
||||
block,
|
||||
videoId,
|
||||
videoLocaleIds,
|
||||
bilibiliIds,
|
||||
fields: { blockName },
|
||||
questions,
|
||||
assignments,
|
||||
translationPending,
|
||||
scene
|
||||
}
|
||||
}
|
||||
},
|
||||
openHelpModal,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
},
|
||||
t,
|
||||
isChallengeCompleted
|
||||
} = this.props;
|
||||
|
||||
const blockNameTitle = `${t(
|
||||
`intro:${superBlock}.blocks.${block}.title`
|
||||
)} - ${title}`;
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
executeChallenge={this.handleSubmit}
|
||||
containerRef={this.container}
|
||||
nextChallengePath={nextChallengePath}
|
||||
prevChallengePath={prevChallengePath}
|
||||
playScene={() => this.setIsScenePlaying(true)}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
|
||||
/>
|
||||
<Container>
|
||||
<Row>
|
||||
{videoId && (
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
<Spacer size='m' />
|
||||
<VideoPlayer
|
||||
bilibiliIds={bilibiliIds}
|
||||
onVideoLoad={this.onVideoLoad}
|
||||
title={title}
|
||||
videoId={videoId}
|
||||
videoIsLoaded={this.state.videoIsLoaded}
|
||||
videoLocaleIds={videoLocaleIds}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='m' />
|
||||
<ChallengeTitle
|
||||
isCompleted={isChallengeCompleted}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
<PrismFormatted className={'line-numbers'} text={description} />
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
|
||||
{scene && (
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={this.state.isScenePlaying}
|
||||
setIsPlaying={this.setIsScenePlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
{instructions && (
|
||||
<PrismFormatted
|
||||
className={'line-numbers'}
|
||||
text={instructions}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ObserveKeys>
|
||||
{assignments.length > 0 && (
|
||||
<Assignments
|
||||
assignments={assignments}
|
||||
allAssignmentsCompleted={
|
||||
this.state.allAssignmentsCompleted
|
||||
}
|
||||
handleAssignmentChange={this.handleAssignmentChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MultipleChoiceQuestions
|
||||
questions={questions}
|
||||
selectedOptions={this.state.selectedMcqOptions}
|
||||
handleOptionChange={this.handleMcqOptionChange}
|
||||
submittedMcqAnswers={this.state.submittedMcqAnswers}
|
||||
showFeedback={this.state.showFeedback}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
|
||||
{explanation ? (
|
||||
<ChallegeExplanation explanation={explanation} />
|
||||
) : (
|
||||
<Spacer size='m' />
|
||||
)}
|
||||
|
||||
<Button
|
||||
block={true}
|
||||
size='medium'
|
||||
variant='primary'
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{t('buttons.check-answer')}
|
||||
</Button>
|
||||
<Spacer size='xxs' />
|
||||
<Button
|
||||
block={true}
|
||||
size='medium'
|
||||
variant='primary'
|
||||
onClick={openHelpModal}
|
||||
>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Button>
|
||||
<Spacer size='l' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
</Row>
|
||||
</Container>
|
||||
<ShortcutsModal />
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShowOdin.displayName = 'ShowOdin';
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(ShowOdin));
|
||||
|
||||
export const query = graphql`
|
||||
query TheOdinProject($id: String!) {
|
||||
challengeNode(id: { eq: $id }) {
|
||||
challenge {
|
||||
videoId
|
||||
videoLocaleIds {
|
||||
espanol
|
||||
italian
|
||||
portuguese
|
||||
}
|
||||
bilibiliIds {
|
||||
aid
|
||||
bvid
|
||||
cid
|
||||
}
|
||||
title
|
||||
description
|
||||
instructions
|
||||
explanation
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
block
|
||||
fields {
|
||||
slug
|
||||
blockName
|
||||
tests {
|
||||
text
|
||||
testString
|
||||
}
|
||||
}
|
||||
questions {
|
||||
text
|
||||
answers {
|
||||
answer
|
||||
feedback
|
||||
}
|
||||
solution
|
||||
}
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
translationPending
|
||||
assignments
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -32,9 +32,6 @@ import {
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
|
||||
// Styles
|
||||
import '../video.css';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
isChallengeCompletedSelector,
|
||||
|
||||
@@ -36,11 +36,6 @@ const video = path.resolve(
|
||||
'../../src/templates/Challenges/video/show.tsx'
|
||||
);
|
||||
|
||||
const odin = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/odin/show.tsx'
|
||||
);
|
||||
|
||||
const exam = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/exam/show.tsx'
|
||||
@@ -69,7 +64,6 @@ const views = {
|
||||
quiz,
|
||||
video,
|
||||
codeAlly,
|
||||
odin,
|
||||
exam,
|
||||
msTrophy,
|
||||
fillInTheBlank,
|
||||
|
||||
@@ -96,13 +96,13 @@ export const viewTypes = {
|
||||
[codeAllyPractice]: 'codeAlly',
|
||||
[codeAllyCert]: 'codeAlly',
|
||||
[multifileCertProject]: 'classic',
|
||||
[theOdinProject]: 'odin',
|
||||
[theOdinProject]: 'generic',
|
||||
[colab]: 'frontend',
|
||||
[exam]: 'exam',
|
||||
[msTrophy]: 'msTrophy',
|
||||
[multipleChoice]: 'odin',
|
||||
[multipleChoice]: 'generic',
|
||||
[python]: 'modern',
|
||||
[dialogue]: 'generic', // TODO: use generic challengeType for dialogues
|
||||
[dialogue]: 'generic',
|
||||
[fillInTheBlank]: 'fillInTheBlank',
|
||||
[multifilePythonCertProject]: 'classic',
|
||||
[generic]: 'generic'
|
||||
|
||||
Reference in New Issue
Block a user