refactor(client/challenge views): extract items into sharable components (#55946)
parent
8d0550d76c
commit
3074bb199d
|
@ -385,7 +385,7 @@
|
|||
"add-subtitles": "Help improve or add subtitles",
|
||||
"wrong-answer": "Sorry, that's not the right answer. Give it another try?",
|
||||
"check-answer": "Click the button below to check your answer.",
|
||||
"assignment-not-complete": "Please finish the assignments",
|
||||
"assignment-not-complete": "Please complete the assignments",
|
||||
"assignments": "Assignments",
|
||||
"question": "Question",
|
||||
"solution-link": "Solution Link",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.assignments-not-complete {
|
||||
text-align: center;
|
||||
color: var(--danger-color);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import ChallengeHeading from './challenge-heading';
|
||||
import PrismFormatted from './prism-formatted';
|
||||
|
||||
import './assignments.css';
|
||||
|
||||
type AssignmentsProps = {
|
||||
assignments: string[];
|
||||
allAssignmentsCompleted: boolean;
|
||||
handleAssignmentChange: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
totalAssignments: number
|
||||
) => void;
|
||||
};
|
||||
|
||||
function Assignments({
|
||||
assignments,
|
||||
allAssignmentsCompleted,
|
||||
handleAssignmentChange
|
||||
}: AssignmentsProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<ChallengeHeading heading={t('learn.assignments')} />
|
||||
<div className='video-quiz-options'>
|
||||
{assignments.map((assignment, index) => (
|
||||
<label className='video-quiz-option-label' key={index}>
|
||||
<input
|
||||
name='assignment'
|
||||
type='checkbox'
|
||||
onChange={event =>
|
||||
handleAssignmentChange(event, assignments.length)
|
||||
}
|
||||
/>
|
||||
<PrismFormatted className={'video-quiz-option'} text={assignment} />
|
||||
<Spacer size='medium' />
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{!allAssignmentsCompleted && (
|
||||
<>
|
||||
<Spacer size='medium' />
|
||||
<div className='assignments-not-complete'>
|
||||
{t('learn.assignment-not-complete')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Assignments.displayName = 'Assignments';
|
||||
|
||||
export default Assignments;
|
|
@ -0,0 +1,87 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { parseBlanks } from '../fill-in-the-blank/parse-blanks';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import { FillInTheBlank } from '../../../redux/prop-types';
|
||||
import ChallengeHeading from './challenge-heading';
|
||||
|
||||
type FillInTheBlankProps = {
|
||||
fillInTheBlank: FillInTheBlank;
|
||||
answersCorrect: (boolean | null)[];
|
||||
showFeedback: boolean;
|
||||
feedback: string | null;
|
||||
showWrong: boolean;
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
function FillInTheBlanks({
|
||||
fillInTheBlank: { sentence, blanks },
|
||||
answersCorrect,
|
||||
showFeedback,
|
||||
feedback,
|
||||
showWrong,
|
||||
handleInputChange
|
||||
}: FillInTheBlankProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const addInputClass = (index: number): string => {
|
||||
if (answersCorrect[index] === true) return 'green-underline';
|
||||
if (answersCorrect[index] === false) return 'red-underline';
|
||||
return '';
|
||||
};
|
||||
|
||||
const paragraphs = parseBlanks(sentence);
|
||||
const blankAnswers = blanks.map(b => b.answer);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChallengeHeading heading={t('learn.fill-in-the-blank')} />
|
||||
<Spacer size='small' />
|
||||
<div className='fill-in-the-blank-wrap'>
|
||||
{paragraphs.map((p, i) => {
|
||||
return (
|
||||
// both keys, i and j, are stable between renders, since
|
||||
// the paragraphs are static.
|
||||
<p key={i}>
|
||||
{p.map((node, j) => {
|
||||
const { type, value } = node;
|
||||
if (type === 'text') return value;
|
||||
if (type === 'blank')
|
||||
return (
|
||||
<input
|
||||
key={j}
|
||||
type='text'
|
||||
maxLength={blankAnswers[value].length + 3}
|
||||
className={`fill-in-the-blank-input ${addInputClass(
|
||||
value
|
||||
)}`}
|
||||
onChange={handleInputChange}
|
||||
data-index={node.value}
|
||||
size={blankAnswers[value].length}
|
||||
aria-label={t('learn.blank')}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
{showFeedback && feedback && (
|
||||
<>
|
||||
<PrismFormatted text={feedback} />
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
)}
|
||||
<div className='text-center'>
|
||||
{showWrong && <span>{t('learn.wrong-answer')}</span>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
FillInTheBlanks.displayName = 'FillInTheBlanks';
|
||||
|
||||
export default FillInTheBlanks;
|
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Question } from '../../../redux/prop-types';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
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;
|
||||
};
|
||||
|
||||
function MultipleChoiceQuestions({
|
||||
questions: { text, answers },
|
||||
selectedOption,
|
||||
isWrongAnswer,
|
||||
handleOptionChange
|
||||
}: 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')
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
MultipleChoiceQuestions.displayName = 'MultipleChoiceQuestions';
|
||||
|
||||
export default MultipleChoiceQuestions;
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react'; //, ReactElement } f
|
|||
import { Col } from '@freecodecamp/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FullScene } from '../../../../redux/prop-types';
|
||||
import { Loader } from '../../../../components/helpers';
|
||||
import { Loader, Spacer } from '../../../../components/helpers';
|
||||
import ClosedCaptionsIcon from '../../../../assets/icons/closedcaptions';
|
||||
import { sounds, images, backgrounds, characterAssets } from './scene-assets';
|
||||
import Character from './character';
|
||||
|
@ -283,6 +283,7 @@ export function Scene({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
import YouTube from 'react-youtube';
|
||||
|
||||
import Loader from '../../../components/helpers/loader';
|
||||
import envData from '../../../../config/env.json';
|
||||
import type { BilibiliIds, VideoLocaleIds } from '../../../redux/prop-types';
|
||||
|
||||
|
@ -47,7 +49,13 @@ function VideoPlayer({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='video-wrapper'>
|
||||
{!videoIsLoaded ? (
|
||||
<div className='video-placeholder-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{bilibiliSrc ? (
|
||||
<iframe
|
||||
frameBorder='no'
|
||||
|
@ -71,7 +79,7 @@ function VideoPlayer({
|
|||
videoId={videoId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
|||
import Hotkeys from '../components/hotkeys';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import ChallengeHeading from '../components/challenge-heading';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import {
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import Scene from '../components/scene/scene';
|
||||
import Assignments from '../components/assignments';
|
||||
|
||||
// Styles
|
||||
import '../odin/show.css';
|
||||
|
@ -256,45 +256,12 @@ class ShowDialogue extends Component<ShowDialogueProps, ShowDialogueState> {
|
|||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='medium' />
|
||||
<ObserveKeys>
|
||||
<ChallengeHeading heading={t('learn.assignments')} />
|
||||
<div className='video-quiz-options'>
|
||||
{assignments.map((assignment, index) => (
|
||||
<label className='video-quiz-option-label' key={index}>
|
||||
<input
|
||||
name='assignment'
|
||||
type='checkbox'
|
||||
onChange={event =>
|
||||
this.handleAssignmentChange(
|
||||
event,
|
||||
assignments.length
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<PrismFormatted
|
||||
className={'video-quiz-option'}
|
||||
text={assignment}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
<Assignments
|
||||
assignments={assignments}
|
||||
allAssignmentsCompleted={this.state.allAssignmentsCompleted}
|
||||
handleAssignmentChange={this.handleAssignmentChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{!this.state.allAssignmentsCompleted &&
|
||||
assignments.length > 0 && (
|
||||
<>
|
||||
<br />
|
||||
<span>{t('learn.assignment-not-complete')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
<Button
|
||||
block={true}
|
||||
|
|
|
@ -36,25 +36,6 @@
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
.code-tag code {
|
||||
font-size: 100%;
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.first-code-tag code {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.middle-code-tag code {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.last-code-tag code {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.green-underline {
|
||||
border-bottom-color: var(--success-background) !important;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Package Utilities
|
||||
import { graphql } from 'gatsby';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { ObserveKeys } from 'react-hotkeys';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
@ -18,9 +18,9 @@ import LearnLayout from '../../../components/layouts/learn';
|
|||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import ChallengeHeading from '../components/challenge-heading';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import FillInTheBlanks from '../components/fill-in-the-blanks';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import {
|
||||
challengeMounted,
|
||||
|
@ -31,7 +31,6 @@ import {
|
|||
} from '../redux/actions';
|
||||
import Scene from '../components/scene/scene';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import { parseBlanks } from './parse-blanks';
|
||||
|
||||
// Styles
|
||||
import '../video.css';
|
||||
|
@ -237,25 +236,6 @@ class ShowFillInTheBlank extends Component<
|
|||
});
|
||||
};
|
||||
|
||||
addCodeTags(str: string, index: number, numberOfBlanks: number): string {
|
||||
if (index === 0) return `${str}</code>`;
|
||||
if (index < numberOfBlanks) return `<code>${str}</code>`;
|
||||
return `<code>${str}`;
|
||||
}
|
||||
|
||||
addPrismClass(index: number, numberOfBlanks: number): string {
|
||||
if (index === 0) return `first-code-tag`;
|
||||
if (index < numberOfBlanks) return `middle-code-tag`;
|
||||
return `last-code-tag`;
|
||||
}
|
||||
|
||||
addInputClass(index: number): string {
|
||||
const { answersCorrect } = this.state;
|
||||
if (answersCorrect[index] === true) return 'green-underline';
|
||||
if (answersCorrect[index] === false) return 'red-underline';
|
||||
return '';
|
||||
}
|
||||
|
||||
setIsScenePlaying = (shouldPlay: boolean) => {
|
||||
this.setState({
|
||||
isScenePlaying: shouldPlay
|
||||
|
@ -274,7 +254,7 @@ class ShowFillInTheBlank extends Component<
|
|||
block,
|
||||
translationPending,
|
||||
fields: { blockName },
|
||||
fillInTheBlank: { sentence, blanks },
|
||||
fillInTheBlank,
|
||||
scene
|
||||
}
|
||||
}
|
||||
|
@ -292,9 +272,6 @@ class ShowFillInTheBlank extends Component<
|
|||
)} - ${title}`;
|
||||
|
||||
const { allBlanksFilled, feedback, showFeedback, showWrong } = this.state;
|
||||
const paragraphs = parseBlanks(sentence);
|
||||
|
||||
const blankAnswers = blanks.map(b => b.answer);
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
|
@ -324,78 +301,34 @@ class ShowFillInTheBlank extends Component<
|
|||
</Col>
|
||||
|
||||
{scene && (
|
||||
<>
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={this.state.isScenePlaying}
|
||||
setIsPlaying={this.setIsScenePlaying}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={this.state.isScenePlaying}
|
||||
setIsPlaying={this.setIsScenePlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ChallengeHeading heading={t('learn.fill-in-the-blank')} />
|
||||
<Spacer size='small' />
|
||||
{instructions && (
|
||||
<>
|
||||
<PrismFormatted text={instructions} />
|
||||
<Spacer size='small' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* what we want to observe is ctrl/cmd + enter, but ObserveKeys is buggy and throws an error
|
||||
if it encounters a key combination, so we have to pass in the individual keys to observe */}
|
||||
<ObserveKeys only={['ctrl', 'cmd', 'enter']}>
|
||||
<div className='fill-in-the-blank-wrap'>
|
||||
{paragraphs.map((p, i) => {
|
||||
return (
|
||||
// both keys, i and j, are stable between renders, since
|
||||
// the paragraphs are static.
|
||||
<p key={i}>
|
||||
{p.map((node, j) => {
|
||||
if (node.type === 'text') return node.value;
|
||||
if (node.type === 'blank')
|
||||
return (
|
||||
<input
|
||||
key={j}
|
||||
type='text'
|
||||
maxLength={
|
||||
blankAnswers[node.value].length + 3
|
||||
}
|
||||
className={`fill-in-the-blank-input ${this.addInputClass(
|
||||
node.value
|
||||
)}`}
|
||||
onChange={this.handleInputChange}
|
||||
data-index={node.value}
|
||||
size={blankAnswers[node.value].length}
|
||||
aria-label={t('learn.blank')}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<FillInTheBlanks
|
||||
fillInTheBlank={fillInTheBlank}
|
||||
answersCorrect={this.state.answersCorrect}
|
||||
showFeedback={showFeedback}
|
||||
feedback={feedback}
|
||||
showWrong={showWrong}
|
||||
handleInputChange={this.handleInputChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
{showFeedback && feedback && (
|
||||
<>
|
||||
<PrismFormatted text={feedback} />
|
||||
<Spacer size='small' />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{showWrong ? (
|
||||
<span>{t('learn.wrong-answer')}</span>
|
||||
) : (
|
||||
<span>{t('learn.check-answer')}</span>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
<Button
|
||||
block={true}
|
||||
variant='primary'
|
||||
|
|
|
@ -13,7 +13,6 @@ import { Container, Col, Row, Button } from '@freecodecamp/ui';
|
|||
import ShortcutsModal from '../components/shortcuts-modal';
|
||||
|
||||
// Local Utilities
|
||||
import Loader from '../../../components/helpers/loader';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
|
@ -24,7 +23,8 @@ 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 ChallengeHeading from '../components/challenge-heading';
|
||||
import MultipleChoiceQuestions from '../components/multiple-choice-questions';
|
||||
import Assignments from '../components/assignments';
|
||||
import {
|
||||
challengeMounted,
|
||||
updateChallengeMeta,
|
||||
|
@ -236,7 +236,7 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
|||
videoLocaleIds,
|
||||
bilibiliIds,
|
||||
fields: { blockName },
|
||||
question: { text, answers, solution },
|
||||
question,
|
||||
assignments,
|
||||
translationPending,
|
||||
scene
|
||||
|
@ -256,10 +256,7 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
|||
`intro:${superBlock}.blocks.${block}.title`
|
||||
)} - ${title}`;
|
||||
|
||||
const feedback =
|
||||
this.state.selectedOption !== null
|
||||
? answers[this.state.selectedOption].feedback
|
||||
: undefined;
|
||||
const { solution } = question;
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
|
@ -280,23 +277,17 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
|||
{videoId && (
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
<Spacer size='medium' />
|
||||
<div className='video-wrapper'>
|
||||
{!this.state.videoIsLoaded ? (
|
||||
<div className='video-placeholder-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
<VideoPlayer
|
||||
bilibiliIds={bilibiliIds}
|
||||
onVideoLoad={this.onVideoLoad}
|
||||
title={title}
|
||||
videoId={videoId}
|
||||
videoIsLoaded={this.state.videoIsLoaded}
|
||||
videoLocaleIds={videoLocaleIds}
|
||||
/>
|
||||
</div>
|
||||
<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='medium' />
|
||||
<ChallengeTitle
|
||||
|
@ -310,104 +301,33 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
|||
</Col>
|
||||
|
||||
{scene && (
|
||||
<>
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={this.state.isScenePlaying}
|
||||
setIsPlaying={this.setIsScenePlaying}
|
||||
/>{' '}
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={this.state.isScenePlaying}
|
||||
setIsPlaying={this.setIsScenePlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ObserveKeys>
|
||||
{assignments.length > 0 && (
|
||||
<>
|
||||
<ChallengeHeading heading={t('learn.assignments')} />
|
||||
<div className='video-quiz-options'>
|
||||
{assignments.map((assignment, index) => (
|
||||
<label
|
||||
className='video-quiz-option-label'
|
||||
key={index}
|
||||
>
|
||||
<input
|
||||
name='assignment'
|
||||
type='checkbox'
|
||||
onChange={event =>
|
||||
this.handleAssignmentChange(
|
||||
event,
|
||||
assignments.length
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<PrismFormatted
|
||||
className={'video-quiz-option'}
|
||||
text={assignment}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
</label>
|
||||
))}
|
||||
</div>{' '}
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
<Assignments
|
||||
assignments={assignments}
|
||||
allAssignmentsCompleted={
|
||||
this.state.allAssignmentsCompleted
|
||||
}
|
||||
handleAssignmentChange={this.handleAssignmentChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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}>
|
||||
<input
|
||||
aria-label={t('aria.answer')}
|
||||
checked={this.state.selectedOption === index}
|
||||
className='sr-only'
|
||||
name='quiz'
|
||||
onChange={this.handleOptionChange}
|
||||
type='radio'
|
||||
value={index}
|
||||
/>{' '}
|
||||
<span className='video-quiz-input-visible'>
|
||||
{this.state.selectedOption === index ? (
|
||||
<span className='video-quiz-selected-input' />
|
||||
) : null}
|
||||
</span>
|
||||
<PrismFormatted
|
||||
className={'video-quiz-option'}
|
||||
text={answer}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<MultipleChoiceQuestions
|
||||
questions={question}
|
||||
selectedOption={this.state.selectedOption}
|
||||
isWrongAnswer={this.state.isWrongAnswer}
|
||||
handleOptionChange={this.handleOptionChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{this.state.isWrongAnswer && (
|
||||
<span>
|
||||
{feedback ? (
|
||||
<PrismFormatted
|
||||
className={'multiple-choice-feedback'}
|
||||
text={feedback}
|
||||
/>
|
||||
) : (
|
||||
t('learn.wrong-answer')
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!this.state.allAssignmentsCompleted &&
|
||||
assignments.length > 0 && (
|
||||
<>
|
||||
<br />
|
||||
<span>{t('learn.assignment-not-complete')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
<Button
|
||||
block={true}
|
||||
size='medium'
|
||||
|
|
|
@ -12,7 +12,6 @@ import { createSelector } from 'reselect';
|
|||
import { Container, Col, Row, Button } from '@freecodecamp/ui';
|
||||
|
||||
// Local Utilities
|
||||
import Loader from '../../../components/helpers/loader';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
|
@ -23,7 +22,7 @@ import VideoPlayer from '../components/video-player';
|
|||
import ChallengeTitle from '../components/challenge-title';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import MultipleChoiceQuestions from '../components/multiple-choice-questions';
|
||||
import {
|
||||
challengeMounted,
|
||||
updateChallengeMeta,
|
||||
|
@ -201,7 +200,7 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
|
|||
videoLocaleIds,
|
||||
bilibiliIds,
|
||||
fields: { blockName },
|
||||
question: { text, answers, solution }
|
||||
question
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -218,10 +217,7 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
|
|||
`intro:${superBlock}.blocks.${block}.title`
|
||||
)} - ${title}`;
|
||||
|
||||
const feedback =
|
||||
this.state.selectedOption !== null
|
||||
? answers[this.state.selectedOption].feedback
|
||||
: undefined;
|
||||
const { solution } = question;
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
|
@ -248,84 +244,28 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
|
|||
|
||||
{challengeType === challengeTypes.video && (
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
<div className='video-wrapper'>
|
||||
{!this.state.videoIsLoaded ? (
|
||||
<div className='video-placeholder-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
<VideoPlayer
|
||||
bilibiliIds={bilibiliIds}
|
||||
onVideoLoad={this.onVideoLoad}
|
||||
title={title}
|
||||
videoId={videoId}
|
||||
videoIsLoaded={this.state.videoIsLoaded}
|
||||
videoLocaleIds={videoLocaleIds}
|
||||
/>
|
||||
</div>
|
||||
<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}>
|
||||
<ChallengeDescription description={description} />
|
||||
<PrismFormatted className={'line-numbers'} text={text} />
|
||||
<Spacer size='medium' />
|
||||
<ObserveKeys>
|
||||
<div className='video-quiz-options'>
|
||||
{answers.map(({ answer }, index) => (
|
||||
// answers are static and have no natural id property, so
|
||||
// index should be fine as a key:
|
||||
<label
|
||||
className='video-quiz-option-label'
|
||||
key={index}
|
||||
htmlFor={`mc-question-${index}`}
|
||||
>
|
||||
<input
|
||||
checked={this.state.selectedOption === index}
|
||||
className='sr-only'
|
||||
name='quiz'
|
||||
onChange={this.handleOptionChange}
|
||||
type='radio'
|
||||
value={index}
|
||||
id={`mc-question-${index}`}
|
||||
/>{' '}
|
||||
<span className='video-quiz-input-visible'>
|
||||
{this.state.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>
|
||||
<MultipleChoiceQuestions
|
||||
questions={question}
|
||||
selectedOption={this.state.selectedOption}
|
||||
isWrongAnswer={this.state.showWrong}
|
||||
handleOptionChange={this.handleOptionChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{this.state.showWrong ? (
|
||||
<span>
|
||||
{feedback ? (
|
||||
<PrismFormatted
|
||||
className={'multiple-choice-feedback'}
|
||||
text={feedback}
|
||||
/>
|
||||
) : (
|
||||
t('learn.wrong-answer')
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t('learn.check-answer')}</span>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
<Button
|
||||
block={true}
|
||||
variant='primary'
|
||||
|
|
Loading…
Reference in New Issue