feat(client): render all mcq's and add first lecture placeholder (#56582)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Tom
2024-10-15 10:24:38 -05:00
committed by GitHub
parent 815f0291b6
commit b449a7de7d
12 changed files with 650 additions and 153 deletions

View File

@@ -0,0 +1,9 @@
---
title: Introduction to What is HTML?
block: lecture-what-is-html
superBlock: front-end-development
---
## Introduction to What is HTML?
Learn what HTML is in these lecture videos.

View File

@@ -7,74 +7,109 @@ import ChallengeHeading from './challenge-heading';
import PrismFormatted from './prism-formatted';
type MultipleChoiceQuestionsProps = {
questions: Question;
selectedOption: number | null;
isWrongAnswer: boolean;
handleOptionChange: (
changeEvent: React.ChangeEvent<HTMLInputElement>
) => void;
questions: Question[];
selectedOptions: (number | null)[];
handleOptionChange: (questionIndex: number, answerIndex: number) => void;
submittedMcqAnswers: (number | null)[];
showFeedback: boolean;
};
function removeParagraphTags(text: string): string {
return text.replace(/^<p>|<\/p>$/g, '');
}
function MultipleChoiceQuestions({
questions: { text, answers },
selectedOption,
isWrongAnswer,
handleOptionChange
questions,
selectedOptions,
handleOptionChange,
submittedMcqAnswers,
showFeedback
}: MultipleChoiceQuestionsProps): JSX.Element {
const { t } = useTranslation();
const feedback =
selectedOption !== null ? answers[selectedOption].feedback : undefined;
return (
<>
<ChallengeHeading heading={t('learn.question')} />
<PrismFormatted className={'line-numbers'} text={text} />
<div className='video-quiz-options'>
{answers.map(({ answer }, index) => (
<label
className='video-quiz-option-label'
key={index}
htmlFor={`mc-question-${index}`}
>
<input
name='quiz'
checked={selectedOption === index}
className='sr-only'
onChange={handleOptionChange}
type='radio'
value={index}
id={`mc-question-${index}`}
/>{' '}
<span className='video-quiz-input-visible'>
{selectedOption === index ? (
<span className='video-quiz-selected-input' />
) : null}
</span>
<PrismFormatted
className={'video-quiz-option'}
text={answer.replace(/^<p>|<\/p>$/g, '')}
useSpan
noAria
/>
</label>
))}
</div>
{isWrongAnswer && (
<>
<Spacer size='medium' />
<div className='text-center'>
{feedback ? (
<PrismFormatted
className={'multiple-choice-feedback'}
text={feedback}
/>
) : (
t('learn.wrong-answer')
)}
<ChallengeHeading
heading={
questions.length > 1 ? t('learn.questions') : t('learn.question')
}
/>
{questions.map((question, questionIndex) => (
<div key={questionIndex}>
<PrismFormatted className={'line-numbers'} text={question.text} />
<div className='video-quiz-options'>
{question.answers.map(({ answer }, answerIndex) => {
const isSubmittedAnswer =
submittedMcqAnswers[questionIndex] === answerIndex;
const feedback =
questions[questionIndex].answers[answerIndex].feedback;
const isCorrect =
submittedMcqAnswers[questionIndex] ===
// -1 because the solution is 1-indexed
questions[questionIndex].solution - 1;
return (
<React.Fragment key={answerIndex}>
<label
className={`video-quiz-option-label
${showFeedback && isSubmittedAnswer ? 'mcq-hide-border' : ''}
${showFeedback && isSubmittedAnswer ? (isCorrect ? 'mcq-correct-border' : 'mcq-incorrect-border') : ''}`}
htmlFor={`mc-question-${questionIndex}-answer-${answerIndex}`}
>
<input
name='quiz'
checked={selectedOptions[questionIndex] === answerIndex}
className='sr-only'
onChange={() =>
handleOptionChange(questionIndex, answerIndex)
}
type='radio'
value={answerIndex}
id={`mc-question-${questionIndex}-answer-${answerIndex}`}
/>{' '}
<span className='video-quiz-input-visible'>
{selectedOptions[questionIndex] === answerIndex ? (
<span className='video-quiz-selected-input' />
) : null}
</span>
<PrismFormatted
className={'video-quiz-option'}
text={removeParagraphTags(answer)}
useSpan
noAria
/>
</label>
{showFeedback && isSubmittedAnswer && (
<div
className={`video-quiz-option-label mcq-feedback ${isCorrect ? 'mcq-correct' : 'mcq-incorrect'}`}
>
{isCorrect
? t('learn.quiz.correct-answer')
: t('learn.quiz.incorrect-answer')}
{feedback && (
<>
<span>&nbsp;</span>
<PrismFormatted
className={
isCorrect
? 'mcq-prism-correct'
: 'mcq-prism-incorrect'
}
text={removeParagraphTags(feedback)}
useSpan
noAria
/>
</>
)}
</div>
)}
</React.Fragment>
);
})}
</div>
</>
)}
<Spacer size='medium' />
</div>
))}
<Spacer size='medium' />
</>
);

View File

@@ -9,6 +9,7 @@ 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 } from '@freecodecamp/ui';
import ShortcutsModal from '../components/shortcuts-modal';
@@ -78,9 +79,9 @@ interface ShowOdinProps {
interface ShowOdinState {
subtitles: string;
downloadURL: string | null;
selectedOption: number | null;
answer: number;
isWrongAnswer: boolean;
selectedMcqOptions: (number | null)[];
submittedMcqAnswers: (number | null)[];
showFeedback: boolean;
assignmentsCompleted: number;
allAssignmentsCompleted: boolean;
videoIsLoaded: boolean;
@@ -94,14 +95,23 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
constructor(props: ShowOdinProps) {
super(props);
const {
data: {
challengeNode: {
challenge: { assignments, questions }
}
}
} = this.props;
this.state = {
subtitles: '',
downloadURL: null,
selectedOption: null,
answer: 1,
isWrongAnswer: false,
selectedMcqOptions: questions.map(() => null),
submittedMcqAnswers: questions.map(() => null),
showFeedback: false,
assignmentsCompleted: 0,
allAssignmentsCompleted: false,
allAssignmentsCompleted: assignments.length == 0,
videoIsLoaded: false,
isScenePlaying: false
};
@@ -166,34 +176,43 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
}
}
handleSubmit(
solution: number,
openCompletionModal: () => void,
assignments: string[]
) {
const hasAssignments = assignments.length > 0;
const completed = this.state.allAssignmentsCompleted;
const isCorrect = solution - 1 === this.state.selectedOption;
handleSubmit = () => {
const {
data: {
challengeNode: {
challenge: { questions }
}
},
openCompletionModal
} = this.props;
if (isCorrect) {
this.setState({
isWrongAnswer: false
});
if (!hasAssignments || completed) openCompletionModal();
} else {
this.setState({
isWrongAnswer: true
});
}
}
// subract 1 because the solutions are 1-indexed
const mcqSolutions = questions.map(question => question.solution - 1);
handleOptionChange = (
changeEvent: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
isWrongAnswer: false,
selectedOption: parseInt(changeEvent.target.value, 10)
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 = (
@@ -245,7 +264,6 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
}
}
},
openCompletionModal,
openHelpModal,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
@@ -254,18 +272,13 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
isChallengeCompleted
} = this.props;
const question = questions[0];
const { solution } = question;
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
return (
<Hotkeys
executeChallenge={() => {
this.handleSubmit(solution, openCompletionModal, assignments);
}}
executeChallenge={this.handleSubmit}
containerRef={this.container}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
@@ -331,10 +344,11 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
)}
<MultipleChoiceQuestions
questions={question}
selectedOption={this.state.selectedOption}
isWrongAnswer={this.state.isWrongAnswer}
handleOptionChange={this.handleOptionChange}
questions={questions}
selectedOptions={this.state.selectedMcqOptions}
handleOptionChange={this.handleMcqOptionChange}
submittedMcqAnswers={this.state.submittedMcqAnswers}
showFeedback={this.state.showFeedback}
/>
</ObserveKeys>
@@ -348,13 +362,7 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
block={true}
size='medium'
variant='primary'
onClick={() =>
this.handleSubmit(
solution,
openCompletionModal,
assignments
)
}
onClick={this.handleSubmit}
>
{t('buttons.check-answer')}
</Button>

View File

@@ -115,6 +115,36 @@ input:focus-visible + .video-quiz-input-visible {
background: none;
}
.multiple-choice-feedback p {
margin-bottom: 0;
.mcq-correct-border {
border-left-color: var(--success-background);
}
.mcq-incorrect-border {
border-left-color: var(--danger-background);
}
.mcq-correct {
color: var(--success-color);
border-left-color: var(--success-background);
}
.mcq-incorrect {
color: var(--danger-color);
border-left-color: var(--danger-background);
}
.mcq-hide-border {
border-bottom: none;
}
.mcq-feedback {
border-top: none;
}
.mcq-prism-correct code {
color: var(--success-color);
}
.mcq-prism-incorrect code {
color: var(--danger-color);
}

View File

@@ -9,6 +9,7 @@ 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 } from '@freecodecamp/ui';
// Local Utilities
@@ -75,9 +76,9 @@ interface ShowVideoProps {
interface ShowVideoState {
subtitles: string;
downloadURL: string | null;
selectedOption: number | null;
answer: number;
showWrong: boolean;
selectedMcqOptions: (number | null)[];
submittedMcqAnswers: (number | null)[];
showFeedback: boolean;
videoIsLoaded: boolean;
}
@@ -88,16 +89,23 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
constructor(props: ShowVideoProps) {
super(props);
const {
data: {
challengeNode: {
challenge: { questions }
}
}
} = this.props;
this.state = {
subtitles: '',
downloadURL: null,
selectedOption: null,
answer: 1,
showWrong: false,
selectedMcqOptions: questions.map(() => null),
submittedMcqAnswers: questions.map(() => null),
showFeedback: false,
videoIsLoaded: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount(): void {
@@ -157,26 +165,43 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
}
}
handleSubmit(solution: number, openCompletionModal: () => void) {
if (solution - 1 === this.state.selectedOption) {
this.setState({
showWrong: false
});
openCompletionModal();
} else {
this.setState({
showWrong: true
});
}
}
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);
handleOptionChange = (
changeEvent: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
showWrong: false,
selectedOption: parseInt(changeEvent.target.value, 10)
submittedMcqAnswers: this.state.selectedMcqOptions,
showFeedback: true
});
const allMcqAnswersCorrect = isEqual(
mcqSolutions,
this.state.selectedMcqOptions
);
if (allMcqAnswersCorrect) {
openCompletionModal();
}
};
handleMcqOptionChange = (
questionIndex: number,
answerIndex: number
): void => {
this.setState(state => ({
selectedMcqOptions: state.selectedMcqOptions.map((option, index) =>
index === questionIndex ? answerIndex : option
)
}));
};
onVideoLoad = () => {
@@ -204,7 +229,6 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
}
}
},
openCompletionModal,
openHelpModal,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
@@ -213,18 +237,13 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
isChallengeCompleted
} = this.props;
const question = questions[0];
const { solution } = question;
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
return (
<Hotkeys
executeChallenge={() => {
this.handleSubmit(solution, openCompletionModal);
}}
executeChallenge={this.handleSubmit}
containerRef={this.container}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
@@ -260,19 +279,18 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
<ChallengeDescription description={description} />
<ObserveKeys>
<MultipleChoiceQuestions
questions={question}
selectedOption={this.state.selectedOption}
isWrongAnswer={this.state.showWrong}
handleOptionChange={this.handleOptionChange}
questions={questions}
selectedOptions={this.state.selectedMcqOptions}
handleOptionChange={this.handleMcqOptionChange}
submittedMcqAnswers={this.state.submittedMcqAnswers}
showFeedback={this.state.showFeedback}
/>
</ObserveKeys>
<Spacer size='medium' />
<Button
block={true}
variant='primary'
onClick={() =>
this.handleSubmit(solution, openCompletionModal)
}
onClick={this.handleSubmit}
>
{t('buttons.check-answer')}
</Button>