diff --git a/client/less/classic-modal.less b/client/less/classic-modal.less new file mode 100644 index 0000000000..0434162d94 --- /dev/null +++ b/client/less/classic-modal.less @@ -0,0 +1,34 @@ +.challenge-success-modal { + display: flex; + flex-direction: column; + justify-content: center; + height: 50vh; + + .modal-header { + margin-bottom: 0; + + .close { + color: #eee; + font-size: 4rem; + opacity: 0.6; + transition: all 300ms ease-out; + margin-top: 0; + padding-left: 0; + + &:hover { + opacity: 1; + } + } + } + + .modal-body { + padding: 35px; + display: flex; + flex-direction: column; + justify-content: center; + + .fa { + margin-right: 0; + } + } +} diff --git a/client/less/main.less b/client/less/main.less index c5e8895749..b245442a9c 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -1203,3 +1203,4 @@ and (max-width : 400px) { @import "map.less"; @import "drawers.less"; @import "sk-wave.less"; +@import "classic-modal.less"; diff --git a/common/app/routes/challenges/components/Classic-Modal.jsx b/common/app/routes/challenges/components/Classic-Modal.jsx new file mode 100644 index 0000000000..85ff2c39b8 --- /dev/null +++ b/common/app/routes/challenges/components/Classic-Modal.jsx @@ -0,0 +1,81 @@ +import React, { PropTypes } from 'react'; +import { Button, Modal } from 'react-bootstrap'; +import PureComponent from 'react-pure-render/component'; +import FontAwesome from 'react-fontawesome'; + +const propTypes = { + close: PropTypes.func, + open: PropTypes.bool.isRequired, + submitChallenge: PropTypes.func.isRequired, + successMessage: PropTypes.string.isRequired +}; + +export default class ClassicModal extends PureComponent { + constructor(...props) { + super(...props); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + handleKeyDown(e) { + const { open, submitChallenge } = this.props; + if ( + e.keyCode === 13 && + (e.ctrlKey || e.meta) && + open + ) { + e.preventDefault(); + submitChallenge(); + } + } + + render() { + const { + close, + open, + submitChallenge, + successMessage + } = this.props; + return ( + + + { successMessage } + + +
+
+
+ +
+
+
+
+ + + +
+ ); + } +} + +ClassicModal.displayName = 'ClassicModal'; +ClassicModal.propTypes = propTypes; diff --git a/common/app/routes/challenges/components/classic/Classic.jsx b/common/app/routes/challenges/components/classic/Classic.jsx index 5d1617ea02..01b78f94d0 100644 --- a/common/app/routes/challenges/components/classic/Classic.jsx +++ b/common/app/routes/challenges/components/classic/Classic.jsx @@ -8,12 +8,17 @@ import Editor from './Editor.jsx'; import SidePanel from './Side-Panel.jsx'; import Preview from './Preview.jsx'; import BugModal from '../Bug-Modal.jsx'; +import ClassicModal from '../Classic-Modal.jsx'; import { challengeSelector } from '../../redux/selectors'; import { executeChallenge, updateFile, - loadCode + loadCode, + submitChallenge, + closeChallengeModal, + updateSuccessMessage } from '../../redux/actions'; +import { randomCompliment } from '../../../../utils/get-words'; const mapStateToProps = createSelector( challengeSelector, @@ -21,26 +26,35 @@ const mapStateToProps = createSelector( state => state.challengesApp.tests, state => state.challengesApp.files, state => state.challengesApp.key, + state => state.challengesApp.isChallengeModalOpen, + state => state.challengesApp.successMessage, ( { showPreview, mode }, id, tests, files = {}, - key = '' + key = '', + isChallengeModalOpen, + successMessage, ) => ({ id, content: files[key] && files[key].contents || '', file: files[key], showPreview, mode, - tests + tests, + isChallengeModalOpen, + successMessage }) ); const bindableActions = { executeChallenge, updateFile, - loadCode + loadCode, + submitChallenge, + closeChallengeModal, + updateSuccessMessage }; export class Challenge extends PureComponent { @@ -54,16 +68,23 @@ export class Challenge extends PureComponent { file: PropTypes.object, updateFile: PropTypes.func, executeChallenge: PropTypes.func, - loadCode: PropTypes.func + loadCode: PropTypes.func, + submitChallenge: PropTypes.func, + isChallengeModalOpen: PropTypes.bool, + closeChallengeModal: PropTypes.func, + successMessage: PropTypes.string, + updateSuccessMessage: PropTypes.func }; componentDidMount() { this.props.loadCode(); + this.props.updateSuccessMessage(randomCompliment()); } componentWillReceiveProps(nextProps) { if (this.props.id !== nextProps.id) { this.props.loadCode(); + this.props.updateSuccessMessage(randomCompliment()); } } @@ -88,8 +109,13 @@ export class Challenge extends PureComponent { file, mode, showPreview, - executeChallenge + executeChallenge, + submitChallenge, + successMessage, + isChallengeModalOpen, + closeChallengeModal } = this.props; + return (
{ this.renderPreview(showPreview) } +
); } diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js index d4bcb0a421..f258f48fff 100644 --- a/common/app/routes/challenges/redux/actions.js +++ b/common/app/routes/challenges/redux/actions.js @@ -22,6 +22,7 @@ export const fetchChallengeCompleted = createAction( (_, challenge) => challenge, entities => ({ entities }) ); +export const closeChallengeModal = createAction(types.closeChallengeModal); export const resetUi = createAction(types.resetUi); export const updateHint = createAction(types.updateHint); export const lockUntrustedCode = createAction(types.lockUntrustedCode); @@ -29,7 +30,7 @@ export const unlockUntrustedCode = createAction( types.unlockUntrustedCode, () => null ); - +export const updateSuccessMessage = createAction(types.updateSuccessMessage); export const fetchChallenges = createAction(types.fetchChallenges); export const fetchChallengesCompleted = createAction( types.fetchChallengesCompleted, diff --git a/common/app/routes/challenges/redux/completion-saga.js b/common/app/routes/challenges/redux/completion-saga.js index fdf3bc6861..0a92703fec 100644 --- a/common/app/routes/challenges/redux/completion-saga.js +++ b/common/app/routes/challenges/redux/completion-saga.js @@ -7,7 +7,6 @@ import { } from './actions'; import { challengeSelector } from './selectors'; -import { randomCompliment } from '../../../utils/get-words'; import { createErrorObservable, updateUserPoints, @@ -40,14 +39,7 @@ function submitModern(type, state) { const { tests } = state.challengesApp; if (tests.length > 0 && tests.every(test => test.pass && !test.err)) { if (type === types.checkChallenge) { - return Observable.of( - makeToast({ - message: `${randomCompliment()} Go to your next challenge.`, - action: 'Submit', - actionCreator: 'submitChallenge', - timeout: 10000 - }) - ); + return Observable.just(null); } if (type === types.submitChallenge) { diff --git a/common/app/routes/challenges/redux/fetch-challenges-saga.js b/common/app/routes/challenges/redux/fetch-challenges-saga.js index 63f41f999e..95413a28e0 100644 --- a/common/app/routes/challenges/redux/fetch-challenges-saga.js +++ b/common/app/routes/challenges/redux/fetch-challenges-saga.js @@ -1,5 +1,6 @@ import { Observable } from 'rx'; import debug from 'debug'; + import { challengeSelector } from './selectors'; import types from './types'; import { diff --git a/common/app/routes/challenges/redux/reducer.js b/common/app/routes/challenges/redux/reducer.js index ed9bd3f821..54ab917e4c 100644 --- a/common/app/routes/challenges/redux/reducer.js +++ b/common/app/routes/challenges/redux/reducer.js @@ -42,7 +42,9 @@ const initialUiState = { isPressed: false, isCorrect: false, shouldShakeQuestion: false, - shouldShowQuestions: false + shouldShowQuestions: false, + isChallengeModalOpen: false, + successMessage: 'Happy Coding!' }; const initialState = { isCodeLocked: false, @@ -81,7 +83,19 @@ const mainReducer = handleActions( }), [types.updateTests]: (state, { payload: tests }) => ({ ...state, - tests + tests, + isChallengeModalOpen: ( + tests.length > 0 && + tests.every(test => test.pass && !test.err) + ) + }), + [types.closeChallengeModal]: state => ({ + ...state, + isChallengeModalOpen: false + }), + [types.updateSuccessMessage]: (state, { payload }) => ({ + ...state, + successMessage: payload }), [types.updateHint]: state => ({ ...state, diff --git a/common/app/routes/challenges/redux/types.js b/common/app/routes/challenges/redux/types.js index 18772e0415..a4deb334a3 100644 --- a/common/app/routes/challenges/redux/types.js +++ b/common/app/routes/challenges/redux/types.js @@ -21,6 +21,8 @@ export default createTypes([ 'updateHint', 'lockUntrustedCode', 'unlockUntrustedCode', + 'closeChallengeModal', + 'updateSuccessMessage', // map 'updateFilter',