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
This commit is contained in:
@ -257,6 +257,7 @@ exports.createSchemaCustomization = ({ actions }) => {
|
|||||||
const typeDefs = `
|
const typeDefs = `
|
||||||
type ChallengeNode implements Node {
|
type ChallengeNode implements Node {
|
||||||
challengeFiles: [FileContents]
|
challengeFiles: [FileContents]
|
||||||
|
notes: String
|
||||||
url: String
|
url: String
|
||||||
}
|
}
|
||||||
type FileContents {
|
type FileContents {
|
||||||
|
@ -287,6 +287,10 @@
|
|||||||
"info": "信息",
|
"info": "信息",
|
||||||
"code": "編程",
|
"code": "編程",
|
||||||
"tests": "測試",
|
"tests": "測試",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restart-step": "Restart Step",
|
||||||
|
"console": "Console",
|
||||||
|
"notes": "Notes",
|
||||||
"preview": "預覽"
|
"preview": "預覽"
|
||||||
},
|
},
|
||||||
"help-translate": "我們仍然在翻譯以下證書。",
|
"help-translate": "我們仍然在翻譯以下證書。",
|
||||||
|
@ -287,6 +287,10 @@
|
|||||||
"info": "信息",
|
"info": "信息",
|
||||||
"code": "编程",
|
"code": "编程",
|
||||||
"tests": "测试",
|
"tests": "测试",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restart-step": "Restart Step",
|
||||||
|
"console": "Console",
|
||||||
|
"notes": "Notes",
|
||||||
"preview": "预览"
|
"preview": "预览"
|
||||||
},
|
},
|
||||||
"help-translate": "我们仍然在翻译以下证书。",
|
"help-translate": "我们仍然在翻译以下证书。",
|
||||||
|
@ -287,6 +287,10 @@
|
|||||||
"info": "Info",
|
"info": "Info",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
"tests": "Tests",
|
"tests": "Tests",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restart-step": "Restart Step",
|
||||||
|
"console": "Console",
|
||||||
|
"notes": "Notes",
|
||||||
"preview": "Preview"
|
"preview": "Preview"
|
||||||
},
|
},
|
||||||
"help-translate": "We are still translating the following certifications.",
|
"help-translate": "We are still translating the following certifications.",
|
||||||
|
@ -287,6 +287,10 @@
|
|||||||
"info": "Info",
|
"info": "Info",
|
||||||
"code": "Código",
|
"code": "Código",
|
||||||
"tests": "Pruebas",
|
"tests": "Pruebas",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restart-step": "Restart Step",
|
||||||
|
"console": "Console",
|
||||||
|
"notes": "Notes",
|
||||||
"preview": "Vista"
|
"preview": "Vista"
|
||||||
},
|
},
|
||||||
"help-translate": "Todavía estamos traduciendo las siguientes certificaciones.",
|
"help-translate": "Todavía estamos traduciendo las siguientes certificaciones.",
|
||||||
|
@ -287,6 +287,10 @@
|
|||||||
"info": "Informazioni",
|
"info": "Informazioni",
|
||||||
"code": "Codice",
|
"code": "Codice",
|
||||||
"tests": "Test",
|
"tests": "Test",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restart-step": "Restart Step",
|
||||||
|
"console": "Console",
|
||||||
|
"notes": "Notes",
|
||||||
"preview": "Anteprima"
|
"preview": "Anteprima"
|
||||||
},
|
},
|
||||||
"help-translate": "Stiamo ancora traducendo le seguenti certificazioni.",
|
"help-translate": "Stiamo ancora traducendo le seguenti certificazioni.",
|
||||||
|
@ -287,6 +287,10 @@
|
|||||||
"info": "Informações",
|
"info": "Informações",
|
||||||
"code": "Código",
|
"code": "Código",
|
||||||
"tests": "Testes",
|
"tests": "Testes",
|
||||||
|
"restart": "Restart",
|
||||||
|
"restart-step": "Restart Step",
|
||||||
|
"console": "Console",
|
||||||
|
"notes": "Notes",
|
||||||
"preview": "Pré-visualizar"
|
"preview": "Pré-visualizar"
|
||||||
},
|
},
|
||||||
"help-translate": "Ainda estamos traduzindo as certificações a seguir.",
|
"help-translate": "Ainda estamos traduzindo as certificações a seguir.",
|
||||||
|
@ -152,6 +152,7 @@ export type ChallengeNode = {
|
|||||||
owner: string;
|
owner: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
notes: string;
|
||||||
removeComments: boolean;
|
removeComments: boolean;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import BreadCrumb from '../components/bread-crumb';
|
import BreadCrumb from '../components/bread-crumb';
|
||||||
import { resetChallenge } from '../redux';
|
import { resetChallenge } from '../redux';
|
||||||
@ -6,11 +7,12 @@ import EditorTabs from './editor-tabs';
|
|||||||
|
|
||||||
interface ActionRowProps {
|
interface ActionRowProps {
|
||||||
block: string;
|
block: string;
|
||||||
|
hasNotes: boolean;
|
||||||
showConsole: boolean;
|
showConsole: boolean;
|
||||||
showNotes?: boolean;
|
showNotes: boolean;
|
||||||
showPreview: boolean;
|
showPreview: boolean;
|
||||||
superBlock: string;
|
superBlock: string;
|
||||||
switchDisplayTab: (displayTab: string) => void;
|
togglePane: (pane: string) => void;
|
||||||
resetChallenge: () => void;
|
resetChallenge: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,13 +21,16 @@ const mapDispatchToProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ActionRow = ({
|
const ActionRow = ({
|
||||||
switchDisplayTab,
|
hasNotes,
|
||||||
|
togglePane,
|
||||||
|
showNotes,
|
||||||
showPreview,
|
showPreview,
|
||||||
showConsole,
|
showConsole,
|
||||||
superBlock,
|
superBlock,
|
||||||
block,
|
block,
|
||||||
resetChallenge
|
resetChallenge
|
||||||
}: ActionRowProps): JSX.Element => {
|
}: ActionRowProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className='action-row'>
|
<div className='action-row'>
|
||||||
<div className='breadcrumbs-demo'>
|
<div className='breadcrumbs-demo'>
|
||||||
@ -38,22 +43,31 @@ const ActionRow = ({
|
|||||||
onClick={resetChallenge}
|
onClick={resetChallenge}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
Restart Step
|
{t('learn.editor-tabs.restart-step')}
|
||||||
</button>
|
</button>
|
||||||
<div className='panel-display-tabs'>
|
<div className='panel-display-tabs'>
|
||||||
<button
|
<button
|
||||||
className={showConsole ? 'active-tab' : ''}
|
className={showConsole ? 'active-tab' : ''}
|
||||||
onClick={() => switchDisplayTab('showConsole')}
|
onClick={() => togglePane('showConsole')}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
JS Console
|
{t('learn.editor-tabs.console')}
|
||||||
</button>
|
</button>
|
||||||
|
{hasNotes && (
|
||||||
|
<button
|
||||||
|
className={showNotes ? 'active-tab' : ''}
|
||||||
|
onClick={() => togglePane('showNotes')}
|
||||||
|
role='tab'
|
||||||
|
>
|
||||||
|
{t('learn.editor-tabs.notes')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className={showPreview ? 'active-tab' : ''}
|
className={showPreview ? 'active-tab' : ''}
|
||||||
onClick={() => switchDisplayTab('showPreview')}
|
onClick={() => togglePane('showPreview')}
|
||||||
role='tab'
|
role='tab'
|
||||||
>
|
>
|
||||||
Show Preview
|
{t('learn.editor-tabs.preview')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { first } from 'lodash-es';
|
import { first } from 'lodash-es';
|
||||||
import React, { useState, ReactElement } from 'react';
|
import React, { useState, ReactElement } from 'react';
|
||||||
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
|
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
|
||||||
import envData from '../../../../../config/env.json';
|
|
||||||
import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
|
import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
|
||||||
|
import envData from '../../../../../config/env.json';
|
||||||
import {
|
import {
|
||||||
ChallengeFile,
|
ChallengeFile,
|
||||||
ChallengeFiles,
|
ChallengeFiles,
|
||||||
@ -19,15 +19,18 @@ interface DesktopLayoutProps {
|
|||||||
challengeFiles: ChallengeFiles;
|
challengeFiles: ChallengeFiles;
|
||||||
editor: ReactElement | null;
|
editor: ReactElement | null;
|
||||||
hasEditableBoundaries: boolean;
|
hasEditableBoundaries: boolean;
|
||||||
|
hasNotes: boolean;
|
||||||
hasPreview: boolean;
|
hasPreview: boolean;
|
||||||
instructions: ReactElement;
|
instructions: ReactElement;
|
||||||
layoutState: {
|
layoutState: {
|
||||||
codePane: Pane;
|
codePane: Pane;
|
||||||
editorPane: Pane;
|
editorPane: Pane;
|
||||||
instructionPane: Pane;
|
instructionPane: Pane;
|
||||||
|
notesPane: Pane;
|
||||||
previewPane: Pane;
|
previewPane: Pane;
|
||||||
testsPane: Pane;
|
testsPane: Pane;
|
||||||
};
|
};
|
||||||
|
notes: ReactElement;
|
||||||
preview: ReactElement;
|
preview: ReactElement;
|
||||||
resizeProps: ResizeProps;
|
resizeProps: ResizeProps;
|
||||||
superBlock: string;
|
superBlock: string;
|
||||||
@ -43,8 +46,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
|||||||
const [showPreview, setShowPreview] = useState(true);
|
const [showPreview, setShowPreview] = useState(true);
|
||||||
const [showConsole, setShowConsole] = useState(false);
|
const [showConsole, setShowConsole] = useState(false);
|
||||||
|
|
||||||
const switchDisplayTab = (displayTab: string): void => {
|
const togglePane = (pane: string): void => {
|
||||||
switch (displayTab) {
|
switch (pane) {
|
||||||
case 'showPreview':
|
case 'showPreview':
|
||||||
setShowPreview(!showPreview);
|
setShowPreview(!showPreview);
|
||||||
break;
|
break;
|
||||||
@ -72,8 +75,10 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
|||||||
instructions,
|
instructions,
|
||||||
editor,
|
editor,
|
||||||
testOutput,
|
testOutput,
|
||||||
|
hasNotes,
|
||||||
hasPreview,
|
hasPreview,
|
||||||
layoutState,
|
layoutState,
|
||||||
|
notes,
|
||||||
preview,
|
preview,
|
||||||
hasEditableBoundaries,
|
hasEditableBoundaries,
|
||||||
superBlock
|
superBlock
|
||||||
@ -84,20 +89,28 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
|||||||
const displayPreview = projectBasedChallenge
|
const displayPreview = projectBasedChallenge
|
||||||
? showPreview && hasPreview
|
? showPreview && hasPreview
|
||||||
: hasPreview;
|
: hasPreview;
|
||||||
|
const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false;
|
||||||
const displayConsole = projectBasedChallenge ? showConsole : true;
|
const displayConsole = projectBasedChallenge ? showConsole : true;
|
||||||
const { codePane, editorPane, instructionPane, previewPane, testsPane } =
|
const {
|
||||||
layoutState;
|
codePane,
|
||||||
|
editorPane,
|
||||||
|
instructionPane,
|
||||||
|
notesPane,
|
||||||
|
previewPane,
|
||||||
|
testsPane
|
||||||
|
} = layoutState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='desktop-layout'>
|
<div className='desktop-layout'>
|
||||||
{projectBasedChallenge && (
|
{projectBasedChallenge && (
|
||||||
<ActionRow
|
<ActionRow
|
||||||
block={block}
|
block={block}
|
||||||
|
hasNotes={hasNotes}
|
||||||
showConsole={showConsole}
|
showConsole={showConsole}
|
||||||
showNotes={showNotes}
|
showNotes={showNotes}
|
||||||
showPreview={showPreview}
|
showPreview={showPreview}
|
||||||
superBlock={superBlock}
|
superBlock={superBlock}
|
||||||
switchDisplayTab={switchDisplayTab}
|
togglePane={togglePane}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ReflexContainer orientation='vertical'>
|
<ReflexContainer orientation='vertical'>
|
||||||
@ -138,6 +151,13 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
|||||||
</ReflexContainer>
|
</ReflexContainer>
|
||||||
)}
|
)}
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
|
{displayNotes && <ReflexSplitter propagate={true} {...resizeProps} />}
|
||||||
|
{displayNotes && (
|
||||||
|
<ReflexElement flex={notesPane.flex} {...resizeProps}>
|
||||||
|
{notes}
|
||||||
|
</ReflexElement>
|
||||||
|
)}
|
||||||
|
|
||||||
{displayPreview && <ReflexSplitter propagate={true} {...resizeProps} />}
|
{displayPreview && <ReflexSplitter propagate={true} {...resizeProps} />}
|
||||||
{displayPreview && (
|
{displayPreview && (
|
||||||
<ReflexElement flex={previewPane.flex} {...resizeProps}>
|
<ReflexElement flex={previewPane.flex} {...resizeProps}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { TabPane, Tabs } from '@freecodecamp/react-bootstrap';
|
import { TabPane, Tabs } from '@freecodecamp/react-bootstrap';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import React, { Component } from 'react';
|
import React, { Component, ReactElement } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
@ -27,9 +27,12 @@ interface MobileLayoutProps {
|
|||||||
currentTab: number;
|
currentTab: number;
|
||||||
editor: JSX.Element | null;
|
editor: JSX.Element | null;
|
||||||
guideUrl: string;
|
guideUrl: string;
|
||||||
|
hasEditableBoundaries: boolean;
|
||||||
|
hasNotes: boolean;
|
||||||
hasPreview: boolean;
|
hasPreview: boolean;
|
||||||
instructions: JSX.Element;
|
instructions: JSX.Element;
|
||||||
moveToTab: typeof moveToTab;
|
moveToTab: typeof moveToTab;
|
||||||
|
notes: ReactElement;
|
||||||
preview: JSX.Element;
|
preview: JSX.Element;
|
||||||
testOutput: JSX.Element;
|
testOutput: JSX.Element;
|
||||||
videoUrl: string;
|
videoUrl: string;
|
||||||
@ -44,10 +47,13 @@ class MobileLayout extends Component<MobileLayoutProps> {
|
|||||||
const {
|
const {
|
||||||
currentTab,
|
currentTab,
|
||||||
moveToTab,
|
moveToTab,
|
||||||
|
hasEditableBoundaries,
|
||||||
instructions,
|
instructions,
|
||||||
editor,
|
editor,
|
||||||
testOutput,
|
testOutput,
|
||||||
|
hasNotes,
|
||||||
hasPreview,
|
hasPreview,
|
||||||
|
notes,
|
||||||
preview,
|
preview,
|
||||||
guideUrl,
|
guideUrl,
|
||||||
videoUrl,
|
videoUrl,
|
||||||
@ -61,7 +67,9 @@ class MobileLayout extends Component<MobileLayoutProps> {
|
|||||||
|
|
||||||
// Unlike the desktop layout the mobile version does not have an ActionRow,
|
// Unlike the desktop layout the mobile version does not have an ActionRow,
|
||||||
// but still needs a way to switch between the different tabs.
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -71,27 +79,40 @@ class MobileLayout extends Component<MobileLayoutProps> {
|
|||||||
id='challenge-page-tabs'
|
id='challenge-page-tabs'
|
||||||
onSelect={moveToTab}
|
onSelect={moveToTab}
|
||||||
>
|
>
|
||||||
<TabPane eventKey={1} title={i18next.t('learn.editor-tabs.info')}>
|
{!hasEditableBoundaries && (
|
||||||
{instructions}
|
<TabPane
|
||||||
</TabPane>
|
eventKey={eventKeys.shift()}
|
||||||
|
title={i18next.t('learn.editor-tabs.info')}
|
||||||
|
>
|
||||||
|
{instructions}
|
||||||
|
</TabPane>
|
||||||
|
)}
|
||||||
<TabPane
|
<TabPane
|
||||||
eventKey={2}
|
eventKey={eventKeys.shift()}
|
||||||
title={i18next.t('learn.editor-tabs.code')}
|
title={i18next.t('learn.editor-tabs.code')}
|
||||||
{...editorTabPaneProps}
|
{...editorTabPaneProps}
|
||||||
>
|
>
|
||||||
{displayEditorTabs && <EditorTabs />}
|
{projectBasedChallenge && <EditorTabs />}
|
||||||
{editor}
|
{editor}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane
|
<TabPane
|
||||||
eventKey={3}
|
eventKey={eventKeys.shift()}
|
||||||
title={i18next.t('learn.editor-tabs.tests')}
|
title={i18next.t('learn.editor-tabs.tests')}
|
||||||
{...editorTabPaneProps}
|
{...editorTabPaneProps}
|
||||||
>
|
>
|
||||||
{testOutput}
|
{testOutput}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
{hasNotes && projectBasedChallenge && (
|
||||||
|
<TabPane
|
||||||
|
eventKey={eventKeys.shift()}
|
||||||
|
title={i18next.t('learn.editor-tabs.notes')}
|
||||||
|
>
|
||||||
|
{notes}
|
||||||
|
</TabPane>
|
||||||
|
)}
|
||||||
{hasPreview && (
|
{hasPreview && (
|
||||||
<TabPane
|
<TabPane
|
||||||
eventKey={4}
|
eventKey={eventKeys.shift()}
|
||||||
title={i18next.t('learn.editor-tabs.preview')}
|
title={i18next.t('learn.editor-tabs.preview')}
|
||||||
>
|
>
|
||||||
{preview}
|
{preview}
|
||||||
|
@ -9,8 +9,8 @@ import { bindActionCreators, Dispatch } from 'redux';
|
|||||||
import { createStructuredSelector } from 'reselect';
|
import { createStructuredSelector } from 'reselect';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import { challengeTypes } from '../../../../utils/challenge-types';
|
import { challengeTypes } from '../../../../utils/challenge-types';
|
||||||
|
|
||||||
import LearnLayout from '../../../components/layouts/learn';
|
import LearnLayout from '../../../components/layouts/learn';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChallengeFile,
|
ChallengeFile,
|
||||||
ChallengeFiles,
|
ChallengeFiles,
|
||||||
@ -26,6 +26,7 @@ import ResetModal from '../components/ResetModal';
|
|||||||
import ChallengeTitle from '../components/challenge-title';
|
import ChallengeTitle from '../components/challenge-title';
|
||||||
import CompletionModal from '../components/completion-modal';
|
import CompletionModal from '../components/completion-modal';
|
||||||
import HelpModal from '../components/help-modal';
|
import HelpModal from '../components/help-modal';
|
||||||
|
import Notes from '../components/notes';
|
||||||
import Output from '../components/output';
|
import Output from '../components/output';
|
||||||
import Preview from '../components/preview';
|
import Preview from '../components/preview';
|
||||||
import ProjectPreviewModal, {
|
import ProjectPreviewModal, {
|
||||||
@ -115,6 +116,7 @@ interface ReflexLayout {
|
|||||||
codePane: { flex: number };
|
codePane: { flex: number };
|
||||||
editorPane: { flex: number };
|
editorPane: { flex: number };
|
||||||
instructionPane: { flex: number };
|
instructionPane: { flex: number };
|
||||||
|
notesPane: { flex: number };
|
||||||
previewPane: { flex: number };
|
previewPane: { flex: number };
|
||||||
testsPane: { flex: number };
|
testsPane: { flex: number };
|
||||||
}
|
}
|
||||||
@ -126,6 +128,7 @@ const BASE_LAYOUT = {
|
|||||||
editorPane: { flex: 1 },
|
editorPane: { flex: 1 },
|
||||||
instructionPane: { flex: 1 },
|
instructionPane: { flex: 1 },
|
||||||
previewPane: { flex: 0.7 },
|
previewPane: { flex: 0.7 },
|
||||||
|
notesPane: { flex: 0.7 },
|
||||||
testsPane: { flex: 0.25 }
|
testsPane: { flex: 0.25 }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -371,6 +374,10 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNotes(notes?: string) {
|
||||||
|
return <Notes notes={notes} />;
|
||||||
|
}
|
||||||
|
|
||||||
renderPreview() {
|
renderPreview() {
|
||||||
return (
|
return (
|
||||||
<Preview
|
<Preview
|
||||||
@ -397,7 +404,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
|||||||
forumTopicId,
|
forumTopicId,
|
||||||
superBlock,
|
superBlock,
|
||||||
title,
|
title,
|
||||||
usesMultifileEditor
|
usesMultifileEditor,
|
||||||
|
notes
|
||||||
} = this.getChallenge();
|
} = this.getChallenge();
|
||||||
const {
|
const {
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
@ -425,10 +433,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
|||||||
<MobileLayout
|
<MobileLayout
|
||||||
editor={this.renderEditor()}
|
editor={this.renderEditor()}
|
||||||
guideUrl={getGuideUrl({ forumTopicId, title })}
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||||
|
hasEditableBoundaries={this.hasEditableBoundaries()}
|
||||||
|
hasNotes={!!notes}
|
||||||
hasPreview={this.hasPreview()}
|
hasPreview={this.hasPreview()}
|
||||||
instructions={this.renderInstructionsPanel({
|
instructions={this.renderInstructionsPanel({
|
||||||
showToolPanel: false
|
showToolPanel: false
|
||||||
})}
|
})}
|
||||||
|
notes={this.renderNotes(notes)}
|
||||||
preview={this.renderPreview()}
|
preview={this.renderPreview()}
|
||||||
testOutput={this.renderTestOutput()}
|
testOutput={this.renderTestOutput()}
|
||||||
usesMultifileEditor={usesMultifileEditor}
|
usesMultifileEditor={usesMultifileEditor}
|
||||||
@ -441,11 +452,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
|||||||
challengeFiles={challengeFiles}
|
challengeFiles={challengeFiles}
|
||||||
editor={this.renderEditor()}
|
editor={this.renderEditor()}
|
||||||
hasEditableBoundaries={this.hasEditableBoundaries()}
|
hasEditableBoundaries={this.hasEditableBoundaries()}
|
||||||
|
hasNotes={!!notes}
|
||||||
hasPreview={this.hasPreview()}
|
hasPreview={this.hasPreview()}
|
||||||
instructions={this.renderInstructionsPanel({
|
instructions={this.renderInstructionsPanel({
|
||||||
showToolPanel: true
|
showToolPanel: true
|
||||||
})}
|
})}
|
||||||
layoutState={this.state.layout}
|
layoutState={this.state.layout}
|
||||||
|
notes={this.renderNotes(notes)}
|
||||||
preview={this.renderPreview()}
|
preview={this.renderPreview()}
|
||||||
resizeProps={this.resizeProps}
|
resizeProps={this.resizeProps}
|
||||||
superBlock={superBlock}
|
superBlock={superBlock}
|
||||||
@ -481,6 +494,7 @@ export const query = graphql`
|
|||||||
title
|
title
|
||||||
description
|
description
|
||||||
instructions
|
instructions
|
||||||
|
notes
|
||||||
removeComments
|
removeComments
|
||||||
challengeType
|
challengeType
|
||||||
helpCategory
|
helpCategory
|
||||||
|
14
client/src/templates/Challenges/components/notes.tsx
Normal file
14
client/src/templates/Challenges/components/notes.tsx
Normal file
@ -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 && <PrismFormatted text={notes} />}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notes.displayName = 'Notes';
|
||||||
|
|
||||||
|
export default Notes;
|
@ -52,6 +52,7 @@ const schema = Joi.object()
|
|||||||
isComingSoon: Joi.bool(),
|
isComingSoon: Joi.bool(),
|
||||||
isLocked: Joi.bool(),
|
isLocked: Joi.bool(),
|
||||||
isPrivate: Joi.bool(),
|
isPrivate: Joi.bool(),
|
||||||
|
notes: Joi.string().allow(''),
|
||||||
order: Joi.number(),
|
order: Joi.number(),
|
||||||
// video challenges only:
|
// video challenges only:
|
||||||
videoId: Joi.when('challengeType', {
|
videoId: Joi.when('challengeType', {
|
||||||
|
@ -73,6 +73,10 @@ assert.equal(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# --notes--
|
||||||
|
|
||||||
|
Extra information for a challenge, in markdown
|
||||||
|
|
||||||
# --seed--
|
# --seed--
|
||||||
|
|
||||||
## --before-user-code--
|
## --before-user-code--
|
||||||
|
@ -45,12 +45,13 @@ const processor = unified()
|
|||||||
.use(restoreDirectives)
|
.use(restoreDirectives)
|
||||||
.use(addVideoQuestion)
|
.use(addVideoQuestion)
|
||||||
.use(addTests)
|
.use(addTests)
|
||||||
.use(addText, ['description', 'instructions']);
|
.use(addText, ['description', 'instructions', 'notes']);
|
||||||
|
|
||||||
exports.parseMD = function parseMD(filename) {
|
exports.parseMD = function parseMD(filename) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = readSync(filename);
|
const file = readSync(filename);
|
||||||
const tree = processor.parse(file);
|
const tree = processor.parse(file);
|
||||||
|
|
||||||
processor.run(tree, file, function (err, node, file) {
|
processor.run(tree, file, function (err, node, file) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
resolve(file.data);
|
resolve(file.data);
|
||||||
|
Reference in New Issue
Block a user