diff --git a/client/config/cert-and-project-map.ts b/client/config/cert-and-project-map.ts index d01c5036950..20aafb4de00 100644 --- a/client/config/cert-and-project-map.ts +++ b/client/config/cert-and-project-map.ts @@ -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; diff --git a/client/gatsby-node.js b/client/gatsby-node.js index e3913c64f2a..509bb00cb59 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -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); }; diff --git a/client/i18n/locales/english/intro.json b/client/i18n/locales/english/intro.json index adbddeb9d8e..0f305b210b5 100644 --- a/client/i18n/locales/english/intro.json +++ b/client/i18n/locales/english/intro.json @@ -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"], diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index fca773e23bf..790a6f710e5 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -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", diff --git a/client/src/assets/icons/superblock-icon.tsx b/client/src/assets/icons/superblock-icon.tsx index 37c230620d6..ef77a16fcc0 100644 --- a/client/src/assets/icons/superblock-icon.tsx +++ b/client/src/assets/icons/superblock-icon.tsx @@ -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 = { diff --git a/client/src/components/settings/certification.tsx b/client/src/components/settings/certification.tsx index 71942a3cc77..070b4127b75 100644 --- a/client/src/components/settings/certification.tsx +++ b/client/src/components/settings/certification.tsx @@ -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 }) ); diff --git a/client/src/pages/learn/a2-english-for-developers/index.md b/client/src/pages/learn/a2-english-for-developers/index.md new file mode 100644 index 00000000000..354a0255b4a --- /dev/null +++ b/client/src/pages/learn/a2-english-for-developers/index.md @@ -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 diff --git a/client/src/pages/learn/a2-english-for-developers/learn-conversation-starters-in-the-break-room/index.md b/client/src/pages/learn/a2-english-for-developers/learn-conversation-starters-in-the-break-room/index.md new file mode 100644 index 00000000000..4cf7cd99327 --- /dev/null +++ b/client/src/pages/learn/a2-english-for-developers/learn-conversation-starters-in-the-break-room/index.md @@ -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 diff --git a/client/src/pages/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/index.md b/client/src/pages/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/index.md new file mode 100644 index 00000000000..41f81ad70c8 --- /dev/null +++ b/client/src/pages/learn/a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/index.md @@ -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 diff --git a/client/src/pages/learn/a2-english-for-developers/learn-introductions-in-an-online-team-meeting/index.md b/client/src/pages/learn/a2-english-for-developers/learn-introductions-in-an-online-team-meeting/index.md new file mode 100644 index 00000000000..dedc7ce52a3 --- /dev/null +++ b/client/src/pages/learn/a2-english-for-developers/learn-introductions-in-an-online-team-meeting/index.md @@ -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 diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index d0c84819a56..07347c21a8b 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -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[]; diff --git a/client/src/templates/Challenges/dialogue/show.tsx b/client/src/templates/Challenges/dialogue/show.tsx new file mode 100644 index 00000000000..599270745e4 --- /dev/null +++ b/client/src/templates/Challenges/dialogue/show.tsx @@ -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 { + static displayName: string; + private container: React.RefObject = 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, + 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 ( + this.handleSubmit()} + containerRef={this.container} + nextChallengePath={nextChallengePath} + prevChallengePath={prevChallengePath} + > + + + + + {videoId && ( + + +
+ {!this.state.videoIsLoaded ? ( +
+ +
+ ) : null} + +
+ + )} + + +

{title}

+ + + +

{t('learn.assignments')}

+
+ {assignments.map((assignment, index) => ( + + ))} +
+ +
+ +
+ {!this.state.allAssignmentsCompleted && + assignments.length > 0 && ( + <> +
+ {t('learn.assignment-not-complete')} + + )} +
+ + + + + + + +
+
+
+
+ ); + } +} + +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 + } + } + } +`; diff --git a/client/src/templates/Challenges/fill-in-the-blank/show.css b/client/src/templates/Challenges/fill-in-the-blank/show.css new file mode 100644 index 00000000000..dc7723475dc --- /dev/null +++ b/client/src/templates/Challenges/fill-in-the-blank/show.css @@ -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; +} diff --git a/client/src/templates/Challenges/fill-in-the-blank/show.tsx b/client/src/templates/Challenges/fill-in-the-blank/show.tsx new file mode 100644 index 00000000000..175df668790 --- /dev/null +++ b/client/src/templates/Challenges/fill-in-the-blank/show.tsx @@ -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 = 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): 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}`; + if (index < numberOfBlanks) return `${str}`; + return `${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>$/g, '').split('_'); + const blankAnswers = blanks.map(b => b.answer); + + return ( + this.handleSubmit()} + containerRef={this.container} + nextChallengePath={nextChallengePath} + prevChallengePath={prevChallengePath} + > + + + + + + + {title} + + + + + {audioPath && ( + <> + + + {/* TODO: Add tracks for audio elements */} + {/* eslint-disable-next-line jsx-a11y/media-has-caption*/} + + + )} + + + +

{t('learn.fill-in-the-blank')}

+ + +
+

+ {splitSentence.map((s, i) => { + return ( + + + {blankAnswers[i] && ( + + )} + + ); + })} +

+
+
+ + {showFeedback && feedback && ( + <> + + + + )} +
+ {showWrong ? ( + {t('learn.wrong-answer')} + ) : ( + {t('learn.check-answer')} + )} +
+ + + + + + + + + + + + ); + } +} + +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 + } + } + } +`; diff --git a/client/src/templates/Challenges/odin/show.css b/client/src/templates/Challenges/odin/show.css index 8db116b5499..5fb261000c0 100644 --- a/client/src/templates/Challenges/odin/show.css +++ b/client/src/templates/Challenges/odin/show.css @@ -26,3 +26,7 @@ input[type='checkbox']::before { input[type='checkbox']:checked::before { transform: scale(1); } + +audio { + background-color: aqua; +} diff --git a/client/src/templates/Challenges/odin/show.tsx b/client/src/templates/Challenges/odin/show.tsx index d8d44cd1994..51fea334f82 100644 --- a/client/src/templates/Challenges/odin/show.tsx +++ b/client/src/templates/Challenges/odin/show.tsx @@ -216,7 +216,8 @@ class ShowOdin extends Component { bilibiliIds, fields: { blockName }, question: { text, answers, solution }, - assignments + assignments, + audioPath } } }, @@ -276,7 +277,21 @@ class ShowOdin extends Component {

{title}

- + {audioPath && ( + <> + + + {/* TODO: Add tracks for audio elements */} + {/* eslint-disable-next-line jsx-a11y/media-has-caption*/} + + + + )} {assignments.length > 0 && ( <> @@ -443,6 +458,7 @@ export const query = graphql` } translationPending assignments + audioPath } } } diff --git a/client/utils/gatsby/challenge-page-creator.js b/client/utils/gatsby/challenge-page-creator.js index d16e90b22b8..d88e283ef5a 100644 --- a/client/utils/gatsby/challenge-page-creator.js +++ b/client/utils/gatsby/challenge-page-creator.js @@ -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 }; diff --git a/curriculum/challenges/_meta/learn-conversation-starters-in-the-break-room/meta.json b/curriculum/challenges/_meta/learn-conversation-starters-in-the-break-room/meta.json new file mode 100644 index 00000000000..6407cd41306 --- /dev/null +++ b/curriculum/challenges/_meta/learn-conversation-starters-in-the-break-room/meta.json @@ -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" + } \ No newline at end of file diff --git a/curriculum/challenges/_meta/learn-greetings-in-your-first-day-at-the-office/meta.json b/curriculum/challenges/_meta/learn-greetings-in-your-first-day-at-the-office/meta.json new file mode 100644 index 00000000000..9796924fbc4 --- /dev/null +++ b/curriculum/challenges/_meta/learn-greetings-in-your-first-day-at-the-office/meta.json @@ -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" +} \ No newline at end of file diff --git a/curriculum/challenges/_meta/learn-introductions-in-an-online-team-meeting/meta.json b/curriculum/challenges/_meta/learn-introductions-in-an-online-team-meeting/meta.json new file mode 100644 index 00000000000..232bb9c1845 --- /dev/null +++ b/curriculum/challenges/_meta/learn-introductions-in-an-online-team-meeting/meta.json @@ -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" + } \ No newline at end of file diff --git a/curriculum/challenges/english/00-certifications/a2-english-for-developers-certification/a2-english-for-developers-certification.yml b/curriculum/challenges/english/00-certifications/a2-english-for-developers-certification/a2-english-for-developers-certification.yml new file mode 100644 index 00000000000..832dd23b1e6 --- /dev/null +++ b/curriculum/challenges/english/00-certifications/a2-english-for-developers-certification/a2-english-for-developers-certification.yml @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-1.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-1.md new file mode 100644 index 00000000000..7f7f1c0b68f --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-1.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-2.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-2.md new file mode 100644 index 00000000000..73366b5e00d --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-2.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-3.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-3.md new file mode 100644 index 00000000000..fd43bcb1f4e --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-conversation-starters-in-the-break-room/challenge-3.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/dialogue-introducing.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/dialogue-introducing.md new file mode 100644 index 00000000000..13028d7c28a --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/dialogue-introducing.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/i-am-im.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/i-am-im.md new file mode 100644 index 00000000000..298f94aab43 --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/i-am-im.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/im.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/im.md new file mode 100644 index 00000000000..ae1eb96b788 --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/im.md @@ -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` diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/name-and-job-title.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/name-and-job-title.md new file mode 100644 index 00000000000..86d16e620eb --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/name-and-job-title.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/right.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/right.md new file mode 100644 index 00000000000..d08709a07dd --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/right.md @@ -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. diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/team-lead.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/team-lead.md new file mode 100644 index 00000000000..7a2f7f9669e --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/team-lead.md @@ -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. diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/thats-right-2.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/thats-right-2.md new file mode 100644 index 00000000000..9174ab0778d --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/thats-right-2.md @@ -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` diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/thats-right.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/thats-right.md new file mode 100644 index 00000000000..360883cc3cc --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/thats-right.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/you-are.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/you-are.md new file mode 100644 index 00000000000..dd254987129 --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/you-are.md @@ -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.` diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-1.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-1.md new file mode 100644 index 00000000000..f19036232e5 --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-1.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-2.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-2.md new file mode 100644 index 00000000000..4b5e09d7c59 --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-2.md @@ -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 diff --git a/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-3.md b/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-3.md new file mode 100644 index 00000000000..b5d34c2fce7 --- /dev/null +++ b/curriculum/challenges/english/21-a2-english-for-developers/learn-introductions-in-an-online-team-meeting/challenge-3.md @@ -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 diff --git a/curriculum/schema/challenge-schema.js b/curriculum/schema/challenge-schema.js index 966a77a9cd8..53fdcf8a97a 100644 --- a/curriculum/schema/challenge-schema.js +++ b/curriculum/schema/challenge-schema.js @@ -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(), diff --git a/curriculum/utils.js b/curriculum/utils.js index 957703f660b..f83057c5a91 100644 --- a/curriculum/utils.js +++ b/curriculum/utils.js @@ -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' }; diff --git a/curriculum/utils.test.ts b/curriculum/utils.test.ts index efb9fe3880b..7a53d002a5f 100644 --- a/curriculum/utils.test.ts +++ b/curriculum/utils.test.ts @@ -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'); diff --git a/shared/config/certification-settings.ts b/shared/config/certification-settings.ts index 942621118cd..52bd65b06f0 100644 --- a/shared/config/certification-settings.ts +++ b/shared/config/certification-settings.ts @@ -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', diff --git a/shared/config/challenge-types.ts b/shared/config/challenge-types.ts index c1993d9993c..b3095f02068 100644 --- a/shared/config/challenge-types.ts +++ b/shared/config/challenge-types.ts @@ -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' }; diff --git a/shared/config/superblocks.ts b/shared/config/superblocks.ts index 9aeec46704a..67c4817aaa1 100644 --- a/shared/config/superblocks.ts +++ b/shared/config/superblocks.ts @@ -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 ] }; diff --git a/tools/challenge-auditor/index.ts b/tools/challenge-auditor/index.ts index 7998d96fe64..a0eaca7a69a 100644 --- a/tools/challenge-auditor/index.ts +++ b/tools/challenge-auditor/index.ts @@ -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' }; diff --git a/tools/challenge-helper-scripts/fs-utils.ts b/tools/challenge-helper-scripts/fs-utils.ts index 0fe8ebe68b1..d1e018a5d69 100644 --- a/tools/challenge-helper-scripts/fs-utils.ts +++ b/tools/challenge-helper-scripts/fs-utils.ts @@ -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]; diff --git a/tools/challenge-parser/parser/index.js b/tools/challenge-parser/parser/index.js index 1645fc253c6..2b82f653297 100644 --- a/tools/challenge-parser/parser/index.js +++ b/tools/challenge-parser/parser/index.js @@ -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) diff --git a/tools/challenge-parser/parser/plugins/add-fill-in-the-blank.js b/tools/challenge-parser/parser/plugins/add-fill-in-the-blank.js new file mode 100644 index 00000000000..b15cdaa055a --- /dev/null +++ b/tools/challenge-parser/parser/plugins/add-fill-in-the-blank.js @@ -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; diff --git a/tools/scripts/build/build-external-curricula-data.test.ts b/tools/scripts/build/build-external-curricula-data.test.ts index b91b04586fd..a7be6a5e69c 100644 --- a/tools/scripts/build/build-external-curricula-data.test.ts +++ b/tools/scripts/build/build-external-curricula-data.test.ts @@ -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