mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-01 17:03:30 -05:00
feat(client, challenge-parser): add explanation to challenges (#56472)
This commit is contained in:
@@ -261,6 +261,7 @@ exports.createSchemaCustomization = ({ actions }) => {
|
||||
type Challenge {
|
||||
blockType: String
|
||||
challengeFiles: [FileContents]
|
||||
explanation: String
|
||||
notes: String
|
||||
url: String
|
||||
assignments: [String]
|
||||
|
||||
@@ -388,6 +388,7 @@
|
||||
"assignment-not-complete": "Please complete the assignments",
|
||||
"assignments": "Assignments",
|
||||
"question": "Question",
|
||||
"explanation": "Explanation",
|
||||
"solution-link": "Solution Link",
|
||||
"source-code-link": "Source Code Link",
|
||||
"ms-link": "Microsoft Link",
|
||||
|
||||
@@ -174,6 +174,7 @@ export type ChallengeNode = {
|
||||
demoType: 'onClick' | 'onLoad' | null;
|
||||
description: string;
|
||||
challengeFiles: ChallengeFiles;
|
||||
explanation: string;
|
||||
fields: Fields;
|
||||
fillInTheBlank: FillInTheBlank;
|
||||
forumTopicId: number;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.explanation {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import PrismFormatted from './prism-formatted';
|
||||
|
||||
interface ChallengeExplanationProps {
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
function ChallengeExplanation({
|
||||
explanation
|
||||
}: ChallengeExplanationProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<details className='explanation'>
|
||||
<summary>{t('learn.explanation')}</summary>
|
||||
<Spacer size='medium' />
|
||||
<PrismFormatted className={'line-numbers'} text={explanation} />
|
||||
</details>
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ChallengeExplanation.displayName = 'ChallengeExplanation';
|
||||
|
||||
export default ChallengeExplanation;
|
||||
@@ -18,6 +18,7 @@ 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 ChallegeExplanation from '../components/challenge-explanation';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import FillInTheBlanks from '../components/fill-in-the-blanks';
|
||||
@@ -60,7 +61,6 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
interface ShowFillInTheBlankProps {
|
||||
challengeMounted: (arg0: string) => void;
|
||||
data: { challengeNode: ChallengeNode };
|
||||
description: string;
|
||||
isChallengeCompleted: boolean;
|
||||
initTests: (xs: Test[]) => void;
|
||||
openCompletionModal: () => void;
|
||||
@@ -250,6 +250,7 @@ class ShowFillInTheBlank extends Component<
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
explanation,
|
||||
superBlock,
|
||||
block,
|
||||
translationPending,
|
||||
@@ -328,7 +329,13 @@ class ShowFillInTheBlank extends Component<
|
||||
handleInputChange={this.handleInputChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
|
||||
{explanation ? (
|
||||
<ChallegeExplanation explanation={explanation} />
|
||||
) : (
|
||||
<Spacer size='medium' />
|
||||
)}
|
||||
|
||||
<Button
|
||||
block={true}
|
||||
variant='primary'
|
||||
@@ -368,6 +375,7 @@ export const query = graphql`
|
||||
title
|
||||
description
|
||||
instructions
|
||||
explanation
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
|
||||
@@ -23,6 +23,7 @@ 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 {
|
||||
@@ -62,7 +63,6 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
interface ShowOdinProps {
|
||||
challengeMounted: (arg0: string) => void;
|
||||
data: { challengeNode: ChallengeNode };
|
||||
description: string;
|
||||
initTests: (xs: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
openCompletionModal: () => void;
|
||||
@@ -231,6 +231,7 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
explanation,
|
||||
superBlock,
|
||||
block,
|
||||
videoId,
|
||||
@@ -336,7 +337,13 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
handleOptionChange={this.handleOptionChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
|
||||
{explanation ? (
|
||||
<ChallegeExplanation explanation={explanation} />
|
||||
) : (
|
||||
<Spacer size='medium' />
|
||||
)}
|
||||
|
||||
<Button
|
||||
block={true}
|
||||
size='medium'
|
||||
@@ -398,6 +405,7 @@ export const query = graphql`
|
||||
title
|
||||
description
|
||||
instructions
|
||||
explanation
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
{
|
||||
"id": "66607e5b317411dd5e8aae22",
|
||||
"title": "Dialogue 1: I'm Tom"
|
||||
},
|
||||
{
|
||||
"id": "66a909dc2e382527ead44c23",
|
||||
"title": "Task 1"
|
||||
},
|
||||
{
|
||||
"id": "66a90bbd872bca2a1b7e63a9",
|
||||
"title": "Task 2"
|
||||
}
|
||||
],
|
||||
"helpCategory": "English"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
id: 66a909dc2e382527ead44c23
|
||||
title: Task 1
|
||||
challengeType: 19
|
||||
dashedName: task-1
|
||||
---
|
||||
<!-- (Audio) Bob: Hey Lisa, do you have a minute? -->
|
||||
|
||||
# --instructions--
|
||||
|
||||
Listen to the audio and answer the question below.
|
||||
|
||||
# --questions--
|
||||
|
||||
## --text--
|
||||
|
||||
What does Bob want to know from Lisa?
|
||||
|
||||
## --answers--
|
||||
|
||||
If Lisa can lend him some money.
|
||||
|
||||
### --feedback--
|
||||
|
||||
His question is not about borrowing money.
|
||||
|
||||
---
|
||||
|
||||
If Lisa can help him with his project.
|
||||
|
||||
### --feedback--
|
||||
|
||||
The question is not specifically about a project. It is more general.
|
||||
|
||||
---
|
||||
|
||||
If Lisa has time to talk.
|
||||
|
||||
---
|
||||
|
||||
If Lisa is feeling well.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This question is not about Lisa's health.
|
||||
|
||||
## --video-solution--
|
||||
|
||||
3
|
||||
|
||||
# --explanation--
|
||||
|
||||
`Do you have a minute?` is a polite way to ask if someone has time to talk.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-boardroom.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Bob",
|
||||
"position": {
|
||||
"x": 50,
|
||||
"y": 15,
|
||||
"z": 1.2
|
||||
},
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "B1_1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 0,
|
||||
"finishTimestamp": 1.72
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Bob",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Bob",
|
||||
"startTime": 1,
|
||||
"finishTime": 2.72,
|
||||
"dialogue": {
|
||||
"text": "Hey Lisa, do you have a minute?",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Bob",
|
||||
"opacity": 0,
|
||||
"startTime": 3.22
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
id: 66a90bbd872bca2a1b7e63a9
|
||||
title: Task 2
|
||||
challengeType: 22
|
||||
dashedName: task-2
|
||||
---
|
||||
|
||||
<!-- (Audio) Bob: Hey Lisa, do you have a minute? -->
|
||||
|
||||
# --instructions--
|
||||
|
||||
Listen to the audio and complete the sentence below.
|
||||
|
||||
# --fillInTheBlank--
|
||||
|
||||
## --sentence--
|
||||
|
||||
`Hey Lisa, BLANK you have a minute?`
|
||||
|
||||
## --blanks--
|
||||
|
||||
`do`
|
||||
|
||||
### --feedback--
|
||||
|
||||
This is the auxiliary verb used to form the question.
|
||||
|
||||
# --explanation--
|
||||
|
||||
The auxiliary verb `do` is used to form questions in the `Present Simple` tense.
|
||||
|
||||
For example, in the question `Do you have a minute?`, `do` is used to ask if someone has time.
|
||||
|
||||
# --scene--
|
||||
|
||||
```json
|
||||
{
|
||||
"setup": {
|
||||
"background": "company2-boardroom.png",
|
||||
"characters": [
|
||||
{
|
||||
"character": "Bob",
|
||||
"position": {
|
||||
"x": 50,
|
||||
"y": 15,
|
||||
"z": 1.2
|
||||
},
|
||||
"opacity": 0
|
||||
}
|
||||
],
|
||||
"audio": {
|
||||
"filename": "B1_1-1.mp3",
|
||||
"startTime": 1,
|
||||
"startTimestamp": 0,
|
||||
"finishTimestamp": 1.72
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"character": "Bob",
|
||||
"opacity": 1,
|
||||
"startTime": 0
|
||||
},
|
||||
{
|
||||
"character": "Bob",
|
||||
"startTime": 1,
|
||||
"finishTime": 2.72,
|
||||
"dialogue": {
|
||||
"text": "Hey Lisa, do you have a minute?",
|
||||
"align": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"character": "Bob",
|
||||
"opacity": 0,
|
||||
"startTime": 3.22
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -143,6 +143,7 @@ const schema = Joi.object()
|
||||
is: [
|
||||
challengeTypes.step,
|
||||
challengeTypes.video,
|
||||
challengeTypes.multipleChoice,
|
||||
challengeTypes.fillInTheBlank
|
||||
],
|
||||
then: Joi.string().allow(''),
|
||||
@@ -150,6 +151,10 @@ const schema = Joi.object()
|
||||
}),
|
||||
disableLoopProtectTests: Joi.boolean().required(),
|
||||
disableLoopProtectPreview: Joi.boolean().required(),
|
||||
explanation: Joi.when('challengeType', {
|
||||
is: [challengeTypes.multipleChoice, challengeTypes.fillInTheBlank],
|
||||
then: Joi.string()
|
||||
}),
|
||||
challengeFiles: Joi.array().items(fileJoi),
|
||||
guideUrl: Joi.string().uri({ scheme: 'https' }),
|
||||
hasEditableBoundaries: Joi.boolean(),
|
||||
|
||||
@@ -140,6 +140,7 @@ const schema = Joi.object()
|
||||
is: [
|
||||
challengeTypes.step,
|
||||
challengeTypes.video,
|
||||
challengeTypes.multipleChoice,
|
||||
challengeTypes.fillInTheBlank
|
||||
],
|
||||
then: Joi.string().allow(''),
|
||||
@@ -147,6 +148,10 @@ const schema = Joi.object()
|
||||
}),
|
||||
disableLoopProtectTests: Joi.boolean().required(),
|
||||
disableLoopProtectPreview: Joi.boolean().required(),
|
||||
explanation: Joi.when('challengeType', {
|
||||
is: [challengeTypes.multipleChoice, challengeTypes.fillInTheBlank],
|
||||
then: Joi.string()
|
||||
}),
|
||||
challengeFiles: Joi.array().items(fileJoi),
|
||||
guideUrl: Joi.string().uri({ scheme: 'https' }),
|
||||
hasEditableBoundaries: Joi.boolean(),
|
||||
|
||||
@@ -53,7 +53,7 @@ const processor = unified()
|
||||
.use(addScene)
|
||||
.use(addQuizzes)
|
||||
.use(addTests)
|
||||
.use(addText, ['description', 'instructions', 'notes']);
|
||||
.use(addText, ['description', 'instructions', 'notes', 'explanation']);
|
||||
|
||||
exports.parseMD = function parseMD(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Reference in New Issue
Block a user