feat(client/curriculum): add upcoming english superblock and challenge types (#52201)

This commit is contained in:
Tom
2023-11-09 03:08:51 -06:00
committed by GitHub
parent e68b8f1b71
commit ddc459e71e
47 changed files with 1811 additions and 29 deletions

View File

@@ -36,6 +36,7 @@ const foundationalCSharpBase =
'/learn/foundational-c-sharp-with-microsoft/foundational-c-sharp-with-microsoft-certification-exam';
const upcomingPythonBase = '/learn/upcoming-python';
const exampleCertBase = '/learn/example-certification';
const a2EnglishBase = '/learn/a2-english-for-developers';
const legacyFrontEndBase = feLibsBase;
const legacyFrontEndResponsiveBase = responsiveWebBase;
const legacyFrontEndTakeHomeBase = takeHomeBase;
@@ -786,6 +787,19 @@ const allStandardCerts = [
certSlug: Certification.UpcomingPython
}
]
},
{
id: '651dd7e01d697d0aab7833b7',
title: 'A2 English for Developers',
certSlug: Certification.A2English,
projects: [
{
id: '651dd3e06ffb500e3f2ce478',
title: 'Challenge 1',
link: `${a2EnglishBase}/learn-greetings-in-your-first-day-at-the-office/challenge-1`,
certSlug: Certification.A2English
}
]
}
] as const;

View File

@@ -74,6 +74,7 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
edges {
node {
challenge {
audioPath
block
certification
challengeType
@@ -84,6 +85,13 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
slug
blockHashSlug
}
fillInTheBlank {
sentence
blanks {
answer
feedback
}
}
hasEditableBoundaries
id
msTrophyId
@@ -287,12 +295,14 @@ exports.createSchemaCustomization = ({ actions }) => {
challenge: Challenge
}
type Challenge {
audioPath: String
challengeFiles: [FileContents]
notes: String
url: String
assignments: [String]
prerequisites: [PrerequisiteChallenge]
msTrophyId: String
fillInTheBlank: FillInTheBlank
}
type FileContents {
fileKey: String
@@ -307,6 +317,14 @@ exports.createSchemaCustomization = ({ actions }) => {
id: String
title: String
}
type FillInTheBlank {
sentence: String
blanks: [Blank]
}
type Blank {
answer: String
feedback: String
}
`;
createTypes(typeDefs);
};

View File

@@ -1146,6 +1146,27 @@
}
}
},
"a2-english-for-developers": {
"title": "A2 English for Developers (Beta)",
"intro": [
"This course teaches the English language, designed for developers.",
"More text here."
],
"blocks": {
"learn-greetings-in-your-first-day-at-the-office": {
"title": "Learn Greetings in your First Day at the Office",
"intro": ["Learn greetings."]
},
"learn-introductions-in-an-online-team-meeting": {
"title": "Learn Introductions in an Online Team Meeting",
"intro": ["Learn introductions."]
},
"learn-conversation-starters-in-the-break-room": {
"title": "Learn Conversation Starters in the Break Room",
"intro": ["Learn conversation starters."]
}
}
},
"example-certification": {
"title": "Example Certification",
"intro": ["placeholder"],

View File

@@ -44,6 +44,7 @@
"settings": "Settings",
"take-me": "Take me to the Challenges",
"check-answer": "Check your answer",
"submit": "Submit",
"get-hint": "Get a Hint",
"ask-for-help": "Ask for Help",
"create-post": "Create a help post on the forum",
@@ -417,6 +418,7 @@
"building-a-university": "We're Building a Free Computer Science University Degree Program",
"if-help-university": "We've already made a ton of progress. Support our charity with the long road ahead.",
"preview-external-window": "Preview currently showing in external window.",
"fill-in-the-blank": "Fill in the blank",
"exam": {
"qualified": "Congratulations, you have completed all the requirements to qualify for the exam.",
"not-qualified": "You have not met the requirements to be eligible for the exam. To qualify, please complete the following challenges:",
@@ -890,6 +892,8 @@
"college-algebra-with-python-v8": "College Algebra with Python Certification",
"Foundational C# with Microsoft": "Foundational C# with Microsoft",
"foundational-c-sharp-with-microsoft": "Foundational C# with Microsoft Certification",
"A2 English for Developers": "A2 English for Developers",
"a2-english-for-developers": "A2 English for Developers Certification",
"Legacy Front End": "Legacy Front End",
"legacy-front-end": "Front End Certification",
"Legacy Back End": "Legacy Back End",

View File

@@ -37,7 +37,8 @@ const iconMap = {
[SuperBlocks.CollegeAlgebraPy]: CollegeAlgebra,
[SuperBlocks.FoundationalCSharp]: CSharpLogo,
[SuperBlocks.ExampleCertification]: ResponsiveDesign,
[SuperBlocks.UpcomingPython]: PythonIcon
[SuperBlocks.UpcomingPython]: PythonIcon,
[SuperBlocks.A2English]: Graduation
};
type SuperBlockIconProps = {

View File

@@ -131,7 +131,8 @@ const isCertMapSelector = createSelector(
// TODO: remove Example Certification? Also, include Upcoming Python
// Certification.
'Example Certification': false,
'Upcoming Python Certification': false
'Upcoming Python Certification': false,
'A2 English for Developers': false
})
);

View File

@@ -0,0 +1,9 @@
---
title: A2 English for Developers
superBlock: a2-english-for-developers
certification: a2-english-for-developers
---
## Introduction to A2 English for Developers
A2 English for Developers

View File

@@ -0,0 +1,9 @@
---
title: Learn Conversation Starters in the Break Room
block: learn-conversation-starters-in-the-break-room
superBlock: a2-english-for-developers
---
## Introduction to Learn Conversation Starters in the Break Room
Learn Conversation Starters in the Break Room

View File

@@ -0,0 +1,9 @@
---
title: Learn Greetings in your First Day at the Office
block: learn-greetings-in-your-first-day-at-the-office
superBlock: a2-english-for-developers
---
## Introduction to Learn Greetings in your First Day at the Office
Learn Greetings in your First Day at the Office

View File

@@ -0,0 +1,9 @@
---
title: Learn Introductions in an Online Team Meeting
block: learn-introductions-in-an-online-team-meeting
superBlock: a2-english-for-developers
---
## Introduction to Learn Introductions in an Online Team Meeting
Learn Introductions in an Online Team Meeting

View File

@@ -57,6 +57,12 @@ export type Question = {
answers: MultipleChoiceAnswer[];
solution: number;
};
export type FillInTheBlank = {
sentence: string;
blanks: MultipleChoiceAnswer[];
};
export type Fields = {
slug: string;
blockHashSlug: string;
@@ -102,6 +108,7 @@ export type ChallengeWithCompletedNode = {
export type ChallengeNode = {
challenge: {
audioPath: string;
block: string;
certification: string;
challengeOrder: number;
@@ -110,6 +117,7 @@ export type ChallengeNode = {
description: string;
challengeFiles: ChallengeFiles;
fields: Fields;
fillInTheBlank: FillInTheBlank;
forumTopicId: number;
guideUrl: string;
head: string[];

View File

@@ -0,0 +1,334 @@
// Package Utilities
import { Button } from '@freecodecamp/react-bootstrap';
import { graphql } from 'gatsby';
import React, { Component } from 'react';
import Helmet from 'react-helmet';
import { ObserveKeys } from 'react-hotkeys';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { Container, Col, Row } 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 } from '../../../redux/prop-types';
import Hotkeys from '../components/hotkeys';
import VideoPlayer from '../components/video-player';
import CompletionModal from '../components/completion-modal';
import HelpModal from '../components/help-modal';
import PrismFormatted from '../components/prism-formatted';
import {
challengeMounted,
updateChallengeMeta,
openModal
} from '../redux/actions';
import { isChallengeCompletedSelector } from '../redux/selectors';
// Styles
import '../odin/show.css';
import '../video.css';
// Redux Setup
const mapStateToProps = createSelector(
isChallengeCompletedSelector,
(isChallengeCompleted: boolean) => ({
isChallengeCompleted
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
challengeMounted,
openCompletionModal: () => openModal('completion'),
openHelpModal: () => openModal('help')
},
dispatch
);
// Types
interface ShowDialogueProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNode };
description: string;
isChallengeCompleted: boolean;
openCompletionModal: () => void;
openHelpModal: () => void;
pageContext: {
challengeMeta: ChallengeMeta;
};
t: TFunction;
updateChallengeMeta: (arg0: ChallengeMeta) => void;
}
interface ShowDialogueState {
subtitles: string;
downloadURL: string | null;
assignmentsCompleted: number;
allAssignmentsCompleted: boolean;
videoIsLoaded: boolean;
}
// Component
class ShowDialogue extends Component<ShowDialogueProps, ShowDialogueState> {
static displayName: string;
private container: React.RefObject<HTMLElement> = React.createRef();
constructor(props: ShowDialogueProps) {
super(props);
this.state = {
subtitles: '',
downloadURL: null,
assignmentsCompleted: 0,
allAssignmentsCompleted: false,
videoIsLoaded: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount(): void {
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title, challengeType, helpCategory }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
updateChallengeMeta({
...challengeMeta,
title,
challengeType,
helpCategory
});
challengeMounted(challengeMeta.id);
this.container.current?.focus();
}
componentDidUpdate(prevProps: ShowDialogueProps): void {
const {
data: {
challengeNode: {
challenge: { title: prevTitle }
}
}
} = prevProps;
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title: currentTitle, challengeType, helpCategory }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
if (prevTitle !== currentTitle) {
updateChallengeMeta({
...challengeMeta,
title: currentTitle,
challengeType,
helpCategory
});
challengeMounted(challengeMeta.id);
}
}
handleSubmit() {
const { openCompletionModal } = this.props;
if (this.state.allAssignmentsCompleted) {
openCompletionModal();
}
}
handleAssignmentChange = (
event: React.ChangeEvent<HTMLInputElement>,
totalAssignments: number
): void => {
const assignmentsCompleted = event.target.checked
? this.state.assignmentsCompleted + 1
: this.state.assignmentsCompleted - 1;
const allAssignmentsCompleted = totalAssignments === assignmentsCompleted;
this.setState({
assignmentsCompleted,
allAssignmentsCompleted
});
};
onVideoLoad = () => {
this.setState({
videoIsLoaded: true
});
};
render() {
const {
data: {
challengeNode: {
challenge: {
title,
description,
superBlock,
block,
videoId,
fields: { blockName },
assignments
}
}
},
openHelpModal,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
},
t
} = this.props;
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
return (
<Hotkeys
executeChallenge={() => this.handleSubmit()}
containerRef={this.container}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
<LearnLayout>
<Helmet
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
/>
<Container>
<Row>
{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
onVideoLoad={this.onVideoLoad}
title={title}
videoId={videoId}
videoIsLoaded={this.state.videoIsLoaded}
/>
</div>
</Col>
)}
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer size='medium' />
<h2>{title}</h2>
<PrismFormatted className={'line-numbers'} text={description} />
<Spacer size='medium' />
<ObserveKeys>
<h2>{t('learn.assignments')}</h2>
<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' />
</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}
bsSize='large'
bsStyle='primary'
disabled={!this.state.allAssignmentsCompleted}
onClick={() => this.handleSubmit()}
>
{t('buttons.submit')}
</Button>
<Button
block={true}
bsSize='large'
bsStyle='primary'
className='btn-invert'
onClick={openHelpModal}
>
{t('buttons.ask-for-help')}
</Button>
<Spacer size='large' />
</Col>
<CompletionModal />
<HelpModal challengeTitle={title} challengeBlock={blockName} />
</Row>
</Container>
</LearnLayout>
</Hotkeys>
);
}
}
ShowDialogue.displayName = 'ShowDialogue';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(ShowDialogue));
export const query = graphql`
query Dialogue($slug: String!) {
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
challenge {
videoId
title
description
challengeType
helpCategory
superBlock
block
fields {
slug
blockName
}
translationPending
assignments
}
}
}
`;

View File

@@ -0,0 +1,44 @@
.fill-in-the-blank-input {
padding: 0;
text-align: center;
background-color: var(--quaternary-background);
color: var(--tertiary-color);
border-radius: 0;
font-family: var(--font-family-monospace);
overflow-wrap: anywhere;
line-height: 1.5rem;
z-index: 1;
position: relative;
border: 1px solid var(--secondary-color);
border-left: none;
border-right: none;
border-bottom-width: 4px;
border-bottom-color: var(--gray-45) !important;
}
.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;
}
.red-underline {
border-bottom-color: var(--danger-background) !important;
}

View File

@@ -0,0 +1,436 @@
// Package Utilities
import { Button } from '@freecodecamp/react-bootstrap';
import { graphql } from 'gatsby';
import React, { Component, Fragment } from 'react';
import Helmet from 'react-helmet';
import { ObserveKeys } from 'react-hotkeys';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { Container, Col, Row } from '@freecodecamp/ui';
// Local Utilities
import Spacer from '../../../components/helpers/spacer';
import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta } from '../../../redux/prop-types';
import Hotkeys from '../components/hotkeys';
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 {
challengeMounted,
updateChallengeMeta,
openModal,
updateSolutionFormValues
} from '../redux/actions';
import { isChallengeCompletedSelector } from '../redux/selectors';
// Styles
import '../video.css';
import './show.css';
// Redux Setup
const mapStateToProps = createSelector(
isChallengeCompletedSelector,
(isChallengeCompleted: boolean) => ({
isChallengeCompleted
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
challengeMounted,
updateSolutionFormValues,
openCompletionModal: () => openModal('completion'),
openHelpModal: () => openModal('help')
},
dispatch
);
// Types
interface ShowFillInTheBlankProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNode };
description: string;
isChallengeCompleted: boolean;
openCompletionModal: () => void;
openHelpModal: () => void;
pageContext: {
challengeMeta: ChallengeMeta;
};
t: TFunction;
updateChallengeMeta: (arg0: ChallengeMeta) => void;
updateSolutionFormValues: () => void;
}
interface ShowFillInTheBlankState {
showWrong: boolean;
userAnswers: (string | null)[];
answersCorrect: (boolean | null)[];
allBlanksFilled: boolean;
feedback: string | null;
showFeedback: boolean;
}
// Component
class ShowFillInTheBlank extends Component<
ShowFillInTheBlankProps,
ShowFillInTheBlankState
> {
static displayName: string;
private container: React.RefObject<HTMLElement> = React.createRef();
constructor(props: ShowFillInTheBlankProps) {
super(props);
const {
data: {
challengeNode: {
challenge: {
fillInTheBlank: { blanks }
}
}
}
} = props;
const emptyArray = blanks.map(() => null);
this.state = {
showWrong: false,
userAnswers: emptyArray,
answersCorrect: emptyArray,
allBlanksFilled: false,
feedback: null,
showFeedback: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount(): void {
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title, challengeType, helpCategory }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
updateChallengeMeta({
...challengeMeta,
title,
challengeType,
helpCategory
});
challengeMounted(challengeMeta.id);
this.container.current?.focus();
}
componentDidUpdate(prevProps: ShowFillInTheBlankProps): void {
const {
data: {
challengeNode: {
challenge: { title: prevTitle }
}
}
} = prevProps;
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title: currentTitle, challengeType, helpCategory }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
if (prevTitle !== currentTitle) {
updateChallengeMeta({
...challengeMeta,
title: currentTitle,
challengeType,
helpCategory
});
challengeMounted(challengeMeta.id);
}
}
handleSubmit() {
const {
openCompletionModal,
data: {
challengeNode: {
challenge: {
fillInTheBlank: { blanks }
}
}
}
} = this.props;
const { userAnswers } = this.state;
const blankAnswers = blanks.map(b => b.answer);
const newAnswersCorrect = userAnswers.map(
(userAnswer, i) => userAnswer === blankAnswers[i]
);
const hasWrongAnswer = newAnswersCorrect.some(a => a === false);
if (!hasWrongAnswer) {
this.setState({
answersCorrect: newAnswersCorrect
});
openCompletionModal();
} else {
const firstWrongIndex = newAnswersCorrect.findIndex(a => a === false);
const feedback =
firstWrongIndex >= 0 ? blanks[firstWrongIndex].feedback : null;
this.setState({
answersCorrect: newAnswersCorrect,
showWrong: true,
showFeedback: true,
feedback: feedback
});
}
}
handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { userAnswers, answersCorrect } = this.state;
const inputIndex = parseInt(e.target.getAttribute('data-index') as string);
const newUserAnswers = [...userAnswers];
newUserAnswers[inputIndex] = e.target.value;
const newAnswersCorrect = [...answersCorrect];
newAnswersCorrect[inputIndex] = null;
const allBlanksFilled = newUserAnswers.every(a => a);
this.setState({
userAnswers: newUserAnswers,
answersCorrect: newAnswersCorrect,
allBlanksFilled,
showWrong: false
});
};
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 '';
}
render() {
const {
data: {
challengeNode: {
challenge: {
title,
description,
instructions,
superBlock,
block,
translationPending,
fields: { blockName },
fillInTheBlank: { sentence, blanks },
audioPath
}
}
},
openHelpModal,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
},
t,
isChallengeCompleted
} = this.props;
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
const { allBlanksFilled, feedback, showFeedback, showWrong } = this.state;
const splitSentence = sentence.replace(/^<p>|<\/p>$/g, '').split('_');
const blankAnswers = blanks.map(b => b.answer);
return (
<Hotkeys
executeChallenge={() => this.handleSubmit()}
containerRef={this.container}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
<LearnLayout>
<Helmet
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
/>
<Container>
<Row>
<Spacer size='medium' />
<ChallengeTitle
isCompleted={isChallengeCompleted}
translationPending={translationPending}
>
{title}
</ChallengeTitle>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<PrismFormatted text={description} />
{audioPath && (
<>
<Spacer size='small' />
<Spacer size='small' />
{/* TODO: Add tracks for audio elements */}
{/* eslint-disable-next-line jsx-a11y/media-has-caption*/}
<audio className='audio' controls>
<source
src={`https://cdn.freecodecamp.org/${audioPath}`}
type='audio/mp3'
></source>
</audio>
</>
)}
<Spacer size='medium' />
<PrismFormatted text={instructions} />
<Spacer size='medium' />
<h2>{t('learn.fill-in-the-blank')}</h2>
<Spacer size='small' />
<ObserveKeys>
<div>
<p>
{splitSentence.map((s, i) => {
return (
<Fragment key={i}>
<PrismFormatted
text={this.addCodeTags(s, i, blankAnswers.length)}
className={`code-tag ${this.addPrismClass(
i,
blankAnswers.length
)}`}
useSpan
noAria
/>
{blankAnswers[i] && (
<input
type='text'
maxLength={blankAnswers[i].length + 3}
className={`fill-in-the-blank-input ${this.addInputClass(
i
)}`}
onChange={this.handleInputChange}
data-index={i}
style={{
width: `${blankAnswers[i].length * 11 + 11}px`
}}
/>
)}
</Fragment>
);
})}
</p>
</div>
</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}
bsStyle='primary'
disabled={!allBlanksFilled}
onClick={() => this.handleSubmit()}
>
{t('buttons.check-answer')}
</Button>
<Button
block={true}
bsStyle='primary'
className='btn-invert'
onClick={openHelpModal}
>
{t('buttons.ask-for-help')}
</Button>
<Spacer size='large' />
</Col>
<CompletionModal />
<HelpModal challengeTitle={title} challengeBlock={blockName} />
</Row>
</Container>
</LearnLayout>
</Hotkeys>
);
}
}
ShowFillInTheBlank.displayName = 'ShowFillInTheBlank';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(ShowFillInTheBlank));
export const query = graphql`
query FillInTheBlankChallenge($slug: String!) {
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
challenge {
title
description
instructions
challengeType
helpCategory
superBlock
block
fields {
blockName
slug
}
fillInTheBlank {
sentence
blanks {
answer
feedback
}
}
translationPending
audioPath
}
}
}
`;

View File

@@ -26,3 +26,7 @@ input[type='checkbox']::before {
input[type='checkbox']:checked::before {
transform: scale(1);
}
audio {
background-color: aqua;
}

View File

@@ -216,7 +216,8 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
bilibiliIds,
fields: { blockName },
question: { text, answers, solution },
assignments
assignments,
audioPath
}
}
},
@@ -276,7 +277,21 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
<Spacer size='medium' />
<h2>{title}</h2>
<PrismFormatted className={'line-numbers'} text={description} />
<Spacer size='medium' />
{audioPath && (
<>
<Spacer size='small' />
<Spacer size='small' />
{/* TODO: Add tracks for audio elements */}
{/* eslint-disable-next-line jsx-a11y/media-has-caption*/}
<audio className='audio' controls>
<source
src={`https://cdn.freecodecamp.org/${audioPath}`}
type='audio/mp3'
></source>
</audio>
<Spacer size='medium' />
</>
)}
<ObserveKeys>
{assignments.length > 0 && (
<>
@@ -443,6 +458,7 @@ export const query = graphql`
}
translationPending
assignments
audioPath
}
}
}

View File

@@ -49,6 +49,16 @@ const msTrophy = path.resolve(
'../../src/templates/Challenges/ms-trophy/show.tsx'
);
const dialogue = path.resolve(
__dirname,
'../../src/templates/Challenges/dialogue/show.tsx'
);
const fillInTheBlank = path.resolve(
__dirname,
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
);
const views = {
backend,
classic,
@@ -58,7 +68,9 @@ const views = {
codeAlly,
odin,
exam,
msTrophy
msTrophy,
dialogue,
fillInTheBlank
// quiz: Quiz
};

View File

@@ -0,0 +1,26 @@
{
"name": "Learn Conversation Starters in the Break Room",
"isUpcomingChange": true,
"dashedName": "learn-conversation-starters-in-the-break-room",
"order": 2,
"time": "5 hours",
"template": "",
"required": [],
"superBlock": "a2-english-for-developers",
"isBeta": true,
"challengeOrder": [
{
"id": "651dd5ae6ffb500e3f2ce47c",
"title": "Challenge 1"
},
{
"id": "651dd5d86ffb500e3f2ce47d",
"title": "Challenge 2"
},
{
"id": "651dd5f41d697d0aab7833b5",
"title": "Challenge 3"
}
],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,50 @@
{
"name": "Learn Greetings in your First Day at the Office",
"isUpcomingChange": true,
"dashedName": "learn-greetings-in-your-first-day-at-the-office",
"order": 0,
"time": "5 hours",
"template": "",
"required": [],
"superBlock": "a2-english-for-developers",
"isBeta": true,
"challengeOrder": [
{
"id": "651dd3e06ffb500e3f2ce478",
"title": "Dialogue: Introducing"
},
{
"id": "651dd5296ffb500e3f2ce479",
"title": "You Are"
},
{
"id": "651dd5386ffb500e3f2ce47a",
"title": "Right"
},
{
"id": "6537e6ece93e5724eeb27c54",
"title": "Name and Job Title"
},
{
"id": "6543aa3df5f028dba112f275",
"title": "Team Lead"
},
{
"id": "6543aaa9f5f028dba112f276",
"title": "That's Right"
},
{
"id": "6543aae6f5f028dba112f277",
"title": "That's Right: 2"
},
{
"id": "6543abeff5f028dba112f278",
"title": "I am: I'm"
},
{
"id": "6543abf5f5f028dba112f279",
"title": "I'm"
}
],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,26 @@
{
"name": "Learn Introductions in an Online Team Meeting",
"isUpcomingChange": true,
"dashedName": "learn-introductions-in-an-online-team-meeting",
"order": 1,
"time": "5 hours",
"template": "",
"required": [],
"superBlock": "a2-english-for-developers",
"isBeta": true,
"challengeOrder": [
{
"id": "651dd5a46ffb500e3f2ce47b",
"title": "Challenge 1"
},
{
"id": "651dd5e46ffb500e3f2ce47e",
"title": "Challenge 2"
},
{
"id": "651dd6071d697d0aab7833b6",
"title": "Challenge 3"
}
],
"helpCategory": "HTML-CSS"
}

View File

@@ -0,0 +1,9 @@
---
id: 651dd7e01d697d0aab7833b7
title: A2 English for Developers Certification
certification: a2-english-for-developers
challengeType: 7
isPrivate: true
tests:
- id: 651dd3e06ffb500e3f2ce478
title: Challenge 1

View File

@@ -0,0 +1,93 @@
---
id: 651dd5ae6ffb500e3f2ce47c
title: Challenge 1
challengeType: 11
videoId: nLDychdBwUg
dashedName: challenge-1
---
# --description--
Here's the description
# --instructions--
Here's the instructions
# --question--
## --text--
What is Maria doing when she says, `"You must be the new graphic designer"`?
## --answers--
Asking about someone's job role.
### --feedback--
No, that's not correct
---
Giving a job description.
### --feedback--
No, that's not correct
```js
console.log('with code');
```
---
Making a statement based on her assumption.
### --feedback--
No, that's not correct
---
Expressing a possibility.
### --feedback--
No, that's not correct
---
Giving a job description.
### --feedback--
No, that's not correct
---
Giving a job description.
### --feedback--
No, that's not correct
---
Giving a job description.
### --feedback--
No, that's not correct
---
Giving a job description.
### --feedback--
No, that's not correct
## --video-solution--
3

View File

@@ -0,0 +1,41 @@
---
id: 651dd5d86ffb500e3f2ce47d
title: Challenge 2
challengeType: 11
videoId: nLDychdBwUg
dashedName: challenge-2
---
# --description--
Here's the description
# --instructions--
Here's the instructions
# --question--
## --text--
What is Maria assuming about Tom?
## --answers--
Tom is the team lead.
---
Maria is the new graphic designer.
---
Tom is leaving the company.
---
Tom is the new graphic designer.
## --video-solution--
4

View File

@@ -0,0 +1,44 @@
---
id: 651dd5f41d697d0aab7833b5
title: Challenge 3
challengeType: 11
videoId: nLDychdBwUg
dashedName: challenge-3
---
# --description--
Here's the description
# --instructions--
Here's the instructions
Fill in the blank.
Hello! You ____ __ the new graphic designer. I'm Maria, the team lead.
# --question--
## --text--
What is Maria assuming about Tom?
## --answers--
Tom is the team lead.
---
Maria is the new graphic designer.
---
Tom is leaving the company.
---
Tom is the new graphic designer.
## --video-solution--
4

View File

@@ -0,0 +1,15 @@
---
id: 651dd3e06ffb500e3f2ce478
title: "Dialogue: Introducing"
challengeType: 21
videoId: nLDychdBwUg
dashedName: dialogue-introducing
---
# --description--
What the video above to understand the context of the upcoming lessons.
# --assignment--
Watch the video

View File

@@ -0,0 +1,49 @@
---
id: 6543abeff5f028dba112f278
title: "I am: I'm"
challengeType: 19
dashedName: i-am-im
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
In English, the verb `to be` is used to talk about identities, characteristics, and more. The contraction `I'm` is a combination of `I` and `am`. Here, Tom uses it to introduce himself.
# --question--
## --text--
Which operation correctly expands the contraction `I'm`?
## --answers--
`I is`
### --feedback--
Think about which verb form would correctly fit with `I` to talk about oneself in the present.
---
`I am`
---
`I was`
### --feedback--
Think about which verb form would correctly fit with `I` to talk about oneself in the present.
---
`I have`
### --feedback--
Think about which verb form would correctly fit with `I` to talk about oneself in the present.
## --video-solution--
2

View File

@@ -0,0 +1,21 @@
---
id: 6543abf5f5f028dba112f279
title: "I'm"
challengeType: 22
dashedName: im
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
The word `I'm` is a contraction of `I am`. Contractions are a way to shorten common combinations of words, especially with verbs.
# --fillInTheBlank--
## --sentence--
`Hi, that's right! _ Tom McKenzie.`
## --blanks--
`I'm`

View File

@@ -0,0 +1,53 @@
---
id: 6537e6ece93e5724eeb27c54
title: Name and Job Title
challengeType: 19
dashedName: name-and-job-title
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
In English, we often mention our job or role in a company by saying, `I'm [Name], the [Job title].` This lets others know our position or role.
# --question--
## --text--
What is Maria's job role at the company?
## --answers--
`Graphic Designer`
### --feedback--
Focus on the term Maria used to describe herself.
---
`Team Member`
### --feedback--
Focus on the term Maria used to describe herself.
---
`Team Lead`
### --feedback--
Focus on the term Maria used to describe herself.
---
`CEO`
### --feedback--
Focus on the term Maria used to describe herself.
## --video-solution--
3

View File

@@ -0,0 +1,33 @@
---
id: 651dd5386ffb500e3f2ce47a
title: Right
challengeType: 22
dashedName: right
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
In English, to check or confirm something people sometimes use tag questions. For example, `You are a programmer, right?` Here, `right?` is used as a tag to check or confirm the previous statement.
# --fillInTheBlank--
## --sentence--
`Hello, You _ the new graphic designer, _?`
## --blanks--
`are`
### --feedback--
Pay attention to the verb in the sentence.
---
`right`
### --feedback--
Pay attention to the verb in the sentence.

View File

@@ -0,0 +1,33 @@
---
id: 6543aa3df5f028dba112f275
title: Team Lead
challengeType: 22
dashedName: team-lead
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
A `team lead` is a person who leads or manages a team. In the dialogue, Maria introduces herself as the team lead, meaning she has a leadership role.
# --fillInTheBlank--
## --sentence--
`I'm Maria, the _ _.`
## --blanks--
team
### --feedback--
Focus on the term Maria used to describe herself.
---
lead
### --feedback--
Focus on the term Maria used to describe herself.

View File

@@ -0,0 +1,25 @@
---
id: 6543aae6f5f028dba112f277
title: "That's Right: 2"
challengeType: 22
dashedName: thats-right-2
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
Placeholder Description ___ __
# --fillInTheBlank--
## --sentence--
`Hi, _ _! I'm Tom McKenzie. It's a pleasure to meet you.`
## --blanks--
`that's`
---
`right`

View File

@@ -0,0 +1,49 @@
---
id: 6543aaa9f5f028dba112f276
title: "That's Right"
challengeType: 19
dashedName: thats-right
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
When someone makes a correct assumption or guess about you, you can confirm it using phrases like `that's right`. It's a way of agreeing or saying yes to what is said.
# --question--
## --text--
Which phrase does Tom use to confirm Maria's statement about him?
## --answers--
`that's wrong`
### --feedback--
`That's wrong` is used to disagree.
---
`that's okay`
### --feedback--
`that's okay` usually shows acceptance, not confirmation.
---
`that's right`
---
`that's left`
### --feedback--
`that's left` refers to a direction, not confirmation.
## --video-solution--
3

View File

@@ -0,0 +1,27 @@
---
id: 651dd5296ffb500e3f2ce479
title: You Are
challengeType: 22
dashedName: you-are
audioPath: curriculum/js-music-player/We-Are-Going-to-Make-it.mp3
---
# --description--
In English, when making introductions or identifying someone, you use the verb `to be`. In this case, `You are` is used to address the person Maria is talking to and affirmatively identify their occupation.
Maria is introducing herself and confirming Tom's job role. `Are` is used in the present affirmative to make a statement.
# --fillInTheBlank--
## --sentence--
`Hello, You _ the new graphic designer, right?`
## --blanks--
are
### --feedback--
The verb `to be` is an irregular verb. When conjugated with the pronoun `you`, `be` becomes `are`. For example: `You are an English learner.`

View File

@@ -0,0 +1,41 @@
---
id: 651dd5a46ffb500e3f2ce47b
title: Challenge 1
challengeType: 11
videoId: nLDychdBwUg
dashedName: challenge-1
---
# --description--
Here's the description
# --instructions--
Here's the instructions
# --question--
## --text--
What is Maria doing when she says, `"You must be the new graphic designer"`?
## --answers--
Asking about someone's job role.
---
Giving a job description.
---
Making a statement based on her assumption.
---
Expressing a possibility.
## --video-solution--
3

View File

@@ -0,0 +1,41 @@
---
id: 651dd5e46ffb500e3f2ce47e
title: Challenge 2
challengeType: 11
videoId: nLDychdBwUg
dashedName: challenge-2
---
# --description--
Here's the description
# --instructions--
Here's the instructions
# --question--
## --text--
What is Maria assuming about Tom?
## --answers--
Tom is the team lead.
---
Maria is the new graphic designer.
---
Tom is leaving the company.
---
Tom is the new graphic designer.
## --video-solution--
4

View File

@@ -0,0 +1,44 @@
---
id: 651dd6071d697d0aab7833b6
title: Challenge 3
challengeType: 11
videoId: nLDychdBwUg
dashedName: challenge-3
---
# --description--
Here's the description
# --instructions--
Here's the instructions
Fill in the blank.
Hello! You ____ __ the new graphic designer. I'm Maria, the team lead.
# --question--
## --text--
What is Maria assuming about Tom?
## --answers--
Tom is the team lead.
---
Maria is the new graphic designer.
---
Tom is leaving the company.
---
Tom is the new graphic designer.
## --video-solution--
4

View File

@@ -28,17 +28,22 @@ const prerequisitesJoi = Joi.object().keys({
const schema = Joi.object()
.keys({
audioPath: Joi.string(),
block: Joi.string().regex(slugRE).required(),
blockId: Joi.objectId(),
challengeOrder: Joi.number(),
removeComments: Joi.bool().required(),
certification: Joi.string().regex(slugRE),
challengeType: Joi.number().min(0).max(20).required(),
challengeType: Joi.number().min(0).max(22).required(),
checksum: Joi.number(),
// TODO: require this only for normal challenges, not certs
dashedName: Joi.string().regex(slugRE),
description: Joi.when('challengeType', {
is: [challengeTypes.step, challengeTypes.video],
is: [
challengeTypes.step,
challengeTypes.video,
challengeTypes.fillInTheBlank
],
then: Joi.string().allow(''),
otherwise: Joi.string().required()
}),
@@ -55,6 +60,17 @@ const schema = Joi.object()
'C-Sharp'
),
videoUrl: Joi.string().allow(''),
fillInTheBlank: Joi.object().keys({
sentence: Joi.string().required(),
blanks: Joi.array()
.items(
Joi.object().keys({
answer: Joi.string().required(),
feedback: Joi.string().allow(null)
})
)
.required()
}),
forumTopicId: Joi.number(),
id: Joi.objectId().required(),
instructions: Joi.string().allow(''),
@@ -73,7 +89,7 @@ const schema = Joi.object()
}),
// video challenges only:
videoId: Joi.when('challengeType', {
is: challengeTypes.video,
is: [challengeTypes.video, challengeTypes.dialogue],
then: Joi.string().required()
}),
videoLocaleIds: Joi.when('challengeType', {
@@ -112,7 +128,11 @@ const schema = Joi.object()
crossDomain: Joi.bool()
})
),
assignments: Joi.array().items(Joi.string()),
assignments: Joi.when('challengeType', {
is: challengeTypes.dialogue,
then: Joi.array().items(Joi.string()).required(),
otherwise: Joi.array().items(Joi.string())
}),
solutions: Joi.array().items(Joi.array().items(fileJoi).min(1)),
superBlock: Joi.string().regex(slugWithSlashRE),
superOrder: Joi.number(),

View File

@@ -82,6 +82,7 @@ const directoryToSuperblock = {
'19-foundational-c-sharp-with-microsoft':
'foundational-c-sharp-with-microsoft',
'20-upcoming-python': 'upcoming-python',
'21-a2-english-for-developers': 'a2-english-for-developers',
'99-example-certification': 'example-certification'
};

View File

@@ -142,7 +142,7 @@ describe('getSuperBlockFromPath', () => {
);
it('handles all the directories in ./challenges/english', () => {
expect.assertions(21);
expect.assertions(22);
for (const directory of directories) {
expect(() => getSuperBlockFromDir(directory)).not.toThrow();
@@ -150,7 +150,7 @@ describe('getSuperBlockFromPath', () => {
});
it("returns valid superblocks (or 'certifications') for all valid arguments", () => {
expect.assertions(21);
expect.assertions(22);
const superBlockPaths = directories.filter(x => x !== '00-certifications');

View File

@@ -27,6 +27,7 @@ export enum Certification {
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
// Upcoming certifications
UpcomingPython = 'upcoming-python-v8',
A2English = 'a2-english-for-developers-v8',
// Legacy certifications
LegacyFrontEnd = 'legacy-front-end',
LegacyBackEnd = 'legacy-back-end',
@@ -70,7 +71,10 @@ export const legacyFullStackCertification = [
// "Upcoming" certifications are standard certifications that are not live unless
// showUpcomingChanges is true.
export const upcomingCertifications = [Certification.UpcomingPython] as const;
export const upcomingCertifications = [
Certification.UpcomingPython,
Certification.A2English
] as const;
export const certTypes = {
frontEnd: 'isFrontEndCert',

View File

@@ -20,6 +20,8 @@ const exam = 17;
const msTrophy = 18;
const multipleChoice = 19;
const python = 20;
const dialogue = 21;
const fillInTheBlank = 22;
export const challengeTypes = {
html,
@@ -43,7 +45,9 @@ export const challengeTypes = {
exam,
msTrophy,
multipleChoice,
python
python,
dialogue,
fillInTheBlank
};
export const isFinalProject = (challengeType: number) => {
@@ -71,7 +75,9 @@ export const isCodeAllyPractice = (challengeType: number) => {
export const hasNoTests = (challengeType: number): boolean =>
challengeType === multipleChoice ||
challengeType === theOdinProject ||
challengeType === video;
challengeType === video ||
challengeType === dialogue ||
challengeType === fillInTheBlank;
// determine the component view for each challenge
export const viewTypes = {
@@ -94,7 +100,9 @@ export const viewTypes = {
[exam]: 'exam',
[msTrophy]: 'msTrophy',
[multipleChoice]: 'odin',
[python]: 'modern'
[python]: 'modern',
[dialogue]: 'dialogue',
[fillInTheBlank]: 'fillInTheBlank'
};
// determine the type of submit function to use for the challenge on completion
@@ -122,5 +130,7 @@ export const submitTypes = {
[exam]: 'exam',
[msTrophy]: 'msTrophy',
[multipleChoice]: 'tests',
[python]: 'tests'
[python]: 'tests',
[dialogue]: 'tests',
[fillInTheBlank]: 'tests'
};

View File

@@ -21,7 +21,8 @@ export enum SuperBlocks {
CollegeAlgebraPy = 'college-algebra-with-python',
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
ExampleCertification = 'example-certification',
UpcomingPython = 'upcoming-python'
UpcomingPython = 'upcoming-python',
A2English = 'a2-english-for-developers'
}
/*
@@ -78,7 +79,8 @@ export const superBlockOrder: SuperBlockOrder = {
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.ExampleCertification,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
]
};
@@ -101,7 +103,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Chinese]: [
SuperBlocks.FoundationalCSharp,
@@ -109,7 +112,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.ChineseTraditional]: [
SuperBlocks.FoundationalCSharp,
@@ -117,23 +121,27 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Italian]: [
SuperBlocks.FoundationalCSharp,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Portuguese]: [
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Ukrainian]: [
SuperBlocks.CodingInterviewPrep,
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Japanese]: [
SuperBlocks.CollegeAlgebraPy,
@@ -141,7 +149,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.German]: [
SuperBlocks.RelationalDb,
@@ -154,7 +163,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Arabic]: [
SuperBlocks.DataVis,
@@ -171,7 +181,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.ProjectEuler,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.TheOdinProject,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
],
[Languages.Swahili]: [
SuperBlocks.DataVis,
@@ -191,7 +202,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
SuperBlocks.FrontEndDevLibs,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.JsAlgoDataStruct,
SuperBlocks.UpcomingPython
SuperBlocks.UpcomingPython,
SuperBlocks.A2English
]
};

View File

@@ -51,6 +51,7 @@ const superBlockFolderMap = {
'foundational-c-sharp-with-microsoft':
'19-foundational-c-sharp-with-microsoft',
'upcoming-python': '20-upcoming-python',
'a2-english-for-developers': '21-a2-english-for-developers',
'example-certification': '99-example-certification'
};

View File

@@ -23,6 +23,7 @@ export function getSuperBlockSubPath(superBlock: SuperBlocks): string {
[SuperBlocks.ProjectEuler]: '18-project-euler',
[SuperBlocks.FoundationalCSharp]: '19-foundational-c-sharp-with-microsoft',
[SuperBlocks.UpcomingPython]: '20-upcoming-python',
[SuperBlocks.A2English]: '21-a2-english-for-developers',
[SuperBlocks.ExampleCertification]: '99-example-certification'
};
return pathMap[superBlock];

View File

@@ -3,6 +3,7 @@ const frontmatter = require('remark-frontmatter');
const remark = require('remark-parse');
const { readSync } = require('to-vfile');
const unified = require('unified');
const addFillInTheBlank = require('./plugins/add-fill-in-the-blank');
const addFrontmatter = require('./plugins/add-frontmatter');
const addSeed = require('./plugins/add-seed');
const addSolution = require('./plugins/add-solution');
@@ -44,6 +45,7 @@ const processor = unified()
// 'directives' will be from text like the css selector :root. These should be
// converted back to text before they're added to the challenge object.
.use(restoreDirectives)
.use(addFillInTheBlank)
.use(addVideoQuestion)
.use(addAssignment)
.use(addTests)

View File

@@ -0,0 +1,61 @@
const { root } = require('mdast-builder');
const find = require('unist-util-find');
const getAllBetween = require('./utils/between-headings');
const getAllBefore = require('./utils/before-heading');
const mdastToHtml = require('./utils/mdast-to-html');
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
function plugin() {
return transformer;
function transformer(tree, file) {
const fillInTheBlankNodes = getAllBetween(tree, '--fillInTheBlank--');
if (fillInTheBlankNodes.length > 0) {
const fillInTheBlankTree = root(fillInTheBlankNodes);
const sentenceNodes = getAllBetween(fillInTheBlankTree, '--sentence--');
const blanksNodes = getAllBetween(fillInTheBlankTree, '--blanks--');
const fillInTheBlank = getfillInTheBlank(sentenceNodes, blanksNodes);
file.data.fillInTheBlank = fillInTheBlank;
}
}
}
function getfillInTheBlank(sentenceNodes, blanksNodes) {
const sentence = mdastToHtml(sentenceNodes);
const blanks = getBlanks(blanksNodes);
if (!sentence) throw Error('sentence is missing from fill in the blank');
if (!blanks) throw Error('blanks are missing from fill in the blank');
if (sentence.match(/_/g).length !== blanks.length)
throw Error(
`Number of underscores in sentence doesn't match the number of blanks`
);
return { sentence, blanks };
}
function getBlanks(blanksNodes) {
const blanksGroups = splitOnThematicBreak(blanksNodes);
return blanksGroups.map(blanksGroup => {
const blanksTree = root(blanksGroup);
const feedback = find(blanksTree, { value: '--feedback--' });
if (feedback) {
const blanksNodes = getAllBefore(blanksTree, '--feedback--');
const feedbackNodes = getAllBetween(blanksTree, '--feedback--');
return {
answer: blanksNodes[0].children[0].value,
feedback: mdastToHtml(feedbackNodes)
};
}
return { answer: blanksGroup[0].children[0].value, feedback: null };
});
}
module.exports = plugin;

View File

@@ -90,7 +90,8 @@ describe('external curriculum data build', () => {
'foundational-c-sharp-with-microsoft',
'the-odin-project',
'upcoming-python',
'example-certification'
'example-certification',
'a2-english-for-developers'
];
// TODO: this is a hack, we should have a single source of truth for the