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);