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

@@ -1697,7 +1697,10 @@
"intro": ["placeholder"],
"blocks": {
"efpl": { "title": "0", "intro": [] },
"xpmy": { "title": "1", "intro": [] },
"lecture-what-is-html": {
"title": "What is HTML?",
"intro": ["Learn what HTML is in these lecture videos."]
},
"workshop-cat-photo-app": {
"title": "Build a Cat Photo App",
"intro": [

View File

@@ -390,6 +390,7 @@
"assignment-not-complete": "Please complete the assignments",
"assignments": "Assignments",
"question": "Question",
"questions": "Questions",
"explanation": "Explanation",
"solution-link": "Solution Link",
"source-code-link": "Source Code Link",
@@ -491,8 +492,8 @@
"fill-in-the-blank": "Fill in the blank",
"blank": "blank",
"quiz": {
"correct-answer": "Correct",
"incorrect-answer": "Incorrect",
"correct-answer": "Correct!",
"incorrect-answer": "Incorrect.",
"unanswered-questions": "The following questions are unanswered: {{ unansweredQuestions }}. You must answer all questions.",
"have-n-correct-questions": "You have {{ correctAnswerCount }} out of {{ total }} questions correct."
},

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>

View File

@@ -0,0 +1,10 @@
{
"name": "What is HTML?",
"isUpcomingChange": true,
"dashedName": "lecture-what-is-html",
"order": 1,
"superBlock": "front-end-development",
"challengeOrder": [{ "id": "66f6db08d55022680a3cfbc9", "title": "What is HTML and what role does it play on the web?" }],
"helpCategory": "HTML-CSS",
"blockType": "lecture"
}

View File

@@ -10,5 +10,13 @@
{
"id": "64afc37bf3b37856e035b85e",
"title": "Upcoming Python Project"
}
},
{
"id": "6703d8f42ebd112db6f7788a",
"title": "Video Layout"
},
{
"id": "6703d9382ebd112db6f7788b",
"title": "Odin Layout"
}
]}

View File

@@ -0,0 +1,121 @@
---
id: 6703d8f42ebd112db6f7788a
title: Video Layout
challengeType: 11
videoId: nVAaxZ34khk
dashedName: video-layout
---
# --description--
Watch the video and answer the questions below.
# --questions--
## --text--
Question 1?
## --answers--
`answer 1`
### --feedback--
Feedback.
---
`answer 2`
### --feedback--
Feedback.
---
`answer 3`
### --feedback--
Feedback.
---
`answer 4`
## --video-solution--
4
## --text--
Question 2?
## --answers--
`answer 1`
### --feedback--
Feedback.
---
`answer 2`
### --feedback--
Feedback.
---
`answer 3`
---
`answer 4`
### --feedback--
Feedback.
## --video-solution--
3
## --text--
Question 3?
## --answers--
`answer 1`
---
`answer 2`
### --feedback--
Feedback.
---
`answer 3`
### --feedback--
Feedback.
---
`answer 4`
### --feedback--
Feedback.
## --video-solution--
1

View File

@@ -0,0 +1,133 @@
---
id: 6703d9382ebd112db6f7788b
title: Odin Layout
challengeType: 19
videoId: nVAaxZ34khk
dashedName: odin-layout
---
# --description--
Watch the video and answer the questions below.
# --assignment--
assignment 1
---
assignemnt 2
# --questions--
## --text--
Question 1?
## --answers--
`answer 1`
### --feedback--
Here's some `feedback`.
---
`answer 2`
### --feedback--
More JS feedback:
```js
console.log('incorrect');
```
---
`answer 3`
### --feedback--
Feedback.
---
`answer 4`
## --video-solution--
4
## --text--
Question 2?
## --answers--
`answer 1`
### --feedback--
Feedback.
---
`answer 2`
### --feedback--
Feedback.
---
`answer 3`
---
`answer 4`
### --feedback--
Feedback.
## --video-solution--
3
## --text--
Question 3?
## --answers--
`answer 1`
---
`answer 2`
### --feedback--
Feedback.
---
`answer 3`
### --feedback--
Feedback.
---
`answer 4`
### --feedback--
Feedback.
## --video-solution--
1

View File

@@ -0,0 +1,121 @@
---
id: 66f6db08d55022680a3cfbc9
title: What Is HTML and What Role Does It Play on the Web?
challengeType: 11
videoId: nVAaxZ34khk
dashedName: what-is-html-and-what-role-does-it-play-on-the-web
---
# --description--
Watch the video and answer the questions below.
# --questions--
## --text--
What does HTML stand for?
## --answers--
HyperText Maker Language
### --feedback--
Focus on the term for describing the structure and presentation of web pages.
---
HyperText Marker Language
### --feedback--
Focus on the term for describing the structure and presentation of web pages.
---
HyperText Markdown Language
### --feedback--
Focus on the term for describing the structure and presentation of web pages.
---
HyperText Markup Language
## --video-solution--
4
## --text--
Which of the following is the correct syntax for a closing tag?
## --answers--
`<;p>`
### --feedback--
Think about the additional symbol for defining tags apart from left-angle and right-angle brackets.
---
`<p>`
### --feedback--
Think about the additional symbol for defining tags apart from left-angle and right-angle brackets.
---
`</p>`
---
`<///p/>`
### --feedback--
Think about the additional symbol for defining tags apart from left-angle and right-angle brackets.
## --video-solution--
3
## --text--
Which of the following is a valid attribute used inside image elements?
## --answers--
`src`
---
`bold`
### --feedback--
Consider what you often use inside an opening tag to supply additional information to the element.
---
`closing`
### --feedback--
Consider what you often use inside an opening tag to supply additional information to the element.
---
`div`
### --feedback--
Consider what you often use inside an opening tag to supply additional information to the element.
## --video-solution--
1