1- import React from 'react' ;
1+ import React , { useState } from 'react' ;
2+ import { connect } from 'react-redux' ;
23import { useTranslation } from 'react-i18next' ;
4+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
5+ import { faMicrophone } from '@fortawesome/free-solid-svg-icons' ;
36
4- import { Spacer } from '@freecodecamp/ui' ;
7+ import { Button , Spacer } from '@freecodecamp/ui' ;
58import { Question } from '../../../redux/prop-types' ;
9+ import { openModal } from '../redux/actions' ;
10+ import { SuperBlocks } from '../../../../../shared/config/curriculum' ;
11+ import SpeakingModal from './speaking-modal' ;
612import ChallengeHeading from './challenge-heading' ;
713import PrismFormatted from './prism-formatted' ;
814
@@ -12,6 +18,8 @@ type MultipleChoiceQuestionsProps = {
1218 handleOptionChange : ( questionIndex : number , answerIndex : number ) => void ;
1319 submittedMcqAnswers : ( number | null ) [ ] ;
1420 showFeedback : boolean ;
21+ openSpeakingModal : ( ) => void ;
22+ superBlock : SuperBlocks ;
1523} ;
1624
1725function removeParagraphTags ( text : string ) : string {
@@ -23,10 +31,45 @@ function MultipleChoiceQuestions({
2331 selectedOptions,
2432 handleOptionChange,
2533 submittedMcqAnswers,
26- showFeedback
34+ showFeedback,
35+ openSpeakingModal,
36+ superBlock
2737} : MultipleChoiceQuestionsProps ) : JSX . Element {
2838 const { t } = useTranslation ( ) ;
2939
40+ const [ modalText , setModalText ] = useState ( '' ) ;
41+ const [ modalAnswerIndex , setModalAnswerIndex ] = useState < number > ( 0 ) ;
42+ const [ modalQuestionIndex , setModalQuestionIndex ] = useState < number > ( 0 ) ;
43+
44+ function stripCodeTags ( text : string ) : string {
45+ return text . replace ( / < c o d e > ( .* ?) < \/ c o d e > / g, '$1' ) ;
46+ }
47+
48+ const handleSpeakingButtonClick = (
49+ answer : string ,
50+ answerIndex : number ,
51+ questionIndex : number
52+ ) => {
53+ setModalText ( stripCodeTags ( removeParagraphTags ( answer ) ) ) ;
54+ setModalAnswerIndex ( answerIndex ) ;
55+ setModalQuestionIndex ( questionIndex ) ;
56+ openSpeakingModal ( ) ;
57+ } ;
58+
59+ const constructAudioUrl = ( audioId ?: string ) : string | undefined =>
60+ audioId
61+ ? `https://cdn.freecodecamp.org/curriculum/english/animation-assets/sounds/${ audioId } `
62+ : undefined ;
63+
64+ const getAudioUrl = (
65+ questionIndex : number ,
66+ answerIndex : number
67+ ) : string | undefined => {
68+ const answer = questions [ questionIndex ] ?. answers [ answerIndex ] ;
69+ const audioId = answer ?. audioId ?? undefined ;
70+ return constructAudioUrl ( audioId ) ;
71+ } ;
72+
3073 return (
3174 < >
3275 < ChallengeHeading
@@ -50,74 +93,116 @@ function MultipleChoiceQuestions({
5093 // -1 because the solution is 1-indexed
5194 questions [ questionIndex ] . solution - 1 ;
5295
96+ const labelId = `mc-question-${ questionIndex } -answer-${ answerIndex } -label` ;
97+ const hasAudio =
98+ questions [ questionIndex ] ?. answers [ answerIndex ] ?. audioId ;
99+
53100 return (
54- < React . Fragment key = { answerIndex } >
55- < label
56- className = { `video-quiz- option-label
57- ${ showFeedback && isSubmittedAnswer ? 'mcq-hide-border' : '' }
58- ${ showFeedback && isSubmittedAnswer ? ( isCorrect ? 'mcq-correct-border' : 'mcq-incorrect-border' ) : '' } ` }
59- htmlFor = { `mc-question- ${ questionIndex } -answer- ${ answerIndex } ` }
60- >
61- < input
62- name = { `mc-question-${ questionIndex } ` }
63- checked = { selectedOptions [ questionIndex ] === answerIndex }
64- className = 'sr-only'
65- onChange = { ( ) =>
66- handleOptionChange ( questionIndex , answerIndex )
67- }
68- type = 'radio'
69- value = { answerIndex }
70- id = { `mc-question- ${ questionIndex } -answer- ${ answerIndex } ` }
71- /> { ' ' }
72- < span className = 'video-quiz-input-visible' >
73- { selectedOptions [ questionIndex ] === answerIndex ? (
74- < span className = 'video-quiz-selected-input' />
75- ) : null }
76- </ span >
77- < PrismFormatted
78- className = { 'video-quiz-option' }
79- text = { removeParagraphTags ( answer ) }
80- useSpan
81- noAria
82- />
83- </ label >
84- { showFeedback && isSubmittedAnswer && (
85- < div
86- className = { `video-quiz-option-label mcq-feedback ${ isCorrect ? 'mcq-correct' : 'mcq-incorrect' } ` }
87- >
88- < p >
89- { isCorrect
90- ? t ( 'learn.quiz.correct-answer' )
91- : t ( 'learn.quiz.incorrect-answer' ) }
92- </ p >
93- { feedback && (
101+ < div key = { answerIndex } className = 'mcq-option-row' >
102+ < div className = 'mcq-option-with-feedback' >
103+ < div className = 'mcq- option-content' >
104+ < label
105+ id = { labelId }
106+ className = { `video-quiz-option-label mcq-option-label
107+ ${ showFeedback && isSubmittedAnswer ? 'mcq-hide-border' : '' }
108+ ${ showFeedback && isSubmittedAnswer ? ( isCorrect ? 'mcq-correct-border' : 'mcq-incorrect-border' ) : '' } ` }
109+ htmlFor = { `mc-question-${ questionIndex } -answer- ${ answerIndex } ` }
110+ >
111+ < input
112+ name = { `mc-question- ${ questionIndex } ` }
113+ checked = {
114+ selectedOptions [ questionIndex ] === answerIndex
115+ }
116+ className = 'sr-only'
117+ onChange = { ( ) =>
118+ handleOptionChange ( questionIndex , answerIndex )
119+ }
120+ type = 'radio'
121+ value = { answerIndex }
122+ id = { `mc-question- ${ questionIndex } -answer- ${ answerIndex } ` }
123+ /> { ' ' }
124+ < span className = 'video-quiz-input-visible' >
125+ { selectedOptions [ questionIndex ] === answerIndex ? (
126+ < span className = 'video-quiz-selected-input' />
127+ ) : null }
128+ </ span >
129+ < PrismFormatted
130+ className = { 'video-quiz-option' }
131+ text = { removeParagraphTags ( answer ) }
132+ useSpan
133+ noAria
134+ / >
135+ </ label >
136+ </ div >
137+ { showFeedback && isSubmittedAnswer && (
138+ < div
139+ className = { `video-quiz-option-label mcq-feedback ${ isCorrect ? 'mcq-correct' : 'mcq-incorrect' } ` }
140+ >
94141 < p >
95- < PrismFormatted
96- className = {
97- isCorrect
98- ? 'mcq-prism-correct'
99- : 'mcq-prism-incorrect'
100- }
101- text = { removeParagraphTags ( feedback ) }
102- useSpan
103- noAria
104- />
142+ { isCorrect
143+ ? t ( 'learn.quiz.correct-answer' )
144+ : t ( 'learn.quiz.incorrect-answer' ) }
105145 </ p >
106- ) }
146+ { feedback && (
147+ < p >
148+ < PrismFormatted
149+ className = {
150+ isCorrect
151+ ? 'mcq-prism-correct'
152+ : 'mcq-prism-incorrect'
153+ }
154+ text = { removeParagraphTags ( feedback ) }
155+ useSpan
156+ noAria
157+ />
158+ </ p >
159+ ) }
160+ </ div >
161+ ) }
162+ </ div >
163+
164+ { hasAudio && (
165+ < div className = 'mcq-speaking-button-wrapper' >
166+ < Button
167+ size = 'medium'
168+ onClick = { ( ) =>
169+ handleSpeakingButtonClick (
170+ answer ,
171+ answerIndex ,
172+ questionIndex
173+ )
174+ }
175+ className = 'mcq-speaking-button'
176+ aria-describedby = { labelId }
177+ aria-label = { t ( 'speaking-modal.speaking-button' ) }
178+ >
179+ < FontAwesomeIcon icon = { faMicrophone } />
180+ </ Button >
107181 </ div >
108182 ) }
109- </ React . Fragment >
183+ </ div >
110184 ) ;
111185 } ) }
112186 </ div >
113187 < Spacer size = 'm' />
114188 </ fieldset >
115189 ) ) }
190+
116191 < Spacer size = 'm' />
192+ < SpeakingModal
193+ sentence = { modalText }
194+ audioUrl = { getAudioUrl ( modalQuestionIndex , modalAnswerIndex ) }
195+ answerIndex = { modalAnswerIndex }
196+ superBlock = { superBlock }
197+ />
117198 </ >
118199 ) ;
119200}
120201
202+ const mapDispatchToProps = {
203+ openSpeakingModal : ( ) => openModal ( 'speaking' )
204+ } ;
205+
121206MultipleChoiceQuestions . displayName = 'MultipleChoiceQuestions' ;
122207
123- export default MultipleChoiceQuestions ;
208+ export default connect ( null , mapDispatchToProps ) ( MultipleChoiceQuestions ) ;
0 commit comments