diff --git a/common/app/routes/challenges/components/Bug-Modal.jsx b/common/app/routes/challenges/components/Bug-Modal.jsx
new file mode 100644
index 0000000000..e6ad9a916d
--- /dev/null
+++ b/common/app/routes/challenges/components/Bug-Modal.jsx
@@ -0,0 +1,80 @@
+import React, { PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { Button, Modal } from 'react-bootstrap';
+import PureComponent from 'react-pure-render/component';
+import { createIssue, openIssueSearch, closeBugModal } from '../redux/actions';
+
+const mapStateToProps = state => ({ isOpen: state.challengesApp.isBugOpen });
+const actions = { createIssue, openIssueSearch, closeBugModal };
+const bugLink = 'https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/' +
+ 'FreeCodeCamp-Report-Bugs';
+
+export class BugModal extends PureComponent {
+ static propTypes = {
+ isOpen: PropTypes.bool,
+ closeBugModal: PropTypes.func,
+ openIssueSearch: PropTypes.func,
+ createIssue: PropTypes.func
+ };
+
+ render() {
+ const {
+ isOpen,
+ closeBugModal,
+ openIssueSearch,
+ createIssue
+ } = this.props;
+ return (
+
+
+ Did you find a bug?
+ ×
+
+
+
+ Before you submit a new issue,
+ read "Help I've Found a Bug" and
+ browse other issues with this challenge.
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default connect(mapStateToProps, actions)(BugModal);
diff --git a/common/app/routes/challenges/components/classic/Classic.jsx b/common/app/routes/challenges/components/classic/Classic.jsx
index b3a8a2c283..5021d9edfb 100644
--- a/common/app/routes/challenges/components/classic/Classic.jsx
+++ b/common/app/routes/challenges/components/classic/Classic.jsx
@@ -7,6 +7,7 @@ import PureComponent from 'react-pure-render/component';
import Editor from './Editor.jsx';
import SidePanel from './Side-Panel.jsx';
import Preview from './Preview.jsx';
+import BugModal from '../Bug-Modal.jsx';
import { challengeSelector } from '../../redux/selectors';
import {
executeChallenge,
@@ -102,6 +103,7 @@ export class Challenge extends PureComponent {
/>
{ this.renderPreview(showPreview) }
+
);
}
diff --git a/common/app/routes/challenges/components/classic/Side-Panel.jsx b/common/app/routes/challenges/components/classic/Side-Panel.jsx
index e6cf6f7f99..f329c98e5b 100644
--- a/common/app/routes/challenges/components/classic/Side-Panel.jsx
+++ b/common/app/routes/challenges/components/classic/Side-Panel.jsx
@@ -9,7 +9,11 @@ import TestSuite from './Test-Suite.jsx';
import Output from './Output.jsx';
import ToolPanel from './Tool-Panel.jsx';
import { challengeSelector } from '../../redux/selectors';
-import { updateHint, executeChallenge } from '../../redux/actions';
+import {
+ openBugModal,
+ updateHint,
+ executeChallenge
+} from '../../redux/actions';
import { makeToast } from '../../../../toasts/redux/actions';
import { toggleHelpChat } from '../../../../redux/actions';
@@ -17,7 +21,8 @@ const bindableActions = {
makeToast,
executeChallenge,
updateHint,
- toggleHelpChat
+ toggleHelpChat,
+ openBugModal
};
const mapStateToProps = createSelector(
challengeSelector,
@@ -59,7 +64,8 @@ export class SidePanel extends PureComponent {
hints: PropTypes.string,
updateHint: PropTypes.func,
makeToast: PropTypes.func,
- toggleHelpChat: PropTypes.func
+ toggleHelpChat: PropTypes.func,
+ openBugModal: PropTypes.func
};
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
@@ -99,7 +105,8 @@ export class SidePanel extends PureComponent {
executeChallenge,
updateHint,
makeToast,
- toggleHelpChat
+ toggleHelpChat,
+ openBugModal
} = this.props;
const style = {
overflowX: 'hidden',
@@ -131,6 +138,7 @@ export class SidePanel extends PureComponent {
executeChallenge={ executeChallenge }
hint={ hint }
makeToast={ makeToast }
+ openBugModal={ openBugModal }
toggleHelpChat={ toggleHelpChat }
updateHint={ updateHint }
/>
diff --git a/common/app/routes/challenges/components/classic/Tool-Panel.jsx b/common/app/routes/challenges/components/classic/Tool-Panel.jsx
index 7dc3119a55..adabc3e94a 100644
--- a/common/app/routes/challenges/components/classic/Tool-Panel.jsx
+++ b/common/app/routes/challenges/components/classic/Tool-Panel.jsx
@@ -14,7 +14,8 @@ export default class ToolPanel extends PureComponent {
executeChallenge: PropTypes.func,
updateHint: PropTypes.func,
hint: PropTypes.string,
- toggleHelpChat: PropTypes.func
+ toggleHelpChat: PropTypes.func,
+ openBugModal: PropTypes.func
};
makeHint() {
@@ -54,7 +55,8 @@ export default class ToolPanel extends PureComponent {
const {
hint,
executeChallenge,
- toggleHelpChat
+ toggleHelpChat,
+ openBugModal
} = this.props;
return (
@@ -92,6 +94,7 @@ export default class ToolPanel extends PureComponent {
bsSize='large'
bsStyle='primary'
componentClass='label'
+ onClick={ openBugModal }
>
Bug
diff --git a/common/app/routes/challenges/components/project/Project.jsx b/common/app/routes/challenges/components/project/Project.jsx
index 7e79b83505..d995780efd 100644
--- a/common/app/routes/challenges/components/project/Project.jsx
+++ b/common/app/routes/challenges/components/project/Project.jsx
@@ -7,6 +7,7 @@ import PureComponent from 'react-pure-render/component';
import { Col } from 'react-bootstrap';
import SidePanel from './Side-Panel.jsx';
import ToolPanel from './Tool-Panel.jsx';
+import BugModal from '../Bug-Modal.jsx';
import { challengeSelector } from '../../redux/selectors';
@@ -71,6 +72,7 @@ export class Project extends PureComponent {
+
);
diff --git a/common/app/routes/challenges/components/project/Tool-Panel.jsx b/common/app/routes/challenges/components/project/Tool-Panel.jsx
index 3fceff48eb..1533ad6d4c 100644
--- a/common/app/routes/challenges/components/project/Tool-Panel.jsx
+++ b/common/app/routes/challenges/components/project/Tool-Panel.jsx
@@ -9,7 +9,7 @@ import {
BackEndForm
} from './Forms.jsx';
-import { submitChallenge } from '../../redux/actions';
+import { submitChallenge, openBugModal } from '../../redux/actions';
import { challengeSelector } from '../../redux/selectors';
import {
simpleProject,
@@ -19,7 +19,8 @@ import { toggleHelpChat } from '../../../../redux/actions';
const bindableActions = {
submitChallenge,
- toggleHelpChat
+ toggleHelpChat,
+ openBugModal
};
const mapStateToProps = createSelector(
challengeSelector,
@@ -43,7 +44,8 @@ export class ToolPanel extends PureComponent {
isSimple: PropTypes.bool,
isFrontEnd: PropTypes.bool,
isSubmitting: PropTypes.bool,
- toggleHelpChat: PropTypes.func
+ toggleHelpChat: PropTypes.func,
+ openBugModal: PropTypes.func
};
renderSubmitButton(isSignedIn, submitChallenge) {
@@ -69,7 +71,8 @@ export class ToolPanel extends PureComponent {
isSignedIn,
isSubmitting,
submitChallenge,
- toggleHelpChat
+ toggleHelpChat,
+ openBugModal
} = this.props;
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
@@ -94,6 +97,7 @@ export class ToolPanel extends PureComponent {
bsStyle='primary'
className='btn-primary-ghost btn-big'
componentClass='div'
+ onClick={ openBugModal }
>
Bug
diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js
index de3ddb5530..c8b2774ae2 100644
--- a/common/app/routes/challenges/redux/actions.js
+++ b/common/app/routes/challenges/redux/actions.js
@@ -139,3 +139,9 @@ export const endShake = createAction(types.primeNextQuestion);
export const goToNextQuestion = createAction(types.goToNextQuestion);
export const videoCompleted = createAction(types.videoCompleted);
+
+// bug
+export const openBugModal = createAction(types.openBugModal);
+export const closeBugModal = createAction(types.closeBugModal);
+export const openIssueSearch = createAction(types.openIssueSearch);
+export const createIssue = createAction(types.createIssue);
diff --git a/common/app/routes/challenges/redux/bug-saga.js b/common/app/routes/challenges/redux/bug-saga.js
new file mode 100644
index 0000000000..60e876ce86
--- /dev/null
+++ b/common/app/routes/challenges/redux/bug-saga.js
@@ -0,0 +1,72 @@
+import dedent from 'dedent';
+
+import types from '../redux/types';
+import { closeBugModal } from '../redux/actions';
+
+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 + dedent`
+ \`\`\`${fileType}
+ ${fileName}
+ ${file.contents}
+ \`\`\`
+ \n
+ `;
+ }, '\n');
+}
+
+export default function bugSaga(actions$, getState, { window }) {
+ return actions$
+ .filter(({ type }) => (
+ type === types.openIssueSearch ||
+ type === types.createIssue
+ ))
+ .map(({ type }) => {
+ const {
+ challengesApp: {
+ challenge: challengeName,
+ files
+ }
+ } = getState();
+ const {
+ navigator: { userAgent },
+ location: { href }
+ } = window;
+ if (type === types.openIssueSearch) {
+ window.open(
+ 'https://github.com/FreeCodeCamp/FreeCodeCamp/issues?q=' +
+ 'is:issue is:all ' +
+ challengeName
+ );
+ }
+ let textMessage = [
+ 'Challenge [',
+ challengeName,
+ '](',
+ href,
+ ') has an issue.\n',
+ 'User Agent is: ',
+ userAgent,
+ '
.\n',
+ 'Please describe how to reproduce this issue, and include ',
+ 'links to screenshots if possible.\n\n'
+ ].join('');
+ const body = filesToMarkdown(files);
+ if (body.length > 10) {
+ textMessage = textMessage + body;
+ }
+ window.open(
+ 'https://github.com/freecodecamp/freecodecamp/issues/new?&body=' +
+ window.encodeURIComponent(textMessage),
+ '_blank'
+ );
+ return closeBugModal();
+ });
+}
diff --git a/common/app/routes/challenges/redux/index.js b/common/app/routes/challenges/redux/index.js
index 1f33c002e9..b5ff33b4b9 100644
--- a/common/app/routes/challenges/redux/index.js
+++ b/common/app/routes/challenges/redux/index.js
@@ -3,6 +3,7 @@ import completionSaga from './completion-saga';
import nextChallengeSaga from './next-challenge-saga';
import answerSaga from './answer-saga';
import resetChallengeSaga from './reset-challenge-saga';
+import bugSaga from './bug-saga';
export * as actions from './actions';
export reducer from './reducer';
@@ -15,5 +16,6 @@ export const sagas = [
completionSaga,
nextChallengeSaga,
answerSaga,
- resetChallengeSaga
+ resetChallengeSaga,
+ bugSaga
];
diff --git a/common/app/routes/challenges/redux/reducer.js b/common/app/routes/challenges/redux/reducer.js
index 930e74ae78..3b8488e8ec 100644
--- a/common/app/routes/challenges/redux/reducer.js
+++ b/common/app/routes/challenges/redux/reducer.js
@@ -43,6 +43,7 @@ const initialState = {
id: '',
challenge: '',
helpChatRoom: 'Help',
+ isBugOpen: false,
// old code storage key
legacyKey: '',
files: {},
@@ -185,7 +186,10 @@ const mainReducer = handleActions(
isPressed: false,
delta: [ 0, 0 ],
mouse: [ userAnswer ? 1000 : -1000, 0]
- })
+ }),
+
+ [types.openBugModal]: state => ({ ...state, isBugOpen: true }),
+ [types.closeBugModal]: state => ({ ...state, isBugOpen: false })
},
initialState
);
diff --git a/common/app/routes/challenges/redux/types.js b/common/app/routes/challenges/redux/types.js
index e36b2e89a2..5fdb679724 100644
--- a/common/app/routes/challenges/redux/types.js
+++ b/common/app/routes/challenges/redux/types.js
@@ -64,5 +64,11 @@ export default createTypes([
'primeNextQuestion',
'goToNextQuestion',
'transitionVideo',
- 'videoCompleted'
+ 'videoCompleted',
+
+ // bug
+ 'openBugModal',
+ 'closeBugModal',
+ 'openIssueSearch',
+ 'createIssue'
], 'challenges');