diff --git a/client/src/assets/icons/Heart.js b/client/src/assets/icons/Heart.js new file mode 100644 index 0000000000..a2706d8d6e --- /dev/null +++ b/client/src/assets/icons/Heart.js @@ -0,0 +1,38 @@ +import React, { Fragment } from 'react'; + +const propTypes = {}; + +function Heart(props) { + return ( + + Heart + + + + + + + + + + + ); +} + +Heart.displayName = 'Heart'; +Heart.propTypes = propTypes; + +export default Heart; diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/ShowCertification.js index 6455f5df8a..41267ef498 100644 --- a/client/src/client-only-routes/ShowCertification.js +++ b/client/src/client-only-routes/ShowCertification.js @@ -8,7 +8,12 @@ import { Grid, Row, Col, Image } from '@freecodecamp/react-bootstrap'; import { showCertSelector, showCertFetchStateSelector, - showCert + showCert, + userFetchStateSelector, + usernameSelector, + isDonatingSelector, + isDonationRequestedSelector, + preventDonationRequests } from '../redux'; import validCertNames from '../../utils/validCertNames'; import { createFlashMessage } from '../components/Flash/redux'; @@ -16,7 +21,7 @@ import standardErrorMessage from '../utils/standardErrorMessage'; import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage'; import RedirectHome from '../components/RedirectHome'; -import { Loader } from '../components/helpers'; +import { Loader, Link } from '../components/helpers'; const propTypes = { cert: PropTypes.shape({ @@ -35,8 +40,15 @@ const propTypes = { complete: PropTypes.bool, errored: PropTypes.bool }), + isDonating: PropTypes.bool, + isDonationRequested: PropTypes.bool, issueDate: PropTypes.string, + preventDonationRequests: PropTypes.func, showCert: PropTypes.func.isRequired, + signedInUserName: PropTypes.string, + userFetchState: PropTypes.shape({ + complete: PropTypes.bool + }), userFullName: PropTypes.string, username: PropTypes.string, validCertName: PropTypes.bool @@ -47,16 +59,34 @@ const mapStateToProps = (state, { certName }) => { return createSelector( showCertSelector, showCertFetchStateSelector, - (cert, fetchState) => ({ + usernameSelector, + userFetchStateSelector, + isDonatingSelector, + isDonationRequestedSelector, + ( cert, fetchState, - validCertName + signedInUserName, + userFetchState, + isDonating, + isDonationRequested + ) => ({ + cert, + fetchState, + validCertName, + signedInUserName, + userFetchState, + isDonating, + isDonationRequested }) ); }; const mapDispatchToProps = dispatch => - bindActionCreators({ createFlashMessage, showCert }, dispatch); + bindActionCreators( + { createFlashMessage, showCert, preventDonationRequests }, + dispatch + ); class ShowCertification extends Component { componentDidMount() { @@ -72,7 +102,12 @@ class ShowCertification extends Component { fetchState, validCertName, createFlashMessage, - certName + certName, + preventDonationRequests, + signedInUserName, + isDonating, + isDonationRequested, + userFetchState } = this.props; if (!validCertName) { @@ -81,6 +116,7 @@ class ShowCertification extends Component { } const { pending, complete, errored } = fetchState; + const { complete: userComplete } = userFetchState; if (pending) { return ; @@ -103,8 +139,40 @@ class ShowCertification extends Component { certTitle, completionTime } = cert; + + let conditionalDonationMessage = ''; + + if ( + userComplete && + signedInUserName === username && + !isDonating && + !isDonationRequested + ) { + conditionalDonationMessage = ( + + +

+ Only you can see this message. Congratulations on earning this + certification. It’s no easy task. Running freeCodeCamp isn’t easy + either. Nor is it cheap. Help us help you and many other people + around the world. Make a tax-deductible supporting donation to our + nonprofit today. +

+ + Check out our donation dashboard + +
+
+ ); + } + return (
+ {conditionalDonationMessage}
diff --git a/client/src/components/Donation/Donation.css b/client/src/components/Donation/Donation.css index 09e14ef9fb..f557552acc 100644 --- a/client/src/components/Donation/Donation.css +++ b/client/src/components/Donation/Donation.css @@ -162,3 +162,59 @@ li.disabled > a { .servicebot-embed-panel .panel { padding: 20px; } + +.heart-icon-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 40px; +} + +.heart-icon { + width: 150px; + height: auto; + transform: scale(1.5); + opacity: 0; + animation: heart-icon-animation 1s linear 100ms forwards; +} + +@keyframes heart-icon-animation { + 33% { + transform: scale(1.2); + } + 66% { + transform: scale(1.25); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.donation-modal p { + margin: 0; + text-align: center; + font-weight: 700; + font-size: 1.2rem; +} + +.donation-modal .modal-title { + text-align: center; + font-weight: 700; + font-size: 1.5rem; +} + +@media screen and (max-width: 991px) { + .heart-icon-container { + margin: 30px; + } + .donation-modal p { + font-weight: 400; + font-size: 1rem; + } + .donation-modal .modal-title { + font-weight: 600; + font-size: 1.2rem; + } +} diff --git a/client/src/components/Donation/components/DonationModal.js b/client/src/components/Donation/components/DonationModal.js new file mode 100644 index 0000000000..400063ed93 --- /dev/null +++ b/client/src/components/Donation/components/DonationModal.js @@ -0,0 +1,98 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { Modal, Button } from '@freecodecamp/react-bootstrap'; +import { Link } from '../../../components/helpers'; +import { blockNameify } from '../../../../utils/blockNameify'; +import Heart from '../../../assets/icons/Heart'; + +import ga from '../../../analytics'; +import { + closeDonationModal, + isDonationModalOpenSelector +} from '../../../redux'; + +import { challengeMetaSelector } from '../../../templates/Challenges/redux'; + +import '../Donation.css'; + +const mapStateToProps = createSelector( + isDonationModalOpenSelector, + challengeMetaSelector, + (show, { block }) => ({ + show, + block + }) +); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + closeDonationModal + }, + dispatch + ); + +const propTypes = { + block: PropTypes.string, + closeDonationModal: PropTypes.func.isRequired, + show: PropTypes.bool +}; + +class DonateModal extends Component { + render() { + const { show, block } = this.props; + if (show) { + ga.modalview('/donation-modal'); + } + + return ( + + + + Support freeCodeCamp.org + + + +

+ Nicely done. You just completed {blockNameify(block)}. +

+
+ +
+

+ Help us create even more learning resources like this. +

+
+ + + Support our nonprofit + + + +
+ ); + } +} + +DonateModal.displayName = 'DonateModal'; +DonateModal.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DonateModal); diff --git a/client/src/components/layouts/Certification.js b/client/src/components/layouts/Certification.js index 720c2ad6ac..fc113724d9 100644 --- a/client/src/components/layouts/Certification.js +++ b/client/src/components/layouts/Certification.js @@ -1,11 +1,42 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; -function CertificationLayout({ children }) { - return {children}; +import ga from '../../analytics'; +import { fetchUser, isSignedInSelector } from '../../redux'; +import { createSelector } from 'reselect'; + +const mapStateToProps = createSelector( + isSignedInSelector, + isSignedIn => ({ + isSignedIn + }) +); + +const mapDispatchToProps = { fetchUser }; + +class CertificationLayout extends Component { + componentDidMount() { + const { isSignedIn, fetchUser, pathname } = this.props; + if (!isSignedIn) { + fetchUser(); + } + ga.pageview(pathname); + } + render() { + return {this.props.children}; + } } CertificationLayout.displayName = 'CertificationLayout'; -CertificationLayout.propTypes = { children: PropTypes.any }; +CertificationLayout.propTypes = { + children: PropTypes.any, + fetchUser: PropTypes.func.isRequired, + isSignedIn: PropTypes.bool, + pathname: PropTypes.string.isRequired +}; -export default CertificationLayout; +export default connect( + mapStateToProps, + mapDispatchToProps +)(CertificationLayout); diff --git a/client/src/components/layouts/Learn.js b/client/src/components/layouts/Learn.js index 89f86c8695..945d3172e0 100644 --- a/client/src/components/layouts/Learn.js +++ b/client/src/components/layouts/Learn.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { createSelector } from 'reselect'; import { connect } from 'react-redux'; @@ -7,9 +7,11 @@ import { Loader } from '../../components/helpers'; import { userSelector, userFetchStateSelector, - isSignedInSelector + isSignedInSelector, + tryToShowDonationModal } from '../../redux'; import createRedirect from '../../components/createRedirect'; +import DonateModal from '../Donation/components/DonationModal'; import 'prismjs/themes/prism.css'; import './prism.css'; @@ -28,27 +30,40 @@ const mapStateToProps = createSelector( }) ); +const mapDispatchToProps = { + tryToShowDonationModal +}; + const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms'); -function LearnLayout({ - fetchState: { pending, complete }, - isSignedIn, - user: { acceptedPrivacyTerms }, - children -}) { - if (pending && !complete) { - return ; +class LearnLayout extends Component { + componentDidMount() { + this.props.tryToShowDonationModal(); } - if (isSignedIn && !acceptedPrivacyTerms) { - return ; - } + render() { + const { + fetchState: { pending, complete }, + isSignedIn, + user: { acceptedPrivacyTerms }, + children + } = this.props; - return ( - -
{children}
-
- ); + if (pending && !complete) { + return ; + } + + if (isSignedIn && !acceptedPrivacyTerms) { + return ; + } + + return ( + +
{children}
+ +
+ ); + } } LearnLayout.displayName = 'LearnLayout'; @@ -60,9 +75,13 @@ LearnLayout.propTypes = { errored: PropTypes.bool }), isSignedIn: PropTypes.bool, + tryToShowDonationModal: PropTypes.func.isRequired, user: PropTypes.shape({ acceptedPrivacyTerms: PropTypes.bool }) }; -export default connect(mapStateToProps)(LearnLayout); +export default connect( + mapStateToProps, + mapDispatchToProps +)(LearnLayout); diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css index a809ad24c6..d4d8970b76 100644 --- a/client/src/components/layouts/global.css +++ b/client/src/components/layouts/global.css @@ -208,18 +208,19 @@ fieldset[disabled] .btn-primary.focus { } .btn-cta { - background-color: #ffac33; - background-image: linear-gradient(#ffcc4d, #ffac33); - border-color: #f1a02a; + background-color: #feac32; + background-image: linear-gradient(#fecc4c, #ffac33); + border-width: 3px; + border-color: #feac32; color: #0a0a23 !important; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - border: none; } .btn-cta:hover, .btn-cta:focus, .btn-cta:active:hover { - background-color: #e99110; - background-image: linear-gradient(#ffcc4d, #e99110); + background-color: #fecc4c; + border-width: 3px; + border-color: #f1a02a; + background-image: none; color: #0a0a23 !important; } .btn-cta:active { diff --git a/client/src/components/layouts/variables.css b/client/src/components/layouts/variables.css index 5ee0bb4211..529c6dfe99 100644 --- a/client/src/components/layouts/variables.css +++ b/client/src/components/layouts/variables.css @@ -19,6 +19,8 @@ --green-dark: #00471b; --red-light: #ffadad; --red-dark: #850000; + --love-light: #f8577c; + --love-dark: #f82153; } .dark-palette { @@ -36,6 +38,7 @@ --success-background: var(--green-dark); --danger-color: var(--red-light); --danger-background: var(--red-dark); + --love-color: var(--love-light); } .light-palette { @@ -53,4 +56,5 @@ --success-background: var(--green-light); --danger-color: var(--red-dark); --danger-background: var(--red-light); + --love-color: var(--love-dark); } diff --git a/client/src/pages/certification.css b/client/src/pages/certification.css index 5f4c3fab52..351fcb3b98 100644 --- a/client/src/pages/certification.css +++ b/client/src/pages/certification.css @@ -35,6 +35,23 @@ display: flex; align-items: center; min-height: 100vh; + flex-direction: column; +} + +.certificate-outer-wrapper .certification-donation { + padding: 15px 0px; +} + +.certificate-outer-wrapper .certification-donation .btn { + background-color: var(--gray-15); + border-color: var(--gray-85); + color: var(--gray-85); +} + +.certificate-outer-wrapper .certification-donation .btn:hover { + border-color: var(--gray-85); + background-color: var(--gray-85); + color: var(--gray-05); } .certification-namespace header { diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js new file mode 100644 index 0000000000..f486c5e56d --- /dev/null +++ b/client/src/redux/donation-saga.js @@ -0,0 +1,20 @@ +import { put, select, takeEvery, delay } from 'redux-saga/effects'; + +import { + openDonationModal, + shouldRequestDonationSelector, + preventDonationRequests +} from './'; + +function* showDonateModalSaga() { + let shouldRequestDonation = yield select(shouldRequestDonationSelector); + if (shouldRequestDonation) { + yield delay(200); + yield put(openDonationModal()); + yield put(preventDonationRequests()); + } +} + +export function createDonationSaga(types) { + return [takeEvery(types.tryToShowDonationModal, showDonateModalSaga)]; +} diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 07349644f7..ed21b69934 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -10,6 +10,7 @@ import { createAppMountSaga } from './app-mount-saga'; import { createReportUserSaga } from './report-user-saga'; import { createShowCertSaga } from './show-cert-saga'; import { createNightModeSaga } from './night-mode-saga'; +import { createDonationSaga } from './donation-saga'; import hardGoToEpic from './hard-go-to-epic'; import failedUpdatesEpic from './failed-updates-epic'; @@ -31,6 +32,7 @@ export const defaultFetchState = { const initialState = { appUsername: '', + canRequestDonation: false, completionCount: 0, currentChallengeId: store.get(CURRENT_CHALLENGE_KEY), donationRequested: false, @@ -53,12 +55,14 @@ const initialState = { export const types = createTypes( [ 'appMount', - 'closeDonationModal', - 'donationRequested', 'hardGoTo', + 'allowDonationRequests', + 'closeDonationModal', + 'preventDonationRequests', 'openDonationModal', 'onlineStatusChange', 'resetUserData', + 'tryToShowDonationModal', 'submitComplete', 'updateComplete', 'updateCurrentChallengeId', @@ -77,6 +81,7 @@ export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic]; export const sagas = [ ...createAcceptTermsSaga(types), ...createAppMountSaga(types), + ...createDonationSaga(types), ...createFetchUserSaga(types), ...createShowCertSaga(types), ...createReportUserSaga(types), @@ -85,9 +90,15 @@ export const sagas = [ export const appMount = createAction(types.appMount); +export const tryToShowDonationModal = createAction( + types.tryToShowDonationModal +); +export const allowDonationRequests = createAction(types.allowDonationRequests); export const closeDonationModal = createAction(types.closeDonationModal); export const openDonationModal = createAction(types.openDonationModal); -export const donationRequested = createAction(types.donationRequested); +export const preventDonationRequests = createAction( + types.preventDonationRequests +); export const onlineStatusChange = createAction(types.onlineStatusChange); @@ -134,7 +145,8 @@ export const completedChallengesSelector = state => userSelector(state).completedChallenges || []; export const completionCountSelector = state => state[ns].completionCount; export const currentChallengeIdSelector = state => state[ns].currentChallengeId; -export const donationRequestedSelector = state => state[ns].donationRequested; +export const isDonationRequestedSelector = state => state[ns].donationRequested; +export const isDonatingSelector = state => userSelector(state).isDonating; export const isOnlineSelector = state => state[ns].isOnline; export const isSignedInSelector = state => !!state[ns].appUsername; @@ -145,21 +157,14 @@ export const signInLoadingSelector = state => export const showCertSelector = state => state[ns].showCert; export const showCertFetchStateSelector = state => state[ns].showCertFetchState; -export const showDonationSelector = state => { - const completedChallenges = completedChallengesSelector(state); - const completionCount = completionCountSelector(state); - const currentCompletedLength = completedChallenges.length; - const donationRequested = donationRequestedSelector(state); - // the user has not completed 9 challenges in total yet - if (currentCompletedLength < 9) { - return false; - } - // this will mean we are on the 10th submission in total for the user - if (completedChallenges.length === 9 && donationRequested === false) { - return true; - } - // this will mean we are on the 3rd submission for this browser session - if (completionCount === 2 && donationRequested === false) { +export const shouldRequestDonationSelector = state => { + const isDonationRequested = isDonationRequestedSelector(state); + const isDonating = isDonatingSelector(state); + if ( + isDonationRequested === false && + isDonating === false && + state[ns].canRequestDonation + ) { return true; } return false; @@ -209,6 +214,10 @@ function spreadThePayloadOnUser(state, payload) { export const reducer = handleActions( { + [types.allowDonationRequests]: state => ({ + ...state, + canRequestDonation: true + }), [types.fetchUser]: state => ({ ...state, userFetchState: { ...defaultFetchState } @@ -288,7 +297,7 @@ export const reducer = handleActions( ...state, showDonationModal: true }), - [types.donationRequested]: state => ({ + [types.preventDonationRequests]: state => ({ ...state, donationRequested: true }), diff --git a/client/src/templates/Challenges/components/CompletionModal.js b/client/src/templates/Challenges/components/CompletionModal.js index 91d77ff5f4..e94684fb5a 100644 --- a/client/src/templates/Challenges/components/CompletionModal.js +++ b/client/src/templates/Challenges/components/CompletionModal.js @@ -21,7 +21,8 @@ import { isCompletionModalOpenSelector, successMessageSelector, challengeFilesSelector, - challengeMetaSelector + challengeMetaSelector, + lastBlockChalSubmitted } from '../redux'; import { isSignedInSelector } from '../../../redux'; @@ -54,17 +55,11 @@ const mapStateToProps = createSelector( const mapDispatchToProps = function(dispatch) { const dispatchers = { close: () => dispatch(closeModal('completion')), - handleKeypress: e => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - // Since Hotkeys also listens to Ctrl + Enter we have to stop this event - // getting to it. - e.stopPropagation(); - dispatch(submitChallenge()); - } - }, submitChallenge: () => { dispatch(submitChallenge()); + }, + lastBlockChalSubmitted: () => { + dispatch(lastBlockChalSubmitted()); } }; return () => dispatchers; @@ -76,18 +71,18 @@ const propTypes = { completedChallengesIds: PropTypes.array, currentBlockIds: PropTypes.array, files: PropTypes.object.isRequired, - handleKeypress: PropTypes.func.isRequired, id: PropTypes.string, isOpen: PropTypes.bool, isSignedIn: PropTypes.bool.isRequired, + lastBlockChalSubmitted: PropTypes.func, message: PropTypes.string, submitChallenge: PropTypes.func.isRequired, title: PropTypes.string }; export function getCompletedPercent( - completedChallengesIds, - currentBlockIds, + completedChallengesIds = [], + currentBlockIds = [], currentChallengeId ) { completedChallengesIds = completedChallengesIds.includes(currentChallengeId) @@ -106,8 +101,15 @@ export function getCompletedPercent( } export class CompletionModalInner extends Component { + constructor(props) { + super(props); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleKeypress = this.handleKeypress.bind(this); + } + state = { - downloadURL: null + downloadURL: null, + completedPercent: 0 }; static getDerivedStateFromProps(props, state) { @@ -135,7 +137,37 @@ export class CompletionModalInner extends Component { }); newURL = URL.createObjectURL(blob); } - return { downloadURL: newURL }; + + const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props; + let completedPercent = isSignedIn + ? getCompletedPercent(completedChallengesIds, currentBlockIds, id) + : 0; + return { downloadURL: newURL, completedPercent: completedPercent }; + } + + handleKeypress(e) { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + // Since Hotkeys also listens to Ctrl + Enter we have to stop this event + // getting to it. + e.stopPropagation(); + this.handleSubmit(); + } + } + + handleSubmit() { + this.props.submitChallenge(); + this.checkBlockCompletion(); + } + + // check block completion for donation + checkBlockCompletion() { + if ( + this.state.completedPercent === 100 && + !this.props.completedChallengesIds.includes(this.props.id) + ) { + this.props.lastBlockChalSubmitted(); + } } componentWillUnmount() { @@ -149,20 +181,13 @@ export class CompletionModalInner extends Component { const { blockName = '', close, - completedChallengesIds = [], - currentBlockIds = [], - id = '', isOpen, - isSignedIn, - submitChallenge, - handleKeypress, message, - title + title, + isSignedIn } = this.props; - const completedPercent = !isSignedIn - ? 0 - : getCompletedPercent(completedChallengesIds, currentBlockIds, id); + const { completedPercent } = this.state; if (isOpen) { ga.modalview('/completion-modal'); @@ -175,7 +200,7 @@ export class CompletionModalInner extends Component { dialogClassName='challenge-success-modal' keyboard={true} onHide={close} - onKeyDown={isOpen ? handleKeypress : noop} + onKeyDown={isOpen ? this.handleKeypress : noop} show={isOpen} > {isSignedIn ? 'Submit and g' : 'G'}o to next challenge{' '} (Ctrl + Enter) diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js index 0c9ee9dada..40822eebd7 100644 --- a/client/src/templates/Challenges/redux/completion-epic.js +++ b/client/src/templates/Challenges/redux/completion-epic.js @@ -132,6 +132,7 @@ export default function completionEpic(action$, state$) { const meta = challengeMetaSelector(state); const { nextChallengePath, introPath, challengeType } = meta; const closeChallengeModal = of(closeModal('completion')); + let submitter = () => of({ type: 'no-user-signed-in' }); if ( !(challengeType in submitTypes) || diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.js b/client/src/templates/Challenges/redux/current-challenge-saga.js index 685cad5191..92cbeb35a3 100644 --- a/client/src/templates/Challenges/redux/current-challenge-saga.js +++ b/client/src/templates/Challenges/redux/current-challenge-saga.js @@ -4,7 +4,8 @@ import store from 'store'; import { isSignedInSelector, updateComplete, - updateFailed + updateFailed, + allowDonationRequests } from '../../../redux'; import { post } from '../../../utils/ajax'; @@ -33,13 +34,18 @@ export function* currentChallengeSaga({ payload: id }) { } } -function* updateSuccessMessageSaga() { +export function* updateSuccessMessageSaga() { yield put(updateSuccessMessage(randomCompliment())); } +export function* allowDonationRequestsSaga() { + yield put(allowDonationRequests()); +} + export function createCurrentChallengeSaga(types) { return [ takeEvery(types.challengeMounted, currentChallengeSaga), - takeEvery(types.challengeMounted, updateSuccessMessageSaga) + takeEvery(types.challengeMounted, updateSuccessMessageSaga), + takeEvery(types.lastBlockChalSubmitted, allowDonationRequestsSaga) ]; } diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.test.js b/client/src/templates/Challenges/redux/current-challenge-saga.test.js new file mode 100644 index 0000000000..0e4d415ee1 --- /dev/null +++ b/client/src/templates/Challenges/redux/current-challenge-saga.test.js @@ -0,0 +1,12 @@ +/* global expect */ +import { allowDonationRequestsSaga } from './current-challenge-saga'; +import { types as appTypes } from '../../../redux'; + +describe('allowDonationRequestsSaga', () => { + it('should call allowDonationRequests', () => { + const gen = allowDonationRequestsSaga(); + expect(gen.next().value.payload.action.type).toEqual( + appTypes.allowDonationRequests + ); + }); +}); diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 7c12537257..00d7381dbc 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -30,6 +30,7 @@ const initialState = { }, challengeTests: [], consoleOut: '', + hasCompletedBlock: false, inAccessibilityMode: false, isCodeLocked: false, isBuildEnabled: true, @@ -81,7 +82,9 @@ export const types = createTypes( 'moveToTab', 'setEditorFocusability', - 'setAccessibilityMode' + 'setAccessibilityMode', + + 'lastBlockChalSubmitted' ], ns ); @@ -157,6 +160,10 @@ export const moveToTab = createAction(types.moveToTab); export const setEditorFocusability = createAction(types.setEditorFocusability); export const setAccessibilityMode = createAction(types.setAccessibilityMode); +export const lastBlockChalSubmitted = createAction( + types.lastBlockChalSubmitted +); + export const currentTabSelector = state => state[ns].currentTab; export const challengeFilesSelector = state => state[ns].challengeFiles; export const challengeMetaSelector = state => state[ns].challengeMeta; diff --git a/client/utils/gatsby/layoutSelector.js b/client/utils/gatsby/layoutSelector.js index 455d11c227..91b98f3814 100644 --- a/client/utils/gatsby/layoutSelector.js +++ b/client/utils/gatsby/layoutSelector.js @@ -15,7 +15,9 @@ export default function layoutSelector({ element, props }) { return {element}; } if (/^\/certification(\/.*)*/.test(pathname)) { - return {element}; + return ( + {element} + ); } if (/^\/guide(\/.*)*/.test(pathname)) { console.log('Hitting guide for some reason. Need a redirect.');