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