From 3f075f91d86c22fb44a1c75c90d8a3ad5e5cec8d Mon Sep 17 00:00:00 2001 From: Ahmad Abdolsaheb Date: Mon, 9 Dec 2019 17:30:24 +0100 Subject: [PATCH] feat: activate progress based donation modal (#37882) --- client/src/assets/icons/Cup.js | 59 ++++++++ client/src/components/Donation/Donation.css | 13 +- .../Donation/components/DonationModal.js | 132 +++++++++++------- client/src/redux/donation-saga.js | 15 +- client/src/redux/index.js | 68 +++++++-- .../redux/current-challenge-saga.js | 8 +- .../redux/current-challenge-saga.test.js | 10 +- 7 files changed, 222 insertions(+), 83 deletions(-) create mode 100644 client/src/assets/icons/Cup.js diff --git a/client/src/assets/icons/Cup.js b/client/src/assets/icons/Cup.js new file mode 100644 index 0000000000..8e0914ac56 --- /dev/null +++ b/client/src/assets/icons/Cup.js @@ -0,0 +1,59 @@ +/* eslint-disable max-len */ +import React, { Fragment } from 'react'; + +const propTypes = {}; + +function Cup(props) { + return ( + + Gold Cup + + Gold Cup + + + + + + + + + + + + + + + + + ); +} + +Cup.displayName = 'Cup'; +Cup.propTypes = propTypes; + +export default Cup; diff --git a/client/src/components/Donation/Donation.css b/client/src/components/Donation/Donation.css index 1afc60d059..c1f03f70cd 100644 --- a/client/src/components/Donation/Donation.css +++ b/client/src/components/Donation/Donation.css @@ -176,7 +176,7 @@ li.disabled > a { } } -.heart-icon-container { +.donation-icon-container { display: flex; flex-direction: column; align-items: center; @@ -184,15 +184,15 @@ li.disabled > a { margin: 40px; } -.heart-icon { +.donation-icon { width: 150px; height: auto; transform: scale(1.5); opacity: 0; - animation: heart-icon-animation 1s linear 100ms forwards; + animation: donation-icon-animation 1s linear 100ms forwards; } -@keyframes heart-icon-animation { +@keyframes donation-icon-animation { 33% { transform: scale(1.2); } @@ -206,10 +206,9 @@ li.disabled > a { } .donation-modal p { - margin: 0; text-align: center; font-weight: 700; - font-size: 1.2rem; + font-size: 1.1rem; } .donation-modal .modal-title { @@ -219,7 +218,7 @@ li.disabled > a { } @media screen and (max-width: 991px) { - .heart-icon-container { + .donation-icon-container { margin: 30px; } .donation-modal p { diff --git a/client/src/components/Donation/components/DonationModal.js b/client/src/components/Donation/components/DonationModal.js index 400063ed93..d84a067635 100644 --- a/client/src/components/Donation/components/DonationModal.js +++ b/client/src/components/Donation/components/DonationModal.js @@ -1,4 +1,5 @@ -import React, { Component } from 'react'; +/* eslint-disable max-len */ +import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -7,11 +8,14 @@ import { Modal, Button } from '@freecodecamp/react-bootstrap'; import { Link } from '../../../components/helpers'; import { blockNameify } from '../../../../utils/blockNameify'; import Heart from '../../../assets/icons/Heart'; +import Cup from '../../../assets/icons/Cup'; import ga from '../../../analytics'; import { closeDonationModal, - isDonationModalOpenSelector + isDonationModalOpenSelector, + isBlockDonationModalSelector, + activeDonationsSelector } from '../../../redux'; import { challengeMetaSelector } from '../../../templates/Challenges/redux'; @@ -21,9 +25,13 @@ import '../Donation.css'; const mapStateToProps = createSelector( isDonationModalOpenSelector, challengeMetaSelector, - (show, { block }) => ({ + isBlockDonationModalSelector, + activeDonationsSelector, + (show, { block }, isBlockDonation, activeDonors) => ({ show, - block + block, + isBlockDonation, + activeDonors }) ); @@ -36,57 +44,83 @@ const mapDispatchToProps = dispatch => ); const propTypes = { + activeDonors: PropTypes.number, block: PropTypes.string, closeDonationModal: PropTypes.func.isRequired, + isBlockDonation: PropTypes.bool, 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 - - - -
- ); +function DonateModal({ + show, + block, + activeDonors, + isBlockDonation, + closeDonationModal +}) { + if (show) { + ga.modalview('/donation-modal'); } + const blockDonationText = ( +
+
+ +
+

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

+

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

+
+ ); + + const progressDonationText = ( +
+
+ +
+

+ freeCodeCamp.org is a tiny nonprofit that's helping millions of people + learn to code for free. +

+

+ Join {activeDonors} supporters. +

+

Your donation will help keep tech education free and open.

+
+ ); + + return ( + + + + Support freeCodeCamp.org + + + + {isBlockDonation ? blockDonationText : progressDonationText} + + + + Support our nonprofit + + + + + ); } DonateModal.displayName = 'DonateModal'; diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js index 1657302127..a8d3ef06a6 100644 --- a/client/src/redux/donation-saga.js +++ b/client/src/redux/donation-saga.js @@ -2,16 +2,23 @@ import { put, select, takeEvery, delay } from 'redux-saga/effects'; import { openDonationModal, - preventDonationRequests, - shouldRequestDonationSelector + preventBlockDonationRequests, + shouldRequestDonationSelector, + preventProgressDonationRequests, + canRequestBlockDonationSelector } from './'; function* showDonateModalSaga() { let shouldRequestDonation = yield select(shouldRequestDonationSelector); if (shouldRequestDonation) { yield delay(200); - yield put(openDonationModal()); - yield put(preventDonationRequests()); + const isBlockDonation = yield select(canRequestBlockDonationSelector); + yield put(openDonationModal(isBlockDonation)); + if (isBlockDonation) { + yield put(preventBlockDonationRequests()); + } else { + yield put(preventProgressDonationRequests()); + } } } diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 338808dde3..b67bf24b37 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -32,7 +32,8 @@ export const defaultFetchState = { const initialState = { appUsername: '', - canRequestDonation: false, + canRequestBlockDonation: false, + canRequestProgressDonation: true, completionCount: 0, currentChallengeId: store.get(CURRENT_CHALLENGE_KEY), showCert: {}, @@ -48,6 +49,7 @@ const initialState = { }, sessionMeta: { activeDonations: 0 }, showDonationModal: false, + isBlockDonationModal: false, isOnline: true }; @@ -55,9 +57,10 @@ export const types = createTypes( [ 'appMount', 'hardGoTo', - 'allowDonationRequests', + 'allowBlockDonationRequests', 'closeDonationModal', - 'preventDonationRequests', + 'preventBlockDonationRequests', + 'preventProgressDonationRequests', 'openDonationModal', 'onlineStatusChange', 'resetUserData', @@ -92,11 +95,16 @@ export const appMount = createAction(types.appMount); export const tryToShowDonationModal = createAction( types.tryToShowDonationModal ); -export const allowDonationRequests = createAction(types.allowDonationRequests); +export const allowBlockDonationRequests = createAction( + types.allowBlockDonationRequests +); export const closeDonationModal = createAction(types.closeDonationModal); export const openDonationModal = createAction(types.openDonationModal); -export const preventDonationRequests = createAction( - types.preventDonationRequests +export const preventBlockDonationRequests = createAction( + types.preventBlockDonationRequests +); +export const preventProgressDonationRequests = createAction( + types.preventProgressDonationRequests ); export const onlineStatusChange = createAction(types.onlineStatusChange); @@ -149,14 +157,41 @@ export const isDonatingSelector = state => userSelector(state).isDonating; export const isOnlineSelector = state => state[ns].isOnline; export const isSignedInSelector = state => !!state[ns].appUsername; export const isDonationModalOpenSelector = state => state[ns].showDonationModal; +export const canRequestBlockDonationSelector = state => + state[ns].canRequestBlockDonation; +export const isBlockDonationModalSelector = state => + state[ns].isBlockDonationModal; export const signInLoadingSelector = state => userFetchStateSelector(state).pending; export const showCertSelector = state => state[ns].showCert; export const showCertFetchStateSelector = state => state[ns].showCertFetchState; -export const shouldRequestDonationSelector = state => - !isDonatingSelector(state) && state[ns].canRequestDonation; +export const shouldRequestDonationSelector = state => { + const completedChallenges = completedChallengesSelector(state); + const completionCount = completionCountSelector(state); + const canRequestProgressDonation = state[ns].canRequestProgressDonation; + const isDonating = isDonatingSelector(state); + const canRequestBlockDonation = canRequestBlockDonationSelector(state); + + // don't request donation if already donating + if (isDonating) return false; + + // a block has been completed + if (canRequestBlockDonation) return true; + + // a donation has already been requested + if (!canRequestProgressDonation) return false; + + // donations only appear after the user has completed ten challenges (i.e. + // not before the 11th challenge has mounted) + if (completedChallenges.length < 10) { + return false; + } + // this will mean we have completed 3 or more challenges this browser session + // and enough challenges overall to not be new + return completionCount >= 3; +}; export const userByNameSelector = username => state => { const { user } = state[ns]; @@ -203,9 +238,9 @@ function spreadThePayloadOnUser(state, payload) { export const reducer = handleActions( { - [types.allowDonationRequests]: state => ({ + [types.allowBlockDonationRequests]: state => ({ ...state, - canRequestDonation: true + canRequestBlockDonation: true }), [types.fetchUser]: state => ({ ...state, @@ -282,13 +317,18 @@ export const reducer = handleActions( ...state, showDonationModal: false }), - [types.openDonationModal]: state => ({ + [types.openDonationModal]: (state, { payload }) => ({ ...state, - showDonationModal: true + showDonationModal: true, + isBlockDonationModal: payload }), - [types.preventDonationRequests]: state => ({ + [types.preventBlockDonationRequests]: state => ({ ...state, - canRequestDonation: false + canRequestBlockDonation: false + }), + [types.preventProgressDonationRequests]: state => ({ + ...state, + canRequestProgressDonation: false }), [types.resetUserData]: state => ({ ...state, diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.js b/client/src/templates/Challenges/redux/current-challenge-saga.js index 92cbeb35a3..48de051c86 100644 --- a/client/src/templates/Challenges/redux/current-challenge-saga.js +++ b/client/src/templates/Challenges/redux/current-challenge-saga.js @@ -5,7 +5,7 @@ import { isSignedInSelector, updateComplete, updateFailed, - allowDonationRequests + allowBlockDonationRequests } from '../../../redux'; import { post } from '../../../utils/ajax'; @@ -38,14 +38,14 @@ export function* updateSuccessMessageSaga() { yield put(updateSuccessMessage(randomCompliment())); } -export function* allowDonationRequestsSaga() { - yield put(allowDonationRequests()); +export function* allowBlockDonationRequestsSaga() { + yield put(allowBlockDonationRequests()); } export function createCurrentChallengeSaga(types) { return [ takeEvery(types.challengeMounted, currentChallengeSaga), takeEvery(types.challengeMounted, updateSuccessMessageSaga), - takeEvery(types.lastBlockChalSubmitted, allowDonationRequestsSaga) + takeEvery(types.lastBlockChalSubmitted, allowBlockDonationRequestsSaga) ]; } diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.test.js b/client/src/templates/Challenges/redux/current-challenge-saga.test.js index 0e4d415ee1..5431b08a5e 100644 --- a/client/src/templates/Challenges/redux/current-challenge-saga.test.js +++ b/client/src/templates/Challenges/redux/current-challenge-saga.test.js @@ -1,12 +1,12 @@ /* global expect */ -import { allowDonationRequestsSaga } from './current-challenge-saga'; +import { allowBlockDonationRequestsSaga } from './current-challenge-saga'; import { types as appTypes } from '../../../redux'; -describe('allowDonationRequestsSaga', () => { - it('should call allowDonationRequests', () => { - const gen = allowDonationRequestsSaga(); +describe('allowBlockDonationRequestsSaga', () => { + it('should call allowBlockDonationRequests', () => { + const gen = allowBlockDonationRequestsSaga(); expect(gen.next().value.payload.action.type).toEqual( - appTypes.allowDonationRequests + appTypes.allowBlockDonationRequests ); }); });