diff --git a/client/src/templates/Challenges/classic/ActionRow.js b/client/src/templates/Challenges/classic/ActionRow.js
index c53f5ff740..e19e9fbf61 100644
--- a/client/src/templates/Challenges/classic/ActionRow.js
+++ b/client/src/templates/Challenges/classic/ActionRow.js
@@ -1,13 +1,58 @@
import React from 'react';
-
+import PropTypes from 'prop-types';
import EditorTabs from './EditorTabs';
-const ActionRow = () => (
-
-
-
-);
+const propTypes = {
+ block: PropTypes.string,
+ showConsole: PropTypes.bool,
+ showNotes: PropTypes.bool,
+ showPreview: PropTypes.bool,
+ superBlock: PropTypes.string,
+ switchDisplayTab: PropTypes.func
+};
+const ActionRow = ({ switchDisplayTab, showPreview, showConsole }) => {
+ const restartStep = () => {
+ console.log('restart');
+ };
+ return (
+
+
+
+ Responsive Web Design > Basic HTML Cat Photo App >{' '}
+ Step 23 of 213
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+ActionRow.propTypes = propTypes;
ActionRow.displayName = 'ActionRow';
-
export default ActionRow;
diff --git a/client/src/templates/Challenges/classic/DesktopLayout.js b/client/src/templates/Challenges/classic/DesktopLayout.js
index 8961e6863a..f342e40334 100644
--- a/client/src/templates/Challenges/classic/DesktopLayout.js
+++ b/client/src/templates/Challenges/classic/DesktopLayout.js
@@ -29,6 +29,20 @@ const reflexProps = {
};
class DesktopLayout extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { showNotes: false, showPreview: true, showConsole: false };
+ this.switchDisplayTab = this.switchDisplayTab.bind(this);
+ }
+
+ switchDisplayTab(displayTab) {
+ this.setState(state => {
+ return {
+ [displayTab]: !state[displayTab]
+ };
+ });
+ }
+
getChallengeFile() {
const { challengeFiles } = this.props;
return first(Object.keys(challengeFiles).map(key => challengeFiles[key]));
@@ -45,16 +59,30 @@ class DesktopLayout extends Component {
hasEditableBoundries
} = this.props;
+ const { showPreview, showConsole } = this.state;
+
const challengeFile = this.getChallengeFile();
+ const projectBasedChallenge = showUpcomingChanges && hasEditableBoundries;
+ const isPreviewDisplayable = projectBasedChallenge
+ ? showPreview && hasPreview
+ : hasPreview;
+ const isConsoleDisplayable = projectBasedChallenge ? showConsole : true;
return (
- {showUpcomingChanges && hasEditableBoundries && }
+ {projectBasedChallenge && (
+
+ )}
-
- {instructions}
-
-
+ {!projectBasedChallenge && (
+
+ {instructions}
+
+ )}
+ {!projectBasedChallenge && (
+
+ )}
+
{challengeFile && (
@@ -68,15 +96,21 @@ class DesktopLayout extends Component {
}
-
-
- {testOutput}
-
+ {isConsoleDisplayable && (
+
+ )}
+ {isConsoleDisplayable && (
+
+ {testOutput}
+
+ )}
)}
- {hasPreview && }
- {hasPreview && (
+ {isPreviewDisplayable && (
+
+ )}
+ {isPreviewDisplayable && (
{preview}
diff --git a/client/src/templates/Challenges/classic/Editor.js b/client/src/templates/Challenges/classic/Editor.js
index 2bcbe75c3c..91749d7172 100644
--- a/client/src/templates/Challenges/classic/Editor.js
+++ b/client/src/templates/Challenges/classic/Editor.js
@@ -12,7 +12,9 @@ import {
saveEditorContent,
setEditorFocusability,
setAccessibilityMode,
- updateFile
+ updateFile,
+ challengeTestsSelector,
+ submitChallenge
} from '../redux';
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
import { Loader } from '../../../components/helpers';
@@ -43,6 +45,8 @@ const propTypes = {
saveEditorContent: PropTypes.func.isRequired,
setAccessibilityMode: PropTypes.func.isRequired,
setEditorFocusability: PropTypes.func,
+ submitChallenge: PropTypes.func,
+ tests: PropTypes.arrayOf(PropTypes.object),
theme: PropTypes.string,
updateFile: PropTypes.func.isRequired
};
@@ -53,11 +57,20 @@ const mapStateToProps = createSelector(
inAccessibilityModeSelector,
isDonationModalOpenSelector,
userSelector,
- (canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({
+ challengeTestsSelector,
+ (
+ canFocus,
+ output,
+ accessibilityMode,
+ open,
+ { theme = 'default' },
+ tests
+ ) => ({
canFocus: open ? false : canFocus,
output,
inAccessibilityMode: accessibilityMode,
- theme
+ theme,
+ tests
})
);
@@ -66,7 +79,8 @@ const mapDispatchToProps = {
saveEditorContent,
setAccessibilityMode,
setEditorFocusability,
- updateFile
+ updateFile,
+ submitChallenge
};
const modeMap = {
@@ -140,6 +154,7 @@ class Editor extends Component {
viewZoneId: null,
startEditDecId: null,
endEditDecId: null,
+ insideEditDecId: null,
viewZoneHeight: null
};
@@ -182,6 +197,13 @@ class Editor extends Component {
this.focusOnEditor = this.focusOnEditor.bind(this);
}
+ getEditableRegion = () => {
+ const { challengeFiles, fileKey } = this.props;
+ return challengeFiles[fileKey].editableRegionBoundaries
+ ? [...challengeFiles[fileKey].editableRegionBoundaries]
+ : [];
+ };
+
editorWillMount = monaco => {
this._monaco = monaco;
const { challengeFiles, fileKey } = this.props;
@@ -201,9 +223,7 @@ class Editor extends Component {
);
this.data.model = model;
- const editableRegion = challengeFiles[fileKey].editableRegionBoundaries
- ? [...challengeFiles[fileKey].editableRegionBoundaries]
- : [];
+ const editableRegion = this.getEditableRegion();
if (editableRegion.length === 2)
this.decorateForbiddenRanges(editableRegion);
@@ -224,7 +244,6 @@ class Editor extends Component {
editorDidMount = (editor, monaco) => {
this._editor = editor;
- const { challengeFiles, fileKey } = this.props;
editor.updateOptions({
accessibilitySupport: this.props.inAccessibilityMode ? 'on' : 'auto'
});
@@ -286,9 +305,7 @@ class Editor extends Component {
}
});
- const editableBoundaries = challengeFiles[fileKey].editableRegionBoundaries
- ? [...challengeFiles[fileKey].editableRegionBoundaries]
- : [];
+ const editableBoundaries = this.getEditableRegion();
if (editableBoundaries.length === 2) {
// TODO: is there a nicer approach/way of organising everything that
@@ -363,7 +380,7 @@ class Editor extends Component {
this.data.viewZoneHeight = domNode.offsetHeight;
var background = document.createElement('div');
- background.style.background = 'lightgreen';
+ // background.style.background = 'lightgreen';
// We have to wait for the viewZone to finish rendering before adjusting the
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
@@ -392,7 +409,7 @@ class Editor extends Component {
this.data.outputZoneHeight = outputNode.offsetHeight;
var background = document.createElement('div');
- background.style.background = 'lightpink';
+ // background.style.background = 'lightpink';
// We have to wait for the viewZone to finish rendering before adjusting the
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
@@ -413,19 +430,14 @@ class Editor extends Component {
const { description } = this.props;
var domNode = document.createElement('div');
var desc = document.createElement('div');
- var button = document.createElement('button');
- button.innerHTML = 'Run the Tests (Ctrl + Enter)';
- button.onclick = () => {
- const { executeChallenge } = this.props;
- executeChallenge();
- };
-
- domNode.appendChild(desc);
- domNode.appendChild(button);
+ var descContainer = document.createElement('div');
+ descContainer.classList.add('description-container');
+ domNode.classList.add('editor-upper-jaw');
+ domNode.appendChild(descContainer);
+ descContainer.appendChild(desc);
desc.innerHTML = description;
-
- desc.style.background = 'white';
- domNode.style.background = 'lightgreen';
+ // desc.style.background = 'white';
+ // domNode.style.background = 'lightgreen';
// TODO: the solution is probably just to use an overlay that's forced to
// follow the decorations.
// TODO: this is enough for Firefox, but Chrome needs more before the
@@ -436,7 +448,7 @@ class Editor extends Component {
domNode.setAttribute('aria-hidden', true);
- domNode.style.background = 'lightYellow';
+ // domNode.style.background = 'lightYellow';
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
domNode.style.top = this.getViewZoneTop();
@@ -449,11 +461,23 @@ class Editor extends Component {
const outputNode = document.createElement('div');
const statusNode = document.createElement('div');
const hintNode = document.createElement('div');
- outputNode.appendChild(statusNode);
- outputNode.appendChild(hintNode);
+ const editorActionRow = document.createElement('div');
+ editorActionRow.classList.add('action-row-container');
+ outputNode.classList.add('editor-lower-jaw');
+ outputNode.appendChild(editorActionRow);
hintNode.setAttribute('id', 'test-output');
statusNode.setAttribute('id', 'test-status');
- statusNode.innerHTML = '// tests';
+ var button = document.createElement('button');
+ button.setAttribute('id', 'test-button');
+ button.classList.add('btn-block');
+ button.innerHTML = 'Check Your Code (Ctrl + Enter)';
+ editorActionRow.appendChild(button);
+ editorActionRow.appendChild(statusNode);
+ editorActionRow.appendChild(hintNode);
+ button.onclick = () => {
+ const { executeChallenge } = this.props;
+ executeChallenge();
+ };
// TODO: does it?
// The z-index needs increasing as ViewZones default to below the lines.
@@ -528,6 +552,19 @@ class Editor extends Component {
return target.deltaDecorations(oldIds, [lineDecoration]);
}
+ highlightEditableLines(stickiness, target, range, oldIds = []) {
+ const lineDecoration = {
+ range,
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'myEditableLineDecoration',
+ className: 'do-not-edit',
+ stickiness
+ }
+ };
+ return target.deltaDecorations(oldIds, [lineDecoration]);
+ }
+
highlightText(stickiness, target, range, oldIds = []) {
const inlineDecoration = {
range,
@@ -655,6 +692,17 @@ class Editor extends Component {
return this.positionsToRange(model, positions);
});
+ const editableRange = this.positionsToRange(model, [
+ editableRegion[0] + 1,
+ editableRegion[1] - 1
+ ]);
+
+ this.data.insideEditDecId = this.highlightEditableLines(
+ this._monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
+ model,
+ editableRange
+ );
+
// if the forbidden range includes the top of the editor
// we simply don't add those decorations
if (forbiddenRanges[0][1] > 0) {
@@ -743,53 +791,25 @@ class Editor extends Component {
});
};
- // Make sure the zone tracks the decoration (i.e. the region)
- const handleHintsZoneChange = id => {
- const startOfZone = toStartOfLine(
- model.getDecorationRange(id)
- ).collapseToStart();
- // the decoration needs adjusting if the user creates a line immediately
- // before the greyed out region...
- const lineOneRange = this.translateRange(startOfZone, -2);
- // or immediately after it
- const lineTwoRange = this.translateRange(startOfZone, -1);
-
- for (const lineRange of newLineRanges) {
- const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
- lineRange,
- lineOneRange.plusRange(lineTwoRange)
- );
-
- if (shouldMoveZone) {
- this.updateOutputZone();
- }
+ // Make sure the zone tracks the decoration (i.e. the region), which might
+ // have changed if a line has been added or removed
+ const handleHintsZoneChange = () => {
+ if (newLineRanges.length > 0 || deletedLine > 0) {
+ this.updateOutputZone();
}
};
- // Make sure the zone tracks the decoration (i.e. the region)
- const handleDescriptionZoneChange = id => {
- const endOfZone = toLastLine(
- model.getDecorationRange(id)
- ).collapseToStart();
- // the decoration needs adjusting if the user creates a line immediately
- // before the editable region.
- const lineOneRange = this.translateRange(endOfZone, -1);
-
- for (const lineRange of newLineRanges) {
- const shouldMoveZone = this._monaco.Range.areIntersectingOrTouching(
- lineRange,
- lineOneRange
- );
-
- if (shouldMoveZone) {
- this.updateViewZone();
- }
+ // Make sure the zone tracks the decoration (i.e. the region), which might
+ // have changed if a line has been added or removed
+ const handleDescriptionZoneChange = () => {
+ if (newLineRanges.length > 0 || deletedLine > 0) {
+ this.updateViewZone();
}
};
// Stops the greyed out region from covering the editable region. Does not
// change the font decoration.
- const preventOverlap = id => {
+ const preventOverlap = (id, stickiness, highlightFunction) => {
// Even though the decoration covers the whole line, it has a
// startColumn that moves. toStartOfLine ensures that the
// comparison detects if any change has occurred on that line
@@ -821,17 +841,15 @@ class Editor extends Component {
if (touchingDeleted) {
// TODO: if they undo this should be reversed
- const decorations = this.highlightLines(
- this._monaco.editor.TrackedRangeStickiness
- .NeverGrowsWhenTypingAtEdges,
+ const decorations = highlightFunction(
+ stickiness,
model,
newCoveringRange,
- [id]
+ id
);
this.updateOutputZone();
- // when there's a change, decorations will be [oldId, newId]
- return decorations.slice(-1)[0];
+ return decorations;
} else {
return id;
}
@@ -839,7 +857,17 @@ class Editor extends Component {
// we only need to handle the special case of the second region being
// pulled up, the first region already behaves correctly.
- this.data.endEditDecId = preventOverlap(this.data.endEditDecId);
+ this.data.endEditDecId = preventOverlap(
+ this.data.endEditDecId,
+ this._monaco.editor.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore,
+ this.highlightLines
+ );
+
+ this.data.insideEditDecId = preventOverlap(
+ this.data.insideEditDecId,
+ this._monaco.editor.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
+ this.highlightEditableLines
+ );
// TODO: do the same for the description widget
// this has to be handle differently, because we care about the END
@@ -847,10 +875,10 @@ class Editor extends Component {
// if the editable region includes the first line, the first decoration
// will be missing.
if (this.data.startEditDecId) {
- handleDescriptionZoneChange(this.data.startEditDecId);
+ handleDescriptionZoneChange();
warnUser(this.data.startEditDecId);
}
- handleHintsZoneChange(this.data.endEditDecId);
+ handleHintsZoneChange();
warnUser(this.data.endEditDecId);
});
}
@@ -889,7 +917,47 @@ class Editor extends Component {
}
if (this._editor) {
- const { output } = this.props;
+ const { output, tests } = this.props;
+ const editableRegion = this.getEditableRegion();
+ if (this.props.tests !== prevProps.tests && editableRegion.length === 2) {
+ const challengeComplete = tests.every(test => test.pass && !test.err);
+ const chellengeHasErrors = tests.some(test => test.err);
+
+ if (challengeComplete) {
+ let testButton = document.getElementById('test-button');
+ testButton.innerHTML =
+ 'Submit your code and go to next challenge (Ctrl + Enter)';
+ testButton.onclick = () => {
+ const { submitChallenge } = this.props;
+ submitChallenge();
+ };
+
+ let editableRegionDecorators = document.getElementsByClassName(
+ 'myEditableLineDecoration'
+ );
+ if (editableRegionDecorators.length > 0) {
+ for (var i of editableRegionDecorators) {
+ i.classList.add('tests-passed');
+ }
+ }
+ document.getElementById('test-output').innerHTML = '';
+ document.getElementById('test-status').innerHTML =
+ '✅ Step completed.';
+ } else if (chellengeHasErrors) {
+ const wordsArray = [
+ "Not quite. Here's a hint:",
+ 'Try again. This might help:',
+ 'Keep trying. A quick hint for you:',
+ "You're getting there. This may help:",
+ "Hang in there. You'll get there. A hint:",
+ "Don't give up. Here's a hint to get you thinking:"
+ ];
+ document.getElementById('test-status').innerHTML = `✖️ ${
+ wordsArray[Math.floor(Math.random() * wordsArray.length)]
+ }`;
+ document.getElementById('test-output').innerHTML = `${output[1]}`;
+ }
+ }
if (this.props.output !== prevProps.output && this._outputNode) {
// TODO: output gets wiped when the preview gets updated, keeping the
// display is an anti-pattern (the render should not ignore props!).
@@ -897,14 +965,6 @@ class Editor extends Component {
// (shownHint,maybe) and have that persist through previews. But, for
// now:
if (output) {
- if (output[0]) {
- document.getElementById('test-status').innerHTML = output[0];
- }
-
- if (output[1]) {
- document.getElementById('test-output').innerHTML = output[1];
- }
-
// if either id exists, the editable region exists
// TODO: add a layer of abstraction: we should be interacting with
// the editable region, not the ids
diff --git a/client/src/templates/Challenges/classic/editor.css b/client/src/templates/Challenges/classic/editor.css
index 10a8281be6..819fe329cf 100644
--- a/client/src/templates/Challenges/classic/editor.css
+++ b/client/src/templates/Challenges/classic/editor.css
@@ -6,10 +6,6 @@
color: var(--primary-color);
}
-[widgetid='my.overlay.widget'] {
- padding: 10px;
-}
-
.vs .monaco-scrollable-element > .scrollbar > .slider {
z-index: 11;
}
@@ -17,3 +13,64 @@
.editor-container {
background: var(--editor-background);
}
+
+.breadcrumbs-demo {
+ font-size: 16px;
+}
+
+.breadcrumbs-demo span {
+ font-weight: bold;
+}
+
+.tabs-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.action-row button {
+ padding: 4px 16px;
+}
+
+.active-tab {
+ border-color: var(--secondary-color);
+ background-color: var(--secondary-color);
+ color: var(--secondary-background);
+}
+
+/* [widgetid='my.overlay.widget'] {
+ padding: 10px;
+} */
+
+.editor-upper-jaw,
+.editor-lower-jaw {
+ padding: 15px 15px 15px 0px;
+}
+
+.action-row-container,
+.description-container {
+ background-color: var(--secondary-background);
+ padding: 10px;
+ border: 2px solid var(--tertiary-background);
+ max-width: 700px;
+}
+
+#description p:last-child {
+ margin: 0px;
+}
+
+#test-status {
+ padding-bottom: 5px;
+ padding-top: 5px;
+}
+
+.myEditableLineDecoration {
+ background-color: var(--gray-45);
+ width: 15px !important;
+ margin-left: 5px !important;
+ margin-right: 5px !important;
+}
+
+.myEditableLineDecoration.tests-passed {
+ background-color: #4caf50;
+}
diff --git a/client/src/templates/Challenges/components/BreadCrumb.js b/client/src/templates/Challenges/components/BreadCrumb.js
new file mode 100644
index 0000000000..1c7d6c505f
--- /dev/null
+++ b/client/src/templates/Challenges/components/BreadCrumb.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Link } from '../../../components/helpers/index';
+
+import './challenge-title.css';
+import i18next from 'i18next';
+
+const propTypes = {
+ block: PropTypes.string,
+ superBlock: PropTypes.string
+};
+
+function BreadCrumb({ block, superBlock }) {
+ return (
+
+
+
+ {i18next.t(`intro:${superBlock}.title`)}
+
+
+
+
+ {i18next.t(`intro:${superBlock}.blocks.${block}.title`)}
+
+
+ );
+}
+
+BreadCrumb.displayName = 'BreadCrumb';
+BreadCrumb.propTypes = propTypes;
+
+export default BreadCrumb;
diff --git a/client/src/templates/Challenges/components/Challenge-Title.js b/client/src/templates/Challenges/components/Challenge-Title.js
index 21404a4415..2ca72b7406 100644
--- a/client/src/templates/Challenges/components/Challenge-Title.js
+++ b/client/src/templates/Challenges/components/Challenge-Title.js
@@ -1,10 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '../../../components/helpers/index';
+import i18next from 'i18next';
import './challenge-title.css';
import GreenPass from '../../../assets/icons/GreenPass';
-import i18next from 'i18next';
+import BreadCrumb from './BreadCrumb';
const propTypes = {
block: PropTypes.string,
@@ -31,25 +32,7 @@ function ChallengeTitle({
{i18next.t('misc.translation-pending')}
)}
-
-
-
- {i18next.t(`intro:${superBlock}.title`)}
-
-
-
-
- {i18next.t(`intro:${superBlock}.blocks.${block}.title`)}
-
-
+