From 50f0c23d15d40dafb22c0251665750be1edb759a Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 7 Nov 2024 13:02:14 +0100 Subject: [PATCH] refactor(client): use generic comp for multiple choice (#56825) Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com> --- .../Challenges/fill-in-the-blank/show.tsx | 2 - .../Challenges/{odin => generic}/show.css | 0 .../src/templates/Challenges/generic/show.tsx | 74 ++- client/src/templates/Challenges/odin/show.tsx | 478 ------------------ .../src/templates/Challenges/video/show.tsx | 3 - client/utils/gatsby/challenge-page-creator.js | 6 - shared/config/challenge-types.ts | 6 +- 7 files changed, 74 insertions(+), 495 deletions(-) rename client/src/templates/Challenges/{odin => generic}/show.css (100%) delete mode 100644 client/src/templates/Challenges/odin/show.tsx diff --git a/client/src/templates/Challenges/fill-in-the-blank/show.tsx b/client/src/templates/Challenges/fill-in-the-blank/show.tsx index ddd8ed30c05..1c1b757f683 100644 --- a/client/src/templates/Challenges/fill-in-the-blank/show.tsx +++ b/client/src/templates/Challenges/fill-in-the-blank/show.tsx @@ -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 diff --git a/client/src/templates/Challenges/odin/show.css b/client/src/templates/Challenges/generic/show.css similarity index 100% rename from client/src/templates/Challenges/odin/show.css rename to client/src/templates/Challenges/generic/show.css diff --git a/client/src/templates/Challenges/generic/show.tsx b/client/src/templates/Challenges/generic/show.tsx index 53c1a11d73c..b3eb9ece1af 100644 --- a/client/src/templates/Challenges/generic/show.tsx +++ b/client/src/templates/Challenges/generic/show.tsx @@ -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(() => null) + ); + const [submittedMcqAnswers, setSubmittedMcqAnswers] = useState( + questions.map(() => 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 && ( + + )} + + {explanation ? ( + + ) : null} + + + + @@ -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 diff --git a/client/src/templates/Challenges/odin/show.tsx b/client/src/templates/Challenges/odin/show.tsx deleted file mode 100644 index eb4e2d0624a..00000000000 --- a/client/src/templates/Challenges/odin/show.tsx +++ /dev/null @@ -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 { - static displayName: string; - private container: React.RefObject = 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, - 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 ( - this.setIsScenePlaying(true)} - > - - - - - {videoId && ( - - - - - )} - - - - - {title} - - - - - - {scene && ( - - )} - - - {instructions && ( - - )} - - - {assignments.length > 0 && ( - - )} - - - - - {explanation ? ( - - ) : ( - - )} - - - - - - - - - - - - - - ); - } -} - -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 - } - } - } -`; diff --git a/client/src/templates/Challenges/video/show.tsx b/client/src/templates/Challenges/video/show.tsx index 97512336a01..0165e47051f 100644 --- a/client/src/templates/Challenges/video/show.tsx +++ b/client/src/templates/Challenges/video/show.tsx @@ -32,9 +32,6 @@ import { } from '../redux/actions'; import { isChallengeCompletedSelector } from '../redux/selectors'; -// Styles -import '../video.css'; - // Redux Setup const mapStateToProps = createSelector( isChallengeCompletedSelector, diff --git a/client/utils/gatsby/challenge-page-creator.js b/client/utils/gatsby/challenge-page-creator.js index 55c339ed8ce..cf43e0093db 100644 --- a/client/utils/gatsby/challenge-page-creator.js +++ b/client/utils/gatsby/challenge-page-creator.js @@ -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, diff --git a/shared/config/challenge-types.ts b/shared/config/challenge-types.ts index 98ae8b921b0..110ecfe915b 100644 --- a/shared/config/challenge-types.ts +++ b/shared/config/challenge-types.ts @@ -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'