From 1c5d136add18d164857cbec0bc103b76f336dd8d Mon Sep 17 00:00:00 2001 From: Tom <20648924+moT01@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:42:03 -0600 Subject: [PATCH] feat(client): add notes tab to project based curriculum (#44247) * feat: add notes tab to project based curriculum * feat: add console key to i18n * feat: add reset to i18n * fix: use translations in action-row * fix: use hasEditableBoundaries as check for when to display instructions/editor-tabs * fix: clean up notes components and use prism formatting * feat: add notes to docs/how-to-work-on-challenges * revert: unused code * fix: lint errors? * fix: lint errors * fix: add notes to graphql schema * fix: add notes to challenge schema * fix: only display notes on project based * fix: add env data back to mobile layout * fix: prettify * revert: notes * fix: hide notes on mobile for non project based * rename: switchDisplayTab -> togglePane * revert: hasEditableBoundaries check back to projectBasedChallenge check --- client/gatsby-node.js | 1 + .../chinese-traditional/translations.json | 4 ++ client/i18n/locales/chinese/translations.json | 4 ++ client/i18n/locales/english/translations.json | 4 ++ client/i18n/locales/espanol/translations.json | 4 ++ client/i18n/locales/italian/translations.json | 4 ++ .../i18n/locales/portuguese/translations.json | 4 ++ client/src/redux/prop-types.ts | 1 + .../Challenges/classic/action-row.tsx | 30 ++++++++++---- .../Challenges/classic/desktop-layout.tsx | 32 ++++++++++++--- .../Challenges/classic/mobile-layout.tsx | 39 ++++++++++++++----- .../src/templates/Challenges/classic/show.tsx | 18 ++++++++- .../templates/Challenges/components/notes.tsx | 14 +++++++ curriculum/schema/challengeSchema.js | 1 + docs/how-to-work-on-coding-challenges.md | 4 ++ tools/challenge-parser/parser/index.js | 3 +- 16 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 client/src/templates/Challenges/components/notes.tsx diff --git a/client/gatsby-node.js b/client/gatsby-node.js index ced7ee51953..b7d96010ef8 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -257,6 +257,7 @@ exports.createSchemaCustomization = ({ actions }) => { const typeDefs = ` type ChallengeNode implements Node { challengeFiles: [FileContents] + notes: String url: String } type FileContents { diff --git a/client/i18n/locales/chinese-traditional/translations.json b/client/i18n/locales/chinese-traditional/translations.json index 4f129bcf41c..ca3e4cbfa13 100644 --- a/client/i18n/locales/chinese-traditional/translations.json +++ b/client/i18n/locales/chinese-traditional/translations.json @@ -287,6 +287,10 @@ "info": "信息", "code": "編程", "tests": "測試", + "restart": "Restart", + "restart-step": "Restart Step", + "console": "Console", + "notes": "Notes", "preview": "預覽" }, "help-translate": "我們仍然在翻譯以下證書。", diff --git a/client/i18n/locales/chinese/translations.json b/client/i18n/locales/chinese/translations.json index cca9d43ad67..1aecfe5bf6d 100644 --- a/client/i18n/locales/chinese/translations.json +++ b/client/i18n/locales/chinese/translations.json @@ -287,6 +287,10 @@ "info": "信息", "code": "编程", "tests": "测试", + "restart": "Restart", + "restart-step": "Restart Step", + "console": "Console", + "notes": "Notes", "preview": "预览" }, "help-translate": "我们仍然在翻译以下证书。", diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 533ce91d154..2a4ac07f1f1 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -287,6 +287,10 @@ "info": "Info", "code": "Code", "tests": "Tests", + "restart": "Restart", + "restart-step": "Restart Step", + "console": "Console", + "notes": "Notes", "preview": "Preview" }, "help-translate": "We are still translating the following certifications.", diff --git a/client/i18n/locales/espanol/translations.json b/client/i18n/locales/espanol/translations.json index b7129dac531..28437f4ab0c 100644 --- a/client/i18n/locales/espanol/translations.json +++ b/client/i18n/locales/espanol/translations.json @@ -287,6 +287,10 @@ "info": "Info", "code": "Código", "tests": "Pruebas", + "restart": "Restart", + "restart-step": "Restart Step", + "console": "Console", + "notes": "Notes", "preview": "Vista" }, "help-translate": "Todavía estamos traduciendo las siguientes certificaciones.", diff --git a/client/i18n/locales/italian/translations.json b/client/i18n/locales/italian/translations.json index e31524f5a19..603eb02bd9a 100644 --- a/client/i18n/locales/italian/translations.json +++ b/client/i18n/locales/italian/translations.json @@ -287,6 +287,10 @@ "info": "Informazioni", "code": "Codice", "tests": "Test", + "restart": "Restart", + "restart-step": "Restart Step", + "console": "Console", + "notes": "Notes", "preview": "Anteprima" }, "help-translate": "Stiamo ancora traducendo le seguenti certificazioni.", diff --git a/client/i18n/locales/portuguese/translations.json b/client/i18n/locales/portuguese/translations.json index 3f4f1802b00..29965ec82ff 100644 --- a/client/i18n/locales/portuguese/translations.json +++ b/client/i18n/locales/portuguese/translations.json @@ -287,6 +287,10 @@ "info": "Informações", "code": "Código", "tests": "Testes", + "restart": "Restart", + "restart-step": "Restart Step", + "console": "Console", + "notes": "Notes", "preview": "Pré-visualizar" }, "help-translate": "Ainda estamos traduzindo as certificações a seguir.", diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index 6a6ce48c78b..65af9e912c1 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -152,6 +152,7 @@ export type ChallengeNode = { owner: string; type: string; }; + notes: string; removeComments: boolean; isLocked: boolean; isPrivate: boolean; diff --git a/client/src/templates/Challenges/classic/action-row.tsx b/client/src/templates/Challenges/classic/action-row.tsx index 2a4bbd06838..a136595eb46 100644 --- a/client/src/templates/Challenges/classic/action-row.tsx +++ b/client/src/templates/Challenges/classic/action-row.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import BreadCrumb from '../components/bread-crumb'; import { resetChallenge } from '../redux'; @@ -6,11 +7,12 @@ import EditorTabs from './editor-tabs'; interface ActionRowProps { block: string; + hasNotes: boolean; showConsole: boolean; - showNotes?: boolean; + showNotes: boolean; showPreview: boolean; superBlock: string; - switchDisplayTab: (displayTab: string) => void; + togglePane: (pane: string) => void; resetChallenge: () => void; } @@ -19,13 +21,16 @@ const mapDispatchToProps = { }; const ActionRow = ({ - switchDisplayTab, + hasNotes, + togglePane, + showNotes, showPreview, showConsole, superBlock, block, resetChallenge }: ActionRowProps): JSX.Element => { + const { t } = useTranslation(); return (
@@ -38,22 +43,31 @@ const ActionRow = ({ onClick={resetChallenge} role='tab' > - Restart Step + {t('learn.editor-tabs.restart-step')}
+ {hasNotes && ( + + )}
diff --git a/client/src/templates/Challenges/classic/desktop-layout.tsx b/client/src/templates/Challenges/classic/desktop-layout.tsx index 0e11011bc73..d62e66189c9 100644 --- a/client/src/templates/Challenges/classic/desktop-layout.tsx +++ b/client/src/templates/Challenges/classic/desktop-layout.tsx @@ -1,8 +1,8 @@ import { first } from 'lodash-es'; import React, { useState, ReactElement } from 'react'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; -import envData from '../../../../../config/env.json'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles'; +import envData from '../../../../../config/env.json'; import { ChallengeFile, ChallengeFiles, @@ -19,15 +19,18 @@ interface DesktopLayoutProps { challengeFiles: ChallengeFiles; editor: ReactElement | null; hasEditableBoundaries: boolean; + hasNotes: boolean; hasPreview: boolean; instructions: ReactElement; layoutState: { codePane: Pane; editorPane: Pane; instructionPane: Pane; + notesPane: Pane; previewPane: Pane; testsPane: Pane; }; + notes: ReactElement; preview: ReactElement; resizeProps: ResizeProps; superBlock: string; @@ -43,8 +46,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { const [showPreview, setShowPreview] = useState(true); const [showConsole, setShowConsole] = useState(false); - const switchDisplayTab = (displayTab: string): void => { - switch (displayTab) { + const togglePane = (pane: string): void => { + switch (pane) { case 'showPreview': setShowPreview(!showPreview); break; @@ -72,8 +75,10 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { instructions, editor, testOutput, + hasNotes, hasPreview, layoutState, + notes, preview, hasEditableBoundaries, superBlock @@ -84,20 +89,28 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { const displayPreview = projectBasedChallenge ? showPreview && hasPreview : hasPreview; + const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false; const displayConsole = projectBasedChallenge ? showConsole : true; - const { codePane, editorPane, instructionPane, previewPane, testsPane } = - layoutState; + const { + codePane, + editorPane, + instructionPane, + notesPane, + previewPane, + testsPane + } = layoutState; return (
{projectBasedChallenge && ( )} @@ -138,6 +151,13 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { )} + {displayNotes && } + {displayNotes && ( + + {notes} + + )} + {displayPreview && } {displayPreview && ( diff --git a/client/src/templates/Challenges/classic/mobile-layout.tsx b/client/src/templates/Challenges/classic/mobile-layout.tsx index f278b7ded3e..7c1881e76dc 100644 --- a/client/src/templates/Challenges/classic/mobile-layout.tsx +++ b/client/src/templates/Challenges/classic/mobile-layout.tsx @@ -1,6 +1,6 @@ import { TabPane, Tabs } from '@freecodecamp/react-bootstrap'; import i18next from 'i18next'; -import React, { Component } from 'react'; +import React, { Component, ReactElement } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; @@ -27,9 +27,12 @@ interface MobileLayoutProps { currentTab: number; editor: JSX.Element | null; guideUrl: string; + hasEditableBoundaries: boolean; + hasNotes: boolean; hasPreview: boolean; instructions: JSX.Element; moveToTab: typeof moveToTab; + notes: ReactElement; preview: JSX.Element; testOutput: JSX.Element; videoUrl: string; @@ -44,10 +47,13 @@ class MobileLayout extends Component { const { currentTab, moveToTab, + hasEditableBoundaries, instructions, editor, testOutput, + hasNotes, hasPreview, + notes, preview, guideUrl, videoUrl, @@ -61,7 +67,9 @@ class MobileLayout extends Component { // Unlike the desktop layout the mobile version does not have an ActionRow, // but still needs a way to switch between the different tabs. - const displayEditorTabs = showUpcomingChanges && usesMultifileEditor; + const projectBasedChallenge = showUpcomingChanges && usesMultifileEditor; + + const eventKeys = [1, 2, 3, 4, 5]; return ( <> @@ -71,27 +79,40 @@ class MobileLayout extends Component { id='challenge-page-tabs' onSelect={moveToTab} > - - {instructions} - + {!hasEditableBoundaries && ( + + {instructions} + + )} - {displayEditorTabs && } + {projectBasedChallenge && } {editor} {testOutput} + {hasNotes && projectBasedChallenge && ( + + {notes} + + )} {hasPreview && ( {preview} diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index 47e8173635a..1173f938483 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -9,8 +9,8 @@ import { bindActionCreators, Dispatch } from 'redux'; import { createStructuredSelector } from 'reselect'; import store from 'store'; import { challengeTypes } from '../../../../utils/challenge-types'; - import LearnLayout from '../../../components/layouts/learn'; + import { ChallengeFile, ChallengeFiles, @@ -26,6 +26,7 @@ import ResetModal from '../components/ResetModal'; import ChallengeTitle from '../components/challenge-title'; import CompletionModal from '../components/completion-modal'; import HelpModal from '../components/help-modal'; +import Notes from '../components/notes'; import Output from '../components/output'; import Preview from '../components/preview'; import ProjectPreviewModal, { @@ -115,6 +116,7 @@ interface ReflexLayout { codePane: { flex: number }; editorPane: { flex: number }; instructionPane: { flex: number }; + notesPane: { flex: number }; previewPane: { flex: number }; testsPane: { flex: number }; } @@ -126,6 +128,7 @@ const BASE_LAYOUT = { editorPane: { flex: 1 }, instructionPane: { flex: 1 }, previewPane: { flex: 0.7 }, + notesPane: { flex: 0.7 }, testsPane: { flex: 0.25 } }; @@ -371,6 +374,10 @@ class ShowClassic extends Component { ); } + renderNotes(notes?: string) { + return ; + } + renderPreview() { return ( { forumTopicId, superBlock, title, - usesMultifileEditor + usesMultifileEditor, + notes } = this.getChallenge(); const { executeChallenge, @@ -425,10 +433,13 @@ class ShowClassic extends Component { { challengeFiles={challengeFiles} editor={this.renderEditor()} hasEditableBoundaries={this.hasEditableBoundaries()} + hasNotes={!!notes} hasPreview={this.hasPreview()} instructions={this.renderInstructionsPanel({ showToolPanel: true })} layoutState={this.state.layout} + notes={this.renderNotes(notes)} preview={this.renderPreview()} resizeProps={this.resizeProps} superBlock={superBlock} @@ -481,6 +494,7 @@ export const query = graphql` title description instructions + notes removeComments challengeType helpCategory diff --git a/client/src/templates/Challenges/components/notes.tsx b/client/src/templates/Challenges/components/notes.tsx new file mode 100644 index 00000000000..17f53626650 --- /dev/null +++ b/client/src/templates/Challenges/components/notes.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import PrismFormatted from './prism-formatted'; + +interface NotesProps { + notes?: string; +} + +function Notes({ notes }: NotesProps): JSX.Element { + return <>{notes && }; +} + +Notes.displayName = 'Notes'; + +export default Notes; diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index de8016ea638..a086146c1cf 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -52,6 +52,7 @@ const schema = Joi.object() isComingSoon: Joi.bool(), isLocked: Joi.bool(), isPrivate: Joi.bool(), + notes: Joi.string().allow(''), order: Joi.number(), // video challenges only: videoId: Joi.when('challengeType', { diff --git a/docs/how-to-work-on-coding-challenges.md b/docs/how-to-work-on-coding-challenges.md index 5292a7f90e7..22fcd314709 100644 --- a/docs/how-to-work-on-coding-challenges.md +++ b/docs/how-to-work-on-coding-challenges.md @@ -73,6 +73,10 @@ assert.equal( ); ``` +# --notes-- + +Extra information for a challenge, in markdown + # --seed-- ## --before-user-code-- diff --git a/tools/challenge-parser/parser/index.js b/tools/challenge-parser/parser/index.js index 17fa9e6d78a..c2c6fd82447 100644 --- a/tools/challenge-parser/parser/index.js +++ b/tools/challenge-parser/parser/index.js @@ -45,12 +45,13 @@ const processor = unified() .use(restoreDirectives) .use(addVideoQuestion) .use(addTests) - .use(addText, ['description', 'instructions']); + .use(addText, ['description', 'instructions', 'notes']); exports.parseMD = function parseMD(filename) { return new Promise((resolve, reject) => { const file = readSync(filename); const tree = processor.parse(file); + processor.run(tree, file, function (err, node, file) { if (!err) { resolve(file.data);