Merge branch 'fix/merge-78e86f5' of https://github.com/mpontus/freeCodeCamp into fix/toolpanelConflict

This commit is contained in:
Stuart Taylor
2017-12-24 09:21:03 +00:00
9 changed files with 284 additions and 109 deletions

View File

@ -81,10 +81,6 @@
border-bottom: 1px solid @modal-header-border-color; border-bottom: 1px solid @modal-header-border-color;
min-height: (@modal-title-padding + @modal-title-line-height); min-height: (@modal-title-padding + @modal-title-line-height);
} }
// Close icon
.modal-header .close {
margin-top: -2px;
}
// Title text within header // Title text within header
.modal-title { .modal-title {

View File

@ -500,7 +500,7 @@ form.update-email .btn{
} }
} }
.challenge-list-header { .challenges-list-header {
background-color: @brand-primary; background-color: @brand-primary;
color: @gray-lighter; color: @gray-lighter;
font-size: 36px; font-size: 36px;
@ -825,7 +825,7 @@ code {
color: @night-text-color; color: @night-text-color;
.btn-group, .btn-group,
.text-success, .text-success,
.challenge-list-header, .challenges-list-header,
.fcc-footer { .fcc-footer {
background-color: @night-body-bg; background-color: @night-body-bg;
} }

View File

@ -0,0 +1,94 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Modal } from 'react-bootstrap';
import ns from './ns.json';
import {
createQuestion,
openHelpChatRoom,
closeHelpModal,
helpModalSelector
} from './redux';
const mapStateToProps = state => ({ isOpen: helpModalSelector(state) });
const mapDispatchToProps = { createQuestion, openHelpChatRoom, closeHelpModal };
const methodologyUrl = 'https://forum.freecodecamp.org/t/the-read-search-ask-methodology-for-getting-unstuck/137307'; // eslint-disable-line max-len
const propTypes = {
closeHelpModal: PropTypes.func,
createQuestion: PropTypes.func,
isOpen: PropTypes.bool,
openHelpChatRoom: PropTypes.func
};
export class HelpModal extends PureComponent {
render() {
const {
isOpen,
closeHelpModal,
openHelpChatRoom,
createQuestion
} = this.props;
return (
<Modal
show={ isOpen }
>
<Modal.Header className={ `${ns}-list-header` }>
Ask for help?
<span
className='close closing-x'
onClick={ closeHelpModal }
>
×
</span>
</Modal.Header>
<Modal.Body className='text-center'>
<h3>
If you've already tried the&nbsp;Read-Search-Ask&nbsp;method,
then you can ask for help on the freeCodeCamp forum.
</h3>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ methodologyUrl }
onClick={ closeHelpModal }
target='_blank'
>
Learn about the Read-Search-Ask Methodology
</Button>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
onClick={ createQuestion }
>
Create a help post on the forum
</Button>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
onClick={ openHelpChatRoom }
>
Ask for help in the Gitter Chatroom
</Button>
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
onClick={ closeHelpModal }
>
Cancel
</Button>
</Modal.Body>
</Modal>
);
}
}
HelpModal.displayName = 'HelpModal';
HelpModal.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(HelpModal);

View File

@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import ns from './ns.json'; import ns from './ns.json';
import BugModal from './Bug-Modal.jsx'; import BugModal from './Bug-Modal.jsx';
import HelpModal from './Help-Modal.jsx';
import ToolPanel from './Tool-Panel.jsx'; import ToolPanel from './Tool-Panel.jsx';
import ChallengeTitle from './Challenge-Title.jsx'; import ChallengeTitle from './Challenge-Title.jsx';
import ChallengeDescription from './Challenge-Description.jsx'; import ChallengeDescription from './Challenge-Description.jsx';
@ -14,6 +15,7 @@ import TestSuite from './Test-Suite.jsx';
import Output from './Output.jsx'; import Output from './Output.jsx';
import { import {
openBugModal, openBugModal,
openHelpModal,
updateHint, updateHint,
executeChallenge, executeChallenge,
unlockUntrustedCode, unlockUntrustedCode,
@ -22,8 +24,7 @@ import {
testsSelector, testsSelector,
outputSelector, outputSelector,
hintIndexSelector, hintIndexSelector,
codeLockedSelector, codeLockedSelector
chatRoomSelector
} from './redux'; } from './redux';
import { descriptionRegex } from './utils'; import { descriptionRegex } from './utils';
@ -35,6 +36,7 @@ const mapDispatchToProps = {
executeChallenge, executeChallenge,
updateHint, updateHint,
openBugModal, openBugModal,
openHelpModal,
unlockUntrustedCode unlockUntrustedCode
}; };
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
@ -44,7 +46,6 @@ const mapStateToProps = createSelector(
outputSelector, outputSelector,
hintIndexSelector, hintIndexSelector,
codeLockedSelector, codeLockedSelector,
chatRoomSelector,
( (
{ description }, { description },
{ title }, { title },
@ -52,24 +53,22 @@ const mapStateToProps = createSelector(
output, output,
hintIndex, hintIndex,
isCodeLocked, isCodeLocked,
helpChatRoom
) => ({ ) => ({
title, title,
description, description,
tests, tests,
output, output,
isCodeLocked, isCodeLocked
helpChatRoom
}) })
); );
const propTypes = { const propTypes = {
description: PropTypes.arrayOf(PropTypes.string), description: PropTypes.arrayOf(PropTypes.string),
executeChallenge: PropTypes.func, executeChallenge: PropTypes.func,
helpChatRoom: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
isCodeLocked: PropTypes.bool, isCodeLocked: PropTypes.bool,
makeToast: PropTypes.func, makeToast: PropTypes.func,
openBugModal: PropTypes.func, openBugModal: PropTypes.func,
openHelpModal: PropTypes.func,
output: PropTypes.string, output: PropTypes.string,
tests: PropTypes.arrayOf(PropTypes.object), tests: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string, title: PropTypes.string,
@ -125,8 +124,8 @@ export class SidePanel extends PureComponent {
executeChallenge, executeChallenge,
updateHint, updateHint,
makeToast, makeToast,
helpChatRoom,
openBugModal, openBugModal,
openHelpModal,
isCodeLocked, isCodeLocked,
unlockUntrustedCode unlockUntrustedCode
} = this.props; } = this.props;
@ -147,15 +146,16 @@ export class SidePanel extends PureComponent {
</div> </div>
<ToolPanel <ToolPanel
executeChallenge={ executeChallenge } executeChallenge={ executeChallenge }
helpChatRoom={ helpChatRoom }
hint={ hint } hint={ hint }
isCodeLocked={ isCodeLocked } isCodeLocked={ isCodeLocked }
makeToast={ makeToast } makeToast={ makeToast }
openBugModal={ openBugModal } openBugModal={ openBugModal }
openHelpModal={ openHelpModal }
unlockUntrustedCode={ unlockUntrustedCode } unlockUntrustedCode={ unlockUntrustedCode }
updateHint={ updateHint } updateHint={ updateHint }
/> />
<BugModal /> <BugModal />
<HelpModal />
<Output <Output
defaultOutput={ defaultOutput={
`/** `/**

View File

@ -2,6 +2,8 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap'; import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap';
import ns from './ns.json';
const unlockWarning = ( const unlockWarning = (
<Tooltip id='tooltip'> <Tooltip id='tooltip'>
<h4> <h4>
@ -12,11 +14,11 @@ const unlockWarning = (
const propTypes = { const propTypes = {
executeChallenge: PropTypes.func.isRequired, executeChallenge: PropTypes.func.isRequired,
helpChatRoom: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
isCodeLocked: PropTypes.bool, isCodeLocked: PropTypes.bool,
makeToast: PropTypes.func.isRequired, makeToast: PropTypes.func.isRequired,
openBugModal: PropTypes.func.isRequired, openBugModal: PropTypes.func.isRequired,
openHelpModal: PropTypes.func.isRequired,
unlockUntrustedCode: PropTypes.func.isRequired, unlockUntrustedCode: PropTypes.func.isRequired,
updateHint: PropTypes.func.isRequired updateHint: PropTypes.func.isRequired
}; };
@ -93,10 +95,10 @@ export default class ToolPanel extends PureComponent {
render() { render() {
const { const {
executeChallenge, executeChallenge,
helpChatRoom,
hint, hint,
isCodeLocked, isCodeLocked,
openBugModal, openBugModal,
openHelpModal,
unlockUntrustedCode unlockUntrustedCode
} = this.props; } = this.props;
return ( return (
@ -111,13 +113,12 @@ export default class ToolPanel extends PureComponent {
} }
<div className='button-spacer' /> <div className='button-spacer' />
<ButtonGroup <ButtonGroup
className='input-group' className={`input-group ${ns}-tool-panel-btn-grp`}
justified={ true } justified={ true }
> >
<Button <Button
bsSize='large' bsSize='large'
bsStyle='primary' bsStyle='primary'
componentClass='label'
onClick={ this.makeReset } onClick={ this.makeReset }
> >
Reset Reset
@ -125,16 +126,13 @@ export default class ToolPanel extends PureComponent {
<Button <Button
bsSize='large' bsSize='large'
bsStyle='primary' bsStyle='primary'
componentClass='a' onClick={ openHelpModal }
href={ `https://gitter.im/freecodecamp/${helpChatRoom}` }
target='_blank'
> >
Help Help
</Button> </Button>
<Button <Button
bsSize='large' bsSize='large'
bsStyle='primary' bsStyle='primary'
componentClass='label'
onClick={ openBugModal } onClick={ openBugModal }
> >
Bug Bug

View File

@ -216,4 +216,12 @@
width: 100%; width: 100%;
} }
.@{ns}-tool-panel-btn-grp {
display: flex;
.btn {
flex-grow: 1;
}
}
&{ @import "./views/index.less"; } &{ @import "./views/index.less"; }

View File

@ -1,82 +0,0 @@
import { ofType } from 'redux-epic';
import {
types,
closeBugModal
} from '../redux';
import { filesSelector } from '../../../files';
import { currentChallengeSelector } from '../../../redux';
function filesToMarkdown(files = {}) {
const moreThenOneFile = Object.keys(files).length > 1;
return Object.keys(files).reduce((fileString, key) => {
const file = files[key];
if (!file) {
return fileString;
}
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
const fileType = file.ext;
return fileString +
'\`\`\`' +
fileType +
'\n' +
fileName +
'\n' +
file.contents +
'\n' +
'\`\`\`\n\n';
}, '\n');
}
export default function bugEpic(actions, { getState }, { window }) {
return actions::ofType(types.openIssueSearch, types.createIssue)
.map(({ type }) => {
const state = getState();
const files = filesSelector(state);
const challengeName = currentChallengeSelector(state);
const {
navigator: { userAgent },
location: { href }
} = window;
let titleText = challengeName;
if (type === types.openIssueSearch) {
window.open(
'https://forum.freecodecamp.org/search?q=' +
window.encodeURIComponent(titleText)
);
} else {
titleText = 'Need assistance in ' + challengeName;
let textMessage = [
'#### Challenge Name\n',
'[',
challengeName,
'](',
href,
') has an issue.\n',
'#### Issue Description\n',
'<!-- Describe below when the issue happens and how to ',
'reproduce it -->\n\n\n',
'#### Browser Information\n',
'<!-- Describe your workspace in which you are having issues-->\n',
'User Agent is: <code>',
userAgent,
'</code>.\n\n',
'#### Screenshot\n',
'<!-- Add a screenshot of your issue -->\n\n\n',
'#### Your Code'
].join('');
const body = filesToMarkdown(files);
if (body.length > 10) {
textMessage = textMessage + body;
}
window.open(
'https://forum.freecodecamp.org/new-topic?category=General&title=' +
window.encodeURIComponent(titleText) + '&body=' +
window.encodeURIComponent(textMessage),
'_blank'
);
}
return closeBugModal();
});
}

View File

@ -10,7 +10,7 @@ import {
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import bugEpic from './bug-epic'; import modalEpic from './modal-epic';
import completionEpic from './completion-epic.js'; import completionEpic from './completion-epic.js';
import challengeEpic from './challenge-epic.js'; import challengeEpic from './challenge-epic.js';
import executeChallengeEpic from './execute-challenge-epic.js'; import executeChallengeEpic from './execute-challenge-epic.js';
@ -44,7 +44,7 @@ const challengeToFilesMetaCreator =
_.flow(challengeToFiles, createFilesMetaCreator); _.flow(challengeToFiles, createFilesMetaCreator);
export const epics = [ export const epics = [
bugEpic, modalEpic,
challengeEpic, challengeEpic,
codeStorageEpic, codeStorageEpic,
completionEpic, completionEpic,
@ -83,6 +83,12 @@ export const types = createTypes([
'openIssueSearch', 'openIssueSearch',
'createIssue', 'createIssue',
// help
'openHelpModal',
'closeHelpModal',
'createQuestion',
'openHelpChatRoom',
// panes // panes
'toggleClassicEditor', 'toggleClassicEditor',
'toggleMain', 'toggleMain',
@ -157,6 +163,12 @@ export const closeBugModal = createAction(types.closeBugModal);
export const openIssueSearch = createAction(types.openIssueSearch); export const openIssueSearch = createAction(types.openIssueSearch);
export const createIssue = createAction(types.createIssue); export const createIssue = createAction(types.createIssue);
// help
export const openHelpModal = createAction(types.openHelpModal);
export const closeHelpModal = createAction(types.closeHelpModal);
export const createQuestion = createAction(types.createQuestion);
export const openHelpChatRoom = createAction(types.openHelpChatRoom);
// code storage // code storage
export const storedCodeFound = createAction( export const storedCodeFound = createAction(
types.storedCodeFound, types.storedCodeFound,
@ -174,6 +186,7 @@ const initialUiState = {
output: null, output: null,
isChallengeModalOpen: false, isChallengeModalOpen: false,
isBugOpen: false, isBugOpen: false,
isHelpOpen: false,
successMessage: 'Happy Coding!' successMessage: 'Happy Coding!'
}; };
@ -206,6 +219,7 @@ export const challengeModalSelector =
state => getNS(state).isChallengeModalOpen; state => getNS(state).isChallengeModalOpen;
export const bugModalSelector = state => getNS(state).isBugOpen; export const bugModalSelector = state => getNS(state).isBugOpen;
export const helpModalSelector = state => getNS(state).isHelpOpen;
export const challengeRequiredSelector = state => export const challengeRequiredSelector = state =>
challengeSelector(state).required || []; challengeSelector(state).required || [];
@ -318,9 +332,10 @@ export default combineReducers(
...state, ...state,
output: (state.output || '') + output output: (state.output || '') + output
}), }),
[types.openBugModal]: state => ({ ...state, isBugOpen: true }), [types.openBugModal]: state => ({ ...state, isBugOpen: true }),
[types.closeBugModal]: state => ({ ...state, isBugOpen: false }) [types.closeBugModal]: state => ({ ...state, isBugOpen: false }),
[types.openHelpModal]: state => ({ ...state, isHelpOpen: true }),
[types.closeHelpModal]: state => ({ ...state, isHelpOpen: false })
}), }),
initialState, initialState,
ns ns

View File

@ -0,0 +1,146 @@
import { combineEpics, ofType } from 'redux-epic';
import {
types,
chatRoomSelector,
closeBugModal,
closeHelpModal
} from '../redux';
import { filesSelector } from '../../../files';
import { currentChallengeSelector } from '../../../redux';
function filesToMarkdown(files = {}) {
const moreThenOneFile = Object.keys(files).length > 1;
return Object.keys(files).reduce((fileString, key) => {
const file = files[key];
if (!file) {
return fileString;
}
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
const fileType = file.ext;
return fileString +
'\`\`\`' +
fileType +
'\n' +
fileName +
'\n' +
file.contents +
'\n' +
'\`\`\`\n\n';
}, '\n');
}
export function openIssueSearchEpic(actions, { getState }, { window }) {
return actions::ofType(types.openIssueSearch).map(() => {
const state = getState();
const challengeName = currentChallengeSelector(state);
window.open(
'https://forum.freecodecamp.org/search?q=' +
window.encodeURIComponent(challengeName)
);
return closeBugModal();
});
}
export function createIssueEpic(actions, { getState }, { window }) {
return actions::ofType(types.createIssue).map(() => {
const state = getState();
const files = filesSelector(state);
const challengeName = currentChallengeSelector(state);
const {
navigator: { userAgent },
location: { href }
} = window;
const titleText = 'Need assistance in ' + challengeName;
let textMessage = [
'#### Challenge Name\n',
'[',
challengeName,
'](',
href,
') has an issue.\n',
'#### Issue Description\n',
'<!-- Describe below when the issue happens and how to ',
'reproduce it -->\n\n\n',
'#### Browser Information\n',
'<!-- Describe your workspace in which you are having issues-->\n',
'User Agent is: <code>',
userAgent,
'</code>.\n\n',
'#### Screenshot\n',
'<!-- Add a screenshot of your issue -->\n\n\n',
'#### Your Code'
].join('');
const body = filesToMarkdown(files);
if (body.length > 10) {
textMessage += body;
}
window.open(
'https://forum.freecodecamp.org/new-topic'
+ '?category=General'
+ '&title=' + window.encodeURIComponent(titleText)
+ '&body=' + window.encodeURIComponent(textMessage),
'_blank'
);
return closeBugModal();
});
}
export function openHelpChatRoomEpic(actions, { getState }, { window }) {
return actions::ofType(types.openHelpChatRoom).map(() => {
const state = getState();
const helpChatRoom = chatRoomSelector(state);
window.open(
'https://gitter.im/freecodecamp/' +
window.encodeURIComponent(helpChatRoom)
);
return closeHelpModal();
});
}
export function createQuestionEpic(actions, { getState }, { window }) {
return actions::ofType(types.createQuestion).map(() => {
const state = getState();
const files = filesSelector(state);
const challengeName = currentChallengeSelector(state);
const {
navigator: { userAgent },
location: { href }
} = window;
const textMessage = [
'**Tell us what\'s happening:**\n\n\n\n',
'**Your code so far**\n',
filesToMarkdown(files),
'**Your browser information:**\n\n',
'User Agent is: <code>',
userAgent,
'</code>.\n\n',
'**Link to the challenge:**\n',
href
].join('');
window.open(
'https://forum.freecodecamp.org/new-topic'
+ '?category=help'
+ '&title=' + window.encodeURIComponent(challengeName)
+ '&body=' + window.encodeURIComponent(textMessage),
'_blank'
);
return closeHelpModal();
});
}
export default combineEpics(
openIssueSearchEpic,
createIssueEpic,
openHelpChatRoomEpic,
createQuestionEpic
);