diff --git a/client/less/lib/bootstrap/modals.less b/client/less/lib/bootstrap/modals.less index 3671ff0715..e2a0c6309a 100755 --- a/client/less/lib/bootstrap/modals.less +++ b/client/less/lib/bootstrap/modals.less @@ -81,10 +81,6 @@ border-bottom: 1px solid @modal-header-border-color; min-height: (@modal-title-padding + @modal-title-line-height); } -// Close icon -.modal-header .close { - margin-top: -2px; -} // Title text within header .modal-title { diff --git a/client/less/main.less b/client/less/main.less index 8c4c3c3bd6..d02dd193ce 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -500,7 +500,7 @@ form.update-email .btn{ } } -.challenge-list-header { +.challenges-list-header { background-color: @brand-primary; color: @gray-lighter; font-size: 36px; @@ -825,7 +825,7 @@ code { color: @night-text-color; .btn-group, .text-success, - .challenge-list-header, + .challenges-list-header, .fcc-footer { background-color: @night-body-bg; } diff --git a/common/app/routes/Challenges/Help-Modal.jsx b/common/app/routes/Challenges/Help-Modal.jsx new file mode 100644 index 0000000000..fa6ad26931 --- /dev/null +++ b/common/app/routes/Challenges/Help-Modal.jsx @@ -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'; +import { RSA } from '../../../utils/constantStrings.json'; + +const mapStateToProps = state => ({ isOpen: helpModalSelector(state) }); +const mapDispatchToProps = { createQuestion, openHelpChatRoom, closeHelpModal }; + +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 ( + + + Ask for help? + + × + + + +

+ If you've already tried the Read-Search-Ask method, + then you can ask for help on the freeCodeCamp forum. +

+ + + + +
+
+ ); + } +} + +HelpModal.displayName = 'HelpModal'; +HelpModal.propTypes = propTypes; + +export default connect(mapStateToProps, mapDispatchToProps)(HelpModal); diff --git a/common/app/routes/Challenges/Side-Panel.jsx b/common/app/routes/Challenges/Side-Panel.jsx index 0faa9bd992..bc6d87d408 100644 --- a/common/app/routes/Challenges/Side-Panel.jsx +++ b/common/app/routes/Challenges/Side-Panel.jsx @@ -7,6 +7,7 @@ import { connect } from 'react-redux'; import ns from './ns.json'; import BugModal from './Bug-Modal.jsx'; +import HelpModal from './Help-Modal.jsx'; import ToolPanel from './Tool-Panel.jsx'; import ChallengeTitle from './Challenge-Title.jsx'; import ChallengeDescription from './Challenge-Description.jsx'; @@ -14,6 +15,7 @@ import TestSuite from './Test-Suite.jsx'; import Output from './Output.jsx'; import { openBugModal, + openHelpModal, updateHint, executeChallenge, unlockUntrustedCode, @@ -22,8 +24,7 @@ import { testsSelector, outputSelector, hintIndexSelector, - codeLockedSelector, - chatRoomSelector + codeLockedSelector } from './redux'; import { descriptionRegex } from './utils'; @@ -35,6 +36,7 @@ const mapDispatchToProps = { executeChallenge, updateHint, openBugModal, + openHelpModal, unlockUntrustedCode }; const mapStateToProps = createSelector( @@ -44,7 +46,6 @@ const mapStateToProps = createSelector( outputSelector, hintIndexSelector, codeLockedSelector, - chatRoomSelector, ( { description }, { title }, @@ -52,24 +53,22 @@ const mapStateToProps = createSelector( output, hintIndex, isCodeLocked, - helpChatRoom ) => ({ title, description, tests, output, - isCodeLocked, - helpChatRoom + isCodeLocked }) ); const propTypes = { description: PropTypes.arrayOf(PropTypes.string), executeChallenge: PropTypes.func, - helpChatRoom: PropTypes.string, hint: PropTypes.string, isCodeLocked: PropTypes.bool, makeToast: PropTypes.func, openBugModal: PropTypes.func, + openHelpModal: PropTypes.func, output: PropTypes.string, tests: PropTypes.arrayOf(PropTypes.object), title: PropTypes.string, @@ -125,8 +124,8 @@ export class SidePanel extends PureComponent { executeChallenge, updateHint, makeToast, - helpChatRoom, openBugModal, + openHelpModal, isCodeLocked, unlockUntrustedCode } = this.props; @@ -147,15 +146,16 @@ export class SidePanel extends PureComponent { + @@ -12,11 +12,11 @@ const unlockWarning = ( const propTypes = { executeChallenge: PropTypes.func.isRequired, - helpChatRoom: PropTypes.string, hint: PropTypes.string, isCodeLocked: PropTypes.bool, makeToast: PropTypes.func.isRequired, openBugModal: PropTypes.func.isRequired, + openHelpModal: PropTypes.func.isRequired, unlockUntrustedCode: PropTypes.func.isRequired, updateHint: PropTypes.func.isRequired }; @@ -93,12 +93,13 @@ export default class ToolPanel extends PureComponent { render() { const { executeChallenge, - helpChatRoom, hint, isCodeLocked, openBugModal, + openHelpModal, unlockUntrustedCode } = this.props; + return (
{ this.renderHint(hint, this.makeHint) } @@ -110,36 +111,32 @@ export default class ToolPanel extends PureComponent { ) }
- +
+
-
); diff --git a/common/app/routes/Challenges/challenges.less b/common/app/routes/Challenges/challenges.less index 1e8fdcf89f..0fbf31c5af 100644 --- a/common/app/routes/Challenges/challenges.less +++ b/common/app/routes/Challenges/challenges.less @@ -124,12 +124,12 @@ } @keyframes skeletonShimmer{ - 0% { - transform: translateX(-48px); - } - 100% { - transform: translateX(1000px); - } + 0% { + transform: translateX(-48px); + } + 100% { + transform: translateX(1000px); + } } .@{ns}-shimmer { diff --git a/common/app/routes/Challenges/redux/bug-epic.js b/common/app/routes/Challenges/redux/bug-epic.js deleted file mode 100644 index b75715605f..0000000000 --- a/common/app/routes/Challenges/redux/bug-epic.js +++ /dev/null @@ -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', - '\n\n\n', - '#### Browser Information\n', - '\n', - 'User Agent is: ', - userAgent, - '.\n\n', - '#### Screenshot\n', - '\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(); - }); -} diff --git a/common/app/routes/Challenges/redux/index.js b/common/app/routes/Challenges/redux/index.js index e758706287..3b3fadbbc8 100644 --- a/common/app/routes/Challenges/redux/index.js +++ b/common/app/routes/Challenges/redux/index.js @@ -10,7 +10,7 @@ import { import { createSelector } from 'reselect'; import noop from 'lodash/noop'; -import bugEpic from './bug-epic'; +import modalEpic from './modal-epic'; import completionEpic from './completion-epic.js'; import challengeEpic from './challenge-epic.js'; import executeChallengeEpic from './execute-challenge-epic.js'; @@ -44,7 +44,7 @@ const challengeToFilesMetaCreator = _.flow(challengeToFiles, createFilesMetaCreator); export const epics = [ - bugEpic, + modalEpic, challengeEpic, codeStorageEpic, completionEpic, @@ -83,6 +83,12 @@ export const types = createTypes([ 'openIssueSearch', 'createIssue', + // help + 'openHelpModal', + 'closeHelpModal', + 'createQuestion', + 'openHelpChatRoom', + // panes 'toggleClassicEditor', 'toggleMain', @@ -157,6 +163,12 @@ export const closeBugModal = createAction(types.closeBugModal); export const openIssueSearch = createAction(types.openIssueSearch); 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 export const storedCodeFound = createAction( types.storedCodeFound, @@ -174,6 +186,7 @@ const initialUiState = { output: null, isChallengeModalOpen: false, isBugOpen: false, + isHelpOpen: false, successMessage: 'Happy Coding!' }; @@ -206,6 +219,7 @@ export const challengeModalSelector = state => getNS(state).isChallengeModalOpen; export const bugModalSelector = state => getNS(state).isBugOpen; +export const helpModalSelector = state => getNS(state).isHelpOpen; export const challengeRequiredSelector = state => challengeSelector(state).required || []; @@ -318,9 +332,10 @@ export default combineReducers( ...state, output: (state.output || '') + output }), - [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, ns diff --git a/common/app/routes/Challenges/redux/modal-epic.js b/common/app/routes/Challenges/redux/modal-epic.js new file mode 100644 index 0000000000..8cbf28d503 --- /dev/null +++ b/common/app/routes/Challenges/redux/modal-epic.js @@ -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', + '\n\n\n', + '#### Browser Information\n', + '\n', + 'User Agent is: ', + userAgent, + '.\n\n', + '#### Screenshot\n', + '\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: ', + userAgent, + '.\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 +); diff --git a/common/utils/constantStrings.json b/common/utils/constantStrings.json index 37657970f1..e220d9e868 100644 --- a/common/utils/constantStrings.json +++ b/common/utils/constantStrings.json @@ -3,5 +3,6 @@ "defaultProfileImage": "https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png", "donateUrl": "https://www.freecodecamp.org/donate", "forumUrl": "https://forum.freecodecamp.org", - "githubUrl": "https://github.com/freecodecamp/freecodecamp" + "githubUrl": "https://github.com/freecodecamp/freecodecamp", + "RSA": "https://forum.freecodecamp.org/t/the-read-search-ask-methodology-for-getting-unstuck/137307" }